Compare commits

..

677 Commits

Author SHA1 Message Date
37d408f762 Merge branch 'develop' of https://git.nefor.net/MIDNIGHT/enlight-lms into develop 2026-03-09 11:00:49 +00:00
6d7c91ceeb Before update v2.46.0 2026-03-09 10:54:44 +00:00
9041101505 Обновить README.md 2026-03-09 02:10:09 +03:00
Nicolai
d3fda0be37 StudentProfile new design 2026-03-08 23:31:54 +03:00
Nicolai
41de21201e LaderBoard design update 2026-03-08 12:48:34 +03:00
Alexandrina-Kuzeleva
005f85c34f UPD 7
- add links in Mobile
- switch off InstalPromt
2026-01-28 11:06:55 +03:00
Alexandrina-Kuzeleva
6bb6125e81 Update ParentProfile.vue 2025-12-05 16:45:49 +03:00
Alexandrina-Kuzeleva
4da2b844e8 Update ParentProfile.vue 2025-12-05 16:44:39 +03:00
Alexandrina-Kuzeleva
eba1923b7c Update ParentProfile.vue 2025-12-05 16:41:05 +03:00
Alexandrina-Kuzeleva
f28823dbe9 TEST UPD
-add parentprofile
2025-12-05 16:33:32 +03:00
Alexandrina-Kuzeleva
5d122bca7d upd 2025-12-05 16:22:23 +03:00
Alexandrina-Kuzeleva
ef4321586c TEST UPD 2025-12-05 16:13:38 +03:00
Alexandrina-Kuzeleva
336511dcd5 TEST UPD
- add traslation
- bug of doesntexist
2025-12-05 13:17:01 +03:00
Alexandrina-Kuzeleva
d73b6f9026 TEST UPD
- /api/method/frappe.client.get DoesNotExistError
2025-12-05 12:51:37 +03:00
Alexandrina-Kuzeleva
e959c0172d TEST UPD
-schoolchildprofile bug of learn_subjects
-front
2025-12-05 11:59:43 +03:00
Alexandrina-Kuzeleva
a15767c14f TEST UPD
-some traslation
-trest schoolchildrenprofile
-frontworks
2025-12-04 18:57:34 +03:00
Alexandrina-Kuzeleva
20b1743223 TEST UPD
- course creator front
- tg icon
2025-12-04 16:06:39 +03:00
Alexandrina-Kuzeleva
a89930fae6 TEST UPD
-translation
-front works
2025-12-04 15:29:50 +03:00
Alexandrina-Kuzeleva
60e81a921e TEST UPD
- translate leader board
2025-12-04 15:13:11 +03:00
Alexandrina-Kuzeleva
36f75beea9 TEST UPD
-test bio
2025-12-04 15:10:29 +03:00
Alexandrina-Kuzeleva
01a9eab73d TEST UPD
- front works
2025-12-04 14:17:57 +03:00
Alexandrina-Kuzeleva
a4eff5ae38 TEST UPD 2025-12-03 22:52:16 +03:00
Alexandrina-Kuzeleva
46b5495167 TEST UPD
- tranlation My Points
- front works
2025-12-03 22:44:25 +03:00
Alexandrina-Kuzeleva
7c9ef2a702 TEST UPD
-front works for student profile
2025-12-03 22:10:43 +03:00
Alexandrina-Kuzeleva
ee9aed6bbc Update Test.vue 2025-11-28 11:00:45 +03:00
Alexandrina-Kuzeleva
eb4cf6e2db Update Test.vue 2025-11-28 10:57:54 +03:00
Alexandrina-Kuzeleva
c6ad6b495c Update api.py 2025-11-28 10:50:48 +03:00
Alexandrina-Kuzeleva
5499a86854 Update Test.vue 2025-11-28 10:46:51 +03:00
Alexandrina-Kuzeleva
627ccd8214 Update Test.vue 2025-11-28 10:42:35 +03:00
Alexandrina-Kuzeleva
e760d59d9f Update Test.vue
-front works
- fix bonus points
2025-11-28 10:39:04 +03:00
Alexandrina-Kuzeleva
e76858121f TEST UPD
- front works
2025-11-28 10:27:03 +03:00
Alexandrina-Kuzeleva
34f1d02803 TEST UPD
- add roles permision in api method
2025-11-28 10:11:30 +03:00
Alexandrina-Kuzeleva
64610050ca TEST UPD
- current user card
2025-11-27 16:50:33 +03:00
Alexandrina-Kuzeleva
f5bd52a94d Update Test.vue 2025-11-26 18:17:17 +03:00
Alexandrina-Kuzeleva
ce603cac1e Update Test.vue 2025-11-26 18:09:45 +03:00
Alexandrina-Kuzeleva
3108235521 TEST UPD 2025-11-26 17:51:42 +03:00
Alexandrina-Kuzeleva
280aaecf76 Update Test.vue 2025-11-26 17:41:18 +03:00
Alexandrina-Kuzeleva
ba0bb1eabc Update Test.vue 2025-11-26 17:36:32 +03:00
Alexandrina-Kuzeleva
73d0755249 Update Test.vue 2025-11-26 17:27:33 +03:00
Alexandrina-Kuzeleva
4d93dcb9b4 TEST 2025-11-26 17:21:35 +03:00
Alexandrina-Kuzeleva
1fc9b8e279 TEST 2025-11-26 17:15:09 +03:00
Alexandrina-Kuzeleva
c6d05111cc Update LeaderBoard.vue 2025-11-25 15:34:35 +03:00
Alexandrina-Kuzeleva
8fa3d8ba4a Update LeaderBoard.vue 2025-11-25 15:30:22 +03:00
Alexandrina-Kuzeleva
c5317beb3f TEST UPD 2025-11-25 15:28:23 +03:00
Alexandrina-Kuzeleva
7d82e36790 Update Test.vue 2025-11-25 15:18:58 +03:00
Alexandrina-Kuzeleva
f39867b0e2 Update Test.vue 2025-11-25 15:12:07 +03:00
Alexandrina-Kuzeleva
54cef503ad TEST UPD
- add test page
2025-11-25 15:08:07 +03:00
Alexandrina-Kuzeleva
ce51371e62 TEST UPD
- roles
2025-11-25 13:14:13 +03:00
Alexandrina-Kuzeleva
7aabbbd497 Update LeaderBoard.vue 2025-11-25 13:07:26 +03:00
Alexandrina-Kuzeleva
02b89ea137 TEST UPD
- debug
- user role
2025-11-25 13:00:10 +03:00
Alexandrina-Kuzeleva
119a48f3a3 Update LeaderBoard.vue 2025-11-25 12:37:50 +03:00
Alexandrina-Kuzeleva
3146a0354c Update LeaderBoard.vue 2025-11-25 12:34:06 +03:00
Alexandrina-Kuzeleva
8e895a9890 TEST UPD
- debug
2025-11-25 11:55:43 +03:00
Alexandrina-Kuzeleva
ac436cbf79 TEST UPD
- roles
2025-11-25 11:23:15 +03:00
Alexandrina-Kuzeleva
4363aa7734 TEST UPD
- дурацкие названия файлов ненавижу
2025-11-25 11:10:29 +03:00
Alexandrina-Kuzeleva
a65cb073b5 TEST UPD
- try to add imgs
2025-11-25 10:58:57 +03:00
Alexandrina-Kuzeleva
a3b9e4f7b2 TEST UPD
- add front
2025-11-25 10:51:18 +03:00
Alexandrina-Kuzeleva
ebde8a0171 TEST UPD
- try to do anthore logic of script
2025-11-25 10:26:39 +03:00
Alexandrina-Kuzeleva
684299ac3b Update LeaderBoard.vue 2025-11-21 16:47:34 +03:00
Alexandrina-Kuzeleva
c449aef7ae Update LeaderBoard.vue 2025-11-21 16:41:05 +03:00
Alexandrina-Kuzeleva
879a27ed0a TEST UPD
- debug
2025-11-21 16:37:03 +03:00
Alexandrina-Kuzeleva
107e7a4e31 TEST UPD
- test leader board
2025-11-21 16:28:33 +03:00
Alexandrina-Kuzeleva
fa0325106a Update MyPoints.vue 2025-11-21 15:57:41 +03:00
Alexandrina-Kuzeleva
bbfce9363f TEST UPD
- font works
2025-11-21 15:47:58 +03:00
Alexandrina-Kuzeleva
10a6280b78 TEST UPD
- change weekly sum
2025-11-21 15:37:08 +03:00
Alexandrina-Kuzeleva
08e8724b4c UPD 6.1
- delete func
2025-11-21 15:29:07 +03:00
Alexandrina-Kuzeleva
555c7e4e2d TEST UPD
- add today weekly points
2025-11-21 15:18:41 +03:00
Alexandrina-Kuzeleva
3673026a33 TEST UPD
- add load more
2025-11-21 14:50:00 +03:00
Alexandrina-Kuzeleva
cd565ec160 Update MyPoints.vue 2025-11-20 16:44:05 +03:00
Alexandrina-Kuzeleva
bdcbae03ef TEST UPD
- front
2025-11-20 16:40:42 +03:00
Alexandrina-Kuzeleva
296234a093 TEST UPD
- remove upd
2025-11-20 16:36:39 +03:00
Alexandrina-Kuzeleva
10c0955c6c TEST UPD
- add load more
- add front
2025-11-20 16:28:21 +03:00
Alexandrina-Kuzeleva
8ba2bfda63 TEXT UPD
- front
2025-11-20 16:17:10 +03:00
Alexandrina-Kuzeleva
cb06cc53c2 TEST UPD
- front works
2025-11-20 15:53:36 +03:00
Alexandrina-Kuzeleva
826828ba30 TEST UPD
- text-white remove
2025-11-20 15:34:24 +03:00
Alexandrina-Kuzeleva
22de38c72b TEST UPD
- try to fix bug of sum points
2025-11-20 15:23:59 +03:00
Alexandrina-Kuzeleva
0037c01beb TEST UPD
- problem of date
2025-11-20 13:35:11 +03:00
Alexandrina-Kuzeleva
fb17c666a9 TEST UPD
- add my points
2025-11-20 13:29:09 +03:00
Alexandrina-Kuzeleva
2c32fac1f2 TEST UPD
- try to get resource
2025-11-20 13:17:23 +03:00
Alexandrina-Kuzeleva
3f0b00decd TEST UPD
- another logic of download user and data
2025-11-20 13:06:55 +03:00
Alexandrina-Kuzeleva
160c7863f0 TEST UPD
- My points page
2025-11-20 12:56:28 +03:00
Alexandrina-Kuzeleva
c4d185f2d6 UPD 6
- add locale file, ru
- AI in Quizes, now it is off
- bug of translation in Courses
- fronted of tags in Course Detail in Mobile ver
- another locale bugs
2025-11-20 12:44:53 +03:00
Alexandrina-Kuzeleva
6b13b1231a UPD 5
- add Profiles for Student, Schoolchildren, Parent, Course Creator
2025-11-12 17:18:03 +03:00
Alexandrina-Kuzeleva
661137d500 UPD 4.1
- error
2025-11-11 17:38:06 +03:00
Alexandrina-Kuzeleva
962dcc1ce9 UPD 4
- add MobileLayout changes
2025-11-11 17:27:09 +03:00
Alexandrina-Kuzeleva
655df62d6c UPD 3.1
- add imports in ProfileAbout
2025-11-10 17:34:30 +03:00
Alexandrina-Kuzeleva
d827a10c84 UPD 3
- checked and fixed problems: timer in lesson, sing-up, page length in courses
- add Points and Courses in ProfileAbout
NEW:
- add icon for RuTube
2025-11-10 17:28:26 +03:00
Alexandrina-Kuzeleva
25c640fabb UPD 2
- add RuTube servise
2025-11-10 17:00:10 +03:00
Alexandrina-Kuzeleva
0cb8d21290 UPD 1
- add all AppSidebar links that used
- add singup form
2025-11-10 12:27:28 +03:00
Jannat Patel
7a47591967 Merge pull request #1819 from rehanrehman389/misc-fix
fix: UI improvements
2025-11-06 16:43:28 +05:30
Jannat Patel
6931ca27c3 Merge pull request #1824 from pateljannat/issues-145
fix: roles, permission and access on profile page
2025-11-06 12:51:45 +05:30
Jannat Patel
d00d2de1cc fix: export livecodeURL from settings store 2025-11-06 12:28:27 +05:30
Jannat Patel
b1be568991 fix: removed uncalled function 2025-11-06 12:23:18 +05:30
Jannat Patel
28be3891d2 fix: roles, permission and access on profile page 2025-11-06 12:21:12 +05:30
Jannat Patel
27d2297e2b Merge pull request #1823 from pateljannat/issues-144
fix: misc improvements
2025-11-05 12:50:21 +05:30
Jannat Patel
7212ddd5c5 fix: evaluators and modetators can now see schedule of other evaluators 2025-11-05 12:34:40 +05:30
Jannat Patel
f4e9ac5bf1 fix: IPhone PWA install prompt 2025-11-05 11:59:45 +05:30
Jannat Patel
8fec484d66 Merge pull request #1818 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-11-04 13:14:49 +05:30
Jannat Patel
bcf781c37b chore: Serbian (Latin) translations 2025-11-03 00:37:46 +05:30
Jannat Patel
d8a8e689d0 chore: Bosnian translations 2025-11-03 00:37:45 +05:30
Jannat Patel
a844b95de3 chore: Burmese translations 2025-11-03 00:37:44 +05:30
Jannat Patel
ece885f973 chore: Thai translations 2025-11-03 00:37:42 +05:30
Jannat Patel
66dd30604b chore: Tamil translations 2025-11-03 00:37:41 +05:30
Jannat Patel
d0f0f4905c chore: Indonesian translations 2025-11-03 00:37:40 +05:30
Jannat Patel
c9cb6702b6 chore: Portuguese, Brazilian translations 2025-11-03 00:37:39 +05:30
Jannat Patel
1ddb980242 chore: Vietnamese translations 2025-11-03 00:37:37 +05:30
Jannat Patel
94b626a4d2 chore: Chinese Simplified translations 2025-11-03 00:37:36 +05:30
Jannat Patel
d2a011462d chore: Turkish translations 2025-11-03 00:37:35 +05:30
Jannat Patel
4c34926af0 chore: Serbian (Cyrillic) translations 2025-11-03 00:37:34 +05:30
Jannat Patel
ce35cd1009 chore: Russian translations 2025-11-03 00:37:32 +05:30
Jannat Patel
56d072bd06 chore: Portuguese translations 2025-11-03 00:37:31 +05:30
Jannat Patel
5d336ef669 chore: Polish translations 2025-11-03 00:37:30 +05:30
Jannat Patel
b47c59eac1 chore: Dutch translations 2025-11-03 00:37:29 +05:30
Jannat Patel
87285db361 chore: Italian translations 2025-11-03 00:37:27 +05:30
Jannat Patel
84312e498c chore: Hungarian translations 2025-11-03 00:37:26 +05:30
Jannat Patel
bd763d9462 chore: German translations 2025-11-03 00:37:25 +05:30
Jannat Patel
a00e66f786 chore: Czech translations 2025-11-03 00:37:24 +05:30
Jannat Patel
78c7b52088 chore: Arabic translations 2025-11-03 00:37:23 +05:30
Jannat Patel
c3a5bee993 chore: Spanish translations 2025-11-03 00:37:21 +05:30
Jannat Patel
c2b5b7c3e2 chore: French translations 2025-11-03 00:37:20 +05:30
Jannat Patel
3992f00353 chore: Persian translations 2025-11-03 00:37:19 +05:30
Jannat Patel
97d853e0d3 chore: Danish translations 2025-11-03 00:37:18 +05:30
Jannat Patel
f786cec75f chore: Norwegian Bokmal translations 2025-11-03 00:37:16 +05:30
Jannat Patel
07cd08b55e chore: Croatian translations 2025-11-03 00:37:15 +05:30
Jannat Patel
ca42faf14a chore: Swedish translations 2025-11-03 00:37:14 +05:30
Rehan Ansari
87f5b68279 fix: UI improvements 2025-11-02 13:43:25 +05:30
Jannat Patel
6b31edb687 chore: Esperanto translations 2025-10-31 23:03:40 +05:30
Jannat Patel
6a64048bb6 Merge pull request #1817 from frappe/pot_develop_2025-10-31
chore: update POT file
2025-10-31 22:03:56 +05:30
frappe-pr-bot
6cf069ee6a chore: update POT file 2025-10-31 16:04:36 +00:00
Jannat Patel
3b74bba6ab Merge pull request #1813 from rehanrehman389/missing-type-fix
fix: add missing type prop
2025-10-30 11:13:31 +05:30
Rehan Ansari
8689788523 fix: add missing type prop 2025-10-29 21:41:48 +05:30
Frappe PR Bot
1193776d06 chore(release): Bumped to Version 2.39.2 2025-10-29 12:29:14 +00:00
Jannat Patel
022514a0a7 Merge pull request #1810 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-28 10:56:10 +05:30
Jannat Patel
dc7f8a59ed chore: Esperanto translations 2025-10-27 22:34:42 +05:30
Jannat Patel
4e5a76a6c1 chore: Serbian (Latin) translations 2025-10-27 22:34:41 +05:30
Jannat Patel
64c4a25ee8 chore: Bosnian translations 2025-10-27 22:34:39 +05:30
Jannat Patel
47bbdbaa26 chore: Burmese translations 2025-10-27 22:34:38 +05:30
Jannat Patel
f8e0c0e19a chore: Thai translations 2025-10-27 22:34:37 +05:30
Jannat Patel
94cdd19224 chore: Tamil translations 2025-10-27 22:34:35 +05:30
Jannat Patel
d86d046eb0 chore: Indonesian translations 2025-10-27 22:34:34 +05:30
Jannat Patel
25ec6b5a3f chore: Portuguese, Brazilian translations 2025-10-27 22:34:32 +05:30
Jannat Patel
967453a683 chore: Vietnamese translations 2025-10-27 22:34:30 +05:30
Jannat Patel
4c17305c05 chore: Chinese Simplified translations 2025-10-27 22:34:29 +05:30
Jannat Patel
6092131303 chore: Turkish translations 2025-10-27 22:34:28 +05:30
Jannat Patel
35749834d0 chore: Serbian (Cyrillic) translations 2025-10-27 22:34:26 +05:30
Jannat Patel
fe56c7b887 chore: Russian translations 2025-10-27 22:34:25 +05:30
Jannat Patel
2b58a744d2 chore: Portuguese translations 2025-10-27 22:34:24 +05:30
Jannat Patel
dfb94d05e4 chore: Polish translations 2025-10-27 22:34:22 +05:30
Jannat Patel
987c1790d8 chore: Dutch translations 2025-10-27 22:34:21 +05:30
Jannat Patel
0d416b17ce chore: Italian translations 2025-10-27 22:34:19 +05:30
Jannat Patel
473e165c89 chore: Hungarian translations 2025-10-27 22:34:18 +05:30
Jannat Patel
3d52d15004 chore: German translations 2025-10-27 22:34:17 +05:30
Jannat Patel
27278e128c chore: Czech translations 2025-10-27 22:34:16 +05:30
Jannat Patel
13cee3c9b3 chore: Arabic translations 2025-10-27 22:34:14 +05:30
Jannat Patel
fd95e42e9b chore: Spanish translations 2025-10-27 22:34:13 +05:30
Jannat Patel
65cd2f5d01 chore: French translations 2025-10-27 22:34:11 +05:30
Jannat Patel
70759d1888 chore: Persian translations 2025-10-27 22:34:10 +05:30
Jannat Patel
705d6e2f00 chore: Danish translations 2025-10-27 22:34:09 +05:30
Jannat Patel
088591a335 chore: Norwegian Bokmal translations 2025-10-27 22:34:07 +05:30
Jannat Patel
3f037e0d17 chore: Croatian translations 2025-10-27 22:34:06 +05:30
Jannat Patel
e6884b6c93 chore: Swedish translations 2025-10-27 22:34:04 +05:30
Jannat Patel
9943268ca0 Merge pull request #1809 from pateljannat/issues-143
fix: resolved the issue that appeared when adding a chapter
2025-10-27 11:50:25 +05:30
Jannat Patel
620e4d20c2 Merge pull request #1805 from rehanrehman389/dark-mode-fix
fix: multiple fixes for dark mode visibility
2025-10-27 11:39:26 +05:30
Jannat Patel
fd03033ac6 Merge pull request #1804 from frappe/pot_develop_2025-10-24
chore: update POT file
2025-10-27 11:37:35 +05:30
Jannat Patel
939099b8c8 Merge pull request #1808 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-27 11:37:19 +05:30
Jannat Patel
75001b494d fix: escape HTML in job form fields 2025-10-27 11:36:46 +05:30
Jannat Patel
8749e21744 fix: only users with moderator and instructor role should have access to quiz form 2025-10-27 11:06:09 +05:30
Jannat Patel
982ac98e27 fix: resolved the issue that appeared when adding a chapter 2025-10-27 10:32:24 +05:30
Jannat Patel
f31bf17a41 chore: Polish translations 2025-10-26 21:43:16 +05:30
Rehan Ansari
3425d9118d fix: NotPermitted text visibility in dark mode 2025-10-26 00:19:46 +05:30
Rehan Ansari
6be49ecdf3 fix: multiple fixes for dark mode visibility 2025-10-25 15:38:08 +05:30
frappe-pr-bot
ffd6f9578b chore: update POT file 2025-10-24 16:04:25 +00:00
Jannat Patel
41293130ad Merge pull request #1803 from rehanrehman389/brand-setting
fix: handle missing file_url
2025-10-24 10:22:33 +05:30
Rehan Ansari
6cccd28b92 fix: handle missing file_url 2025-10-23 22:50:48 +05:30
Jannat Patel
384f10a722 Merge pull request #1801 from rehanrehman389/streak-fix
fix: improve visibility in dark mode
2025-10-23 18:37:36 +05:30
Jannat Patel
a603e299f1 Merge pull request #1800 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-23 18:36:46 +05:30
Rehan Ansari
05822f82da fix: streak number visibility in dark mode 2025-10-22 23:24:02 +05:30
Rehan Ansari
0508e718cb fix: job modal text in dark mode 2025-10-22 22:56:54 +05:30
Rehan Ansari
574913e9e4 fix: improve visibility in billing page 2025-10-22 22:35:01 +05:30
Rehan Ansari
068adb62a7 fix: improve visibility in dark mode 2025-10-22 21:55:03 +05:30
Jannat Patel
73fa1f9cfe chore: Serbian (Latin) translations 2025-10-22 21:17:52 +05:30
Jannat Patel
f518882926 chore: Serbian (Cyrillic) translations 2025-10-22 21:17:42 +05:30
Jannat Patel
ed566f9eea Merge pull request #1794 from rehanrehman389/setting-scroll-fix
fix: scroll issue in settings
2025-10-22 10:56:40 +05:30
Jannat Patel
8ca32e439a Merge pull request #1795 from rehanrehman389/notes-dark-mode
fix: update font color in dark mode for My Notes
2025-10-22 10:47:35 +05:30
Jannat Patel
35b3b11a3c Merge pull request #1798 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-22 10:46:45 +05:30
Jannat Patel
57d4a53081 chore: Bosnian translations 2025-10-21 21:17:36 +05:30
Jannat Patel
6da05961f2 chore: Polish translations 2025-10-21 21:17:35 +05:30
Jannat Patel
7db3b8c5b8 chore: Danish translations 2025-10-21 21:17:32 +05:30
Jannat Patel
50bafb6fa6 chore: Croatian translations 2025-10-21 21:17:31 +05:30
Jannat Patel
2b3a9072d1 Merge pull request #1793 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-21 21:11:41 +05:30
Rehan Ansari
1c08e57086 fix: update font color in dark mode for My Notes 2025-10-21 13:57:52 +05:30
Rehan Ansari
4290ed2f04 fix: scroll issue in settings 2025-10-20 21:42:41 +05:30
Jannat Patel
342512f3e1 chore: Esperanto translations 2025-10-20 20:49:25 +05:30
Jannat Patel
942c04cb68 chore: Serbian (Latin) translations 2025-10-20 20:49:23 +05:30
Jannat Patel
64bf4ab3f7 chore: Bosnian translations 2025-10-20 20:49:22 +05:30
Jannat Patel
052e69737e chore: Burmese translations 2025-10-20 20:49:21 +05:30
Jannat Patel
02adc4517c chore: Thai translations 2025-10-20 20:49:19 +05:30
Jannat Patel
e36fdd6823 chore: Tamil translations 2025-10-20 20:49:18 +05:30
Jannat Patel
d10a7ed57f chore: Indonesian translations 2025-10-20 20:49:16 +05:30
Jannat Patel
79adf44dfe chore: Portuguese, Brazilian translations 2025-10-20 20:49:15 +05:30
Jannat Patel
4bc3113f34 chore: Vietnamese translations 2025-10-20 20:49:14 +05:30
Jannat Patel
0826704282 chore: Chinese Simplified translations 2025-10-20 20:49:12 +05:30
Jannat Patel
52aa5e6954 chore: Turkish translations 2025-10-20 20:49:11 +05:30
Jannat Patel
fde85607d9 chore: Serbian (Cyrillic) translations 2025-10-20 20:49:09 +05:30
Jannat Patel
cc087af012 chore: Russian translations 2025-10-20 20:49:08 +05:30
Jannat Patel
2c7da1e32e chore: Portuguese translations 2025-10-20 20:49:06 +05:30
Jannat Patel
49fe8952ae chore: Polish translations 2025-10-20 20:49:05 +05:30
Jannat Patel
b298cd0509 chore: Dutch translations 2025-10-20 20:49:04 +05:30
Jannat Patel
a81fc11e73 chore: Italian translations 2025-10-20 20:49:02 +05:30
Jannat Patel
199fb6229d chore: Hungarian translations 2025-10-20 20:49:01 +05:30
Jannat Patel
ec6ecee455 chore: German translations 2025-10-20 20:48:59 +05:30
Jannat Patel
fa72172b77 chore: Czech translations 2025-10-20 20:48:58 +05:30
Jannat Patel
6789700def chore: Arabic translations 2025-10-20 20:48:56 +05:30
Jannat Patel
752744b3a4 chore: Spanish translations 2025-10-20 20:48:55 +05:30
Jannat Patel
c24fa85bf4 chore: French translations 2025-10-20 20:48:54 +05:30
Jannat Patel
4e0b59f6a9 chore: Persian translations 2025-10-20 20:48:52 +05:30
Jannat Patel
bd20214552 chore: Danish translations 2025-10-20 20:48:51 +05:30
Jannat Patel
4af0ea9e47 chore: Norwegian Bokmal translations 2025-10-20 20:48:49 +05:30
Jannat Patel
8651679634 chore: Croatian translations 2025-10-20 20:48:48 +05:30
Jannat Patel
99dcac6d12 chore: Swedish translations 2025-10-20 20:48:46 +05:30
Frappe PR Bot
853bf01c9e chore(release): Bumped to Version 2.39.1 2025-10-20 06:28:36 +00:00
Jannat Patel
39c5ad7267 Merge pull request #1788 from frappe/pot_develop_2025-10-17
chore: update POT file
2025-10-20 11:55:24 +05:30
Jannat Patel
8daa2948fa Merge pull request #1787 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-20 11:55:10 +05:30
Jannat Patel
f2ba25429e chore: Persian translations 2025-10-19 20:29:28 +05:30
Jannat Patel
1fffb4dc67 chore: Danish translations 2025-10-19 20:29:27 +05:30
frappe-pr-bot
45ce2439fd chore: update POT file 2025-10-17 16:04:25 +00:00
Jannat Patel
cb2e77e8f6 chore: Norwegian Bokmal translations 2025-10-17 20:15:15 +05:30
Jannat Patel
800c0b0336 chore: Croatian translations 2025-10-17 20:15:13 +05:30
Jannat Patel
14c23496d5 chore: Swedish translations 2025-10-17 20:15:12 +05:30
Jannat Patel
7756a6d593 fix: increased the rate limit 2025-10-17 15:59:32 +05:30
Jannat Patel
ae7791a204 Merge branch 'develop' of https://github.com/frappe/lms into develop 2025-10-15 14:45:14 +05:30
Jannat Patel
44232c44fc fix: activation level doctype name 2025-10-15 14:44:53 +05:30
Jannat Patel
142fc99761 Merge pull request #1783 from pateljannat/issues-140
fix: misc issues
2025-10-15 12:08:39 +05:30
Jannat Patel
5e6dc55c76 fix: added rate limit to all apis that can be accessed by guest 2025-10-15 11:55:58 +05:30
Jannat Patel
bb2447e821 fix: misc issues 2025-10-15 11:52:23 +05:30
Frappe PR Bot
a88d9cd78e chore(release): Bumped to Version 2.39.0 2025-10-15 05:13:17 +00:00
Jannat Patel
dab82db693 Merge pull request #1779 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-15 10:38:52 +05:30
Jannat Patel
a1183df72c Merge pull request #1780 from pateljannat/issues-139
fix: moderators should be able to access unpublished courses
2025-10-14 20:46:14 +05:30
Jannat Patel
5cfa4f173a fix: moderators should be able to access unpublished courses 2025-10-14 19:27:45 +05:30
Jannat Patel
451ef49d98 chore: Serbian (Latin) translations 2025-10-14 19:20:20 +05:30
Jannat Patel
36a8ebdc1b chore: Serbian (Cyrillic) translations 2025-10-14 19:20:07 +05:30
Jannat Patel
27577edb16 Merge pull request #1776 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-14 10:35:24 +05:30
Jannat Patel
bd69ab314b chore: Burmese translations 2025-10-13 19:07:28 +05:30
Jannat Patel
6abe4ac04a chore: Tamil translations 2025-10-13 19:07:26 +05:30
Jannat Patel
1eeb190653 chore: Esperanto translations 2025-10-13 19:07:24 +05:30
Jannat Patel
23f22d9d9a chore: Serbian (Latin) translations 2025-10-13 19:07:22 +05:30
Jannat Patel
b913f33b3f chore: Norwegian Bokmal translations 2025-10-13 19:07:21 +05:30
Jannat Patel
52da1feb91 chore: Bosnian translations 2025-10-13 19:07:18 +05:30
Jannat Patel
0af9f1bfbf chore: Croatian translations 2025-10-13 19:07:16 +05:30
Jannat Patel
3b0e1c3ce7 chore: Thai translations 2025-10-13 19:07:14 +05:30
Jannat Patel
37ea270c56 chore: Persian translations 2025-10-13 19:07:12 +05:30
Jannat Patel
304550dd94 chore: Indonesian translations 2025-10-13 19:07:11 +05:30
Jannat Patel
69cee24ffe chore: Portuguese, Brazilian translations 2025-10-13 19:07:09 +05:30
Jannat Patel
8334c06a9b chore: Vietnamese translations 2025-10-13 19:07:07 +05:30
Jannat Patel
3e739f2877 chore: Chinese Simplified translations 2025-10-13 19:07:05 +05:30
Jannat Patel
5e33ff4a34 chore: Turkish translations 2025-10-13 19:07:03 +05:30
Jannat Patel
2b234e5d64 chore: Swedish translations 2025-10-13 19:07:01 +05:30
Jannat Patel
c8b328a1c9 chore: Serbian (Cyrillic) translations 2025-10-13 19:07:00 +05:30
Jannat Patel
5e0ac05f90 chore: Russian translations 2025-10-13 19:06:58 +05:30
Jannat Patel
e440097272 chore: Portuguese translations 2025-10-13 19:06:57 +05:30
Jannat Patel
263c858a66 chore: Polish translations 2025-10-13 19:06:55 +05:30
Jannat Patel
82371fb2a8 chore: Dutch translations 2025-10-13 19:06:53 +05:30
Jannat Patel
b6336a4096 chore: Italian translations 2025-10-13 19:06:51 +05:30
Jannat Patel
7acfbbaae7 chore: Hungarian translations 2025-10-13 19:06:50 +05:30
Jannat Patel
5a76b4eb2d chore: German translations 2025-10-13 19:06:48 +05:30
Jannat Patel
8aac88b696 chore: Danish translations 2025-10-13 19:06:47 +05:30
Jannat Patel
d6e71068be chore: Czech translations 2025-10-13 19:06:45 +05:30
Jannat Patel
6b44951ef0 chore: Arabic translations 2025-10-13 19:06:43 +05:30
Jannat Patel
5289ebb923 chore: Spanish translations 2025-10-13 19:06:42 +05:30
Jannat Patel
263fcda053 chore: French translations 2025-10-13 19:06:40 +05:30
Jannat Patel
2e0d26575e Merge pull request #1775 from pateljannat/issues-138
fix: empty state for profile certificates section
2025-10-13 15:24:22 +05:30
Jannat Patel
b9d6670bee fix: timezone for direct evaluation courses 2025-10-13 15:08:29 +05:30
Jannat Patel
f20d39a3e7 fix: empty state for profile certificates section 2025-10-13 14:56:08 +05:30
Jannat Patel
09d948b3a0 Merge pull request #1774 from pateljannat/contact-us-mail-to
feat: contact us email modal
2025-10-13 13:22:26 +05:30
Jannat Patel
96941c83f3 fix: improved sidebar for settings 2025-10-13 13:14:13 +05:30
Jannat Patel
b8ca0e381a feat: contact us email modal 2025-10-13 11:34:46 +05:30
Jannat Patel
4a2c5d77aa Merge pull request #1770 from frappe/pot_develop_2025-10-10
chore: update POT file
2025-10-13 11:11:39 +05:30
Jannat Patel
cf2d29d82e Merge pull request #1771 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-13 11:11:27 +05:30
Jannat Patel
fb2ab63550 chore: Polish translations 2025-10-12 18:34:31 +05:30
Jannat Patel
90efc152a8 chore: Burmese translations 2025-10-11 17:41:57 +05:30
frappe-pr-bot
de6ba49409 chore: update POT file 2025-10-10 16:04:57 +00:00
Frappe PR Bot
9d4196f15a chore(release): Bumped to Version 2.38.0 2025-10-10 10:59:06 +00:00
Jannat Patel
eed7fb970d Merge pull request #1769 from pateljannat/issues-137
fix: private file uploads in assignment text editor
2025-10-10 16:27:00 +05:30
Jannat Patel
fe67f1ab61 Merge pull request #1767 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-10 15:55:51 +05:30
Jannat Patel
78640561f5 fix: private file uploads in assignment text editor 2025-10-10 15:52:35 +05:30
Jannat Patel
f72631a262 fix: reverted the alternative host change 2025-10-10 12:31:49 +05:30
Jannat Patel
670e5d0202 Merge pull request #1768 from pateljannat/issues-136
fix: misc zoom issues
2025-10-10 11:48:47 +05:30
Jannat Patel
ea59d1158a fix: misc zoom issues 2025-10-10 11:40:19 +05:30
Jannat Patel
ba23cf9789 fix: misc zoom issues 2025-10-10 11:39:40 +05:30
Jannat Patel
de585b90ea chore: Persian translations 2025-10-09 16:56:19 +05:30
Jannat Patel
cf40f4e525 Merge pull request #1766 from pateljannat/issues-135
fix: live class issues
2025-10-09 15:50:01 +05:30
Jannat Patel
b273e34ac8 fix: live class issues 2025-10-09 15:35:00 +05:30
Jannat Patel
1a00d708e1 Merge pull request #1762 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-08 17:10:46 +05:30
Jannat Patel
583584d8c5 Merge pull request #1763 from pateljannat/issues-134
fix: misc issues
2025-10-07 19:56:47 +05:30
Jannat Patel
09c087dee7 fix: misc issues 2025-10-07 19:24:47 +05:30
Jannat Patel
f5cff50674 chore: Burmese translations 2025-10-07 17:03:51 +05:30
Jannat Patel
81c48d5182 chore: Persian translations 2025-10-07 17:03:43 +05:30
Jannat Patel
c44414cadb chore: Portuguese, Brazilian translations 2025-10-07 17:03:41 +05:30
Jannat Patel
85db4be514 chore: Arabic translations 2025-10-07 17:03:24 +05:30
Jannat Patel
6526eefaf5 chore: French translations 2025-10-07 17:03:21 +05:30
Jannat Patel
bd1fc5d705 Merge pull request #1760 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-07 10:46:27 +05:30
Jannat Patel
ca547023b0 chore: Tamil translations 2025-10-06 16:21:31 +05:30
Jannat Patel
caf57d355b chore: Esperanto translations 2025-10-06 16:21:30 +05:30
Jannat Patel
7e6da62480 chore: Serbian (Latin) translations 2025-10-06 16:21:28 +05:30
Jannat Patel
e14e560415 chore: Norwegian Bokmal translations 2025-10-06 16:21:26 +05:30
Jannat Patel
0a8ac87cee chore: Bosnian translations 2025-10-06 16:21:25 +05:30
Jannat Patel
42441aafd6 chore: Croatian translations 2025-10-06 16:21:23 +05:30
Jannat Patel
9c3ff958e3 chore: Thai translations 2025-10-06 16:21:21 +05:30
Jannat Patel
efac7af750 chore: Persian translations 2025-10-06 16:21:20 +05:30
Jannat Patel
314935f68e chore: Indonesian translations 2025-10-06 16:21:18 +05:30
Jannat Patel
1efa857d95 chore: Portuguese, Brazilian translations 2025-10-06 16:21:16 +05:30
Jannat Patel
a7409b498e chore: Vietnamese translations 2025-10-06 16:21:13 +05:30
Jannat Patel
a9cb0a8c26 chore: Chinese Simplified translations 2025-10-06 16:21:12 +05:30
Jannat Patel
9333affaf1 chore: Turkish translations 2025-10-06 16:21:10 +05:30
Jannat Patel
38a32be503 chore: Swedish translations 2025-10-06 16:21:08 +05:30
Jannat Patel
fe5f7daf78 chore: Serbian (Cyrillic) translations 2025-10-06 16:20:58 +05:30
Jannat Patel
3c07c3e1cf chore: Russian translations 2025-10-06 16:20:57 +05:30
Jannat Patel
cb87c75ac0 chore: Portuguese translations 2025-10-06 16:20:55 +05:30
Jannat Patel
62ead16817 chore: Polish translations 2025-10-06 16:20:53 +05:30
Jannat Patel
6352e4deb1 chore: Dutch translations 2025-10-06 16:20:52 +05:30
Jannat Patel
0c4b569be6 chore: Italian translations 2025-10-06 16:20:51 +05:30
Jannat Patel
fe4d7cfb75 chore: Hungarian translations 2025-10-06 16:20:49 +05:30
Jannat Patel
d3e791b017 chore: German translations 2025-10-06 16:20:48 +05:30
Jannat Patel
0849183d26 chore: Danish translations 2025-10-06 16:20:46 +05:30
Jannat Patel
3410af8899 chore: Czech translations 2025-10-06 16:20:45 +05:30
Jannat Patel
81a1e3a4c3 chore: Arabic translations 2025-10-06 16:20:44 +05:30
Jannat Patel
c8e18dc445 chore: Spanish translations 2025-10-06 16:20:42 +05:30
Jannat Patel
ad21bd6f53 chore: French translations 2025-10-06 16:20:41 +05:30
Jannat Patel
781457fce3 Merge pull request #1759 from pateljannat/contact-us
feat: contact us
2025-10-06 11:11:19 +05:30
Jannat Patel
6662b713f1 Merge pull request #1757 from frappe/pot_develop_2025-10-03
chore: update POT file
2025-10-06 10:57:28 +05:30
Jannat Patel
34c0d16411 refactor: changed Certified Members to Certifications 2025-10-06 10:33:50 +05:30
Jannat Patel
f7003ecbbe feat: contact us 2025-10-06 10:15:53 +05:30
frappe-pr-bot
134090df5d chore: update POT file 2025-10-03 16:04:25 +00:00
Jannat Patel
efb4feab2e Merge pull request #1755 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-03 10:44:01 +05:30
Jannat Patel
900e959b0b chore: Tamil translations 2025-10-01 14:02:00 +05:30
Jannat Patel
946ffeb3ca chore: Portuguese, Brazilian translations 2025-10-01 14:01:52 +05:30
Jannat Patel
56d32b0674 chore: Vietnamese translations 2025-10-01 14:01:50 +05:30
Jannat Patel
03fc5c084a chore: Chinese Simplified translations 2025-10-01 14:01:48 +05:30
Jannat Patel
2a32355dd8 chore: Polish translations 2025-10-01 14:01:42 +05:30
Jannat Patel
d80d0e9d9b chore: Hungarian translations 2025-10-01 14:01:38 +05:30
Jannat Patel
be0388ee6e chore: Arabic translations 2025-10-01 14:01:33 +05:30
Jannat Patel
414c41162a chore: French translations 2025-10-01 14:01:31 +05:30
Frappe PR Bot
39106de96c chore(release): Bumped to Version 2.37.0 2025-10-01 07:23:35 +00:00
Jannat Patel
8adb76abfb Merge pull request #1753 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-01 12:52:01 +05:30
Jannat Patel
4889b04283 chore: Persian translations 2025-09-30 14:05:54 +05:30
Jannat Patel
e9973a242b Merge pull request #1748 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-09-30 10:29:49 +05:30
Jannat Patel
d324ad0ac5 Merge pull request #1752 from pateljannat/issues-132
fix: misc issues
2025-09-30 10:29:33 +05:30
Jannat Patel
ab6cc2698a fix: assignment should be uploaded as private file 2025-09-29 18:27:50 +05:30
Jannat Patel
da076f71a1 fix: when course is unpublished and not coming either, user should not be able to access the course details 2025-09-29 18:09:03 +05:30
Jannat Patel
63c1fe8e75 fix: border when course card has an image 2025-09-29 15:26:14 +05:30
Jannat Patel
31f0833629 chore: removed unused files 2025-09-29 15:13:39 +05:30
Jannat Patel
d4dc094049 chore: Serbian (Latin) translations 2025-09-29 13:56:06 +05:30
Jannat Patel
21a84e8032 chore: Serbian (Cyrillic) translations 2025-09-29 13:55:53 +05:30
Jannat Patel
819a1baae0 Merge pull request #1745 from UmakanthKaspa/fix/dark-theme-text-visibility
fix: make text visible in dark theme
2025-09-29 12:17:56 +05:30
Jannat Patel
b0ee67faff Merge pull request #1743 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-09-29 12:17:14 +05:30
Jannat Patel
0684bd105a chore: Norwegian Bokmal translations 2025-09-28 13:59:13 +05:30
UmakanthKaspa
5c948862a0 fix: remove extra space in CSS class 2025-09-27 14:15:39 +00:00
UmakanthKaspa
23291b38de fix: add dark theme text colors to Create Program dialog 2025-09-27 13:59:07 +00:00
UmakanthKaspa
9357bd55a6 fix: make text visible in dark theme
- Add text-ink-gray-9 to headings
- Text now shows in both light and dark themes
2025-09-27 13:04:07 +00:00
Jannat Patel
e51d668418 chore: Esperanto translations 2025-09-27 13:46:00 +05:30
Jannat Patel
a48df70631 chore: Serbian (Latin) translations 2025-09-27 13:45:59 +05:30
Jannat Patel
9853e8311b chore: Norwegian Bokmal translations 2025-09-27 13:45:57 +05:30
Jannat Patel
cd8041b048 chore: Bosnian translations 2025-09-27 13:45:56 +05:30
Jannat Patel
00bf5b7ad6 chore: Croatian translations 2025-09-27 13:45:54 +05:30
Jannat Patel
40bcc983c7 chore: Thai translations 2025-09-27 13:45:53 +05:30
Jannat Patel
dfb26f31db chore: Persian translations 2025-09-27 13:45:51 +05:30
Jannat Patel
a57a0bebef chore: Indonesian translations 2025-09-27 13:45:50 +05:30
Jannat Patel
4853621b1b chore: Portuguese, Brazilian translations 2025-09-27 13:45:49 +05:30
Jannat Patel
e3209c6fb3 chore: Vietnamese translations 2025-09-27 13:45:48 +05:30
Jannat Patel
13d25cce1f chore: Chinese Simplified translations 2025-09-27 13:45:47 +05:30
Jannat Patel
cb16b0ca64 chore: Turkish translations 2025-09-27 13:45:45 +05:30
Jannat Patel
b3ce3159e7 chore: Swedish translations 2025-09-27 13:45:44 +05:30
Jannat Patel
76ffc70892 chore: Serbian (Cyrillic) translations 2025-09-27 13:45:42 +05:30
Jannat Patel
2734587981 chore: Russian translations 2025-09-27 13:45:41 +05:30
Jannat Patel
223e93d654 chore: Portuguese translations 2025-09-27 13:45:39 +05:30
Jannat Patel
26351726a8 chore: Polish translations 2025-09-27 13:45:38 +05:30
Jannat Patel
efc84db580 chore: Dutch translations 2025-09-27 13:45:37 +05:30
Jannat Patel
3bf58bb6f0 chore: Italian translations 2025-09-27 13:45:35 +05:30
Jannat Patel
a7962d9404 chore: Hungarian translations 2025-09-27 13:45:34 +05:30
Jannat Patel
5ce9bb306d chore: German translations 2025-09-27 13:45:33 +05:30
Jannat Patel
5b5bb38f4f chore: Danish translations 2025-09-27 13:45:32 +05:30
Jannat Patel
419ad311a0 chore: Czech translations 2025-09-27 13:45:30 +05:30
Jannat Patel
0c3af09566 chore: Arabic translations 2025-09-27 13:45:29 +05:30
Jannat Patel
c9063625ec chore: Spanish translations 2025-09-27 13:45:28 +05:30
Jannat Patel
205858a41d chore: French translations 2025-09-27 13:45:26 +05:30
Jannat Patel
87edad17c3 Merge pull request #1742 from frappe/pot_develop_2025-09-26
chore: update POT file
2025-09-26 22:45:31 +05:30
frappe-pr-bot
1c54e80951 chore: update POT file 2025-09-26 16:04:27 +00:00
Jannat Patel
6c19cdc729 Merge pull request #1741 from pateljannat/fui-upgrade
chore: upgraded frappe ui and made relevant changes
2025-09-25 18:30:15 +05:30
Jannat Patel
84a703bb50 test: updated tests as per latest frappe ui version 2025-09-25 18:19:03 +05:30
Jannat Patel
27ed95b044 Merge pull request #1739 from KerollesFathy/work-mode-on-job-portal
feat(jobs): Add Work Mode on Job Portal
2025-09-25 17:42:57 +05:30
Jannat Patel
0358dfe790 fix: upgraded evaluator schedule as per latest frappe ui 2025-09-25 17:42:12 +05:30
KerollesFathy
68cee65f22 refactor: make all filters on the same line 2025-09-25 11:59:57 +00:00
Jannat Patel
24b2125b97 chore: upgraded frappe ui and made relevant changes 2025-09-25 16:46:29 +05:30
Jannat Patel
eceed12992 Merge pull request #1740 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-09-25 13:02:02 +05:30
Jannat Patel
f9028433a0 chore: Serbian (Latin) translations 2025-09-25 12:44:36 +05:30
Jannat Patel
1936e7b212 chore: Bosnian translations 2025-09-25 12:44:33 +05:30
Jannat Patel
c04a972be3 chore: Croatian translations 2025-09-25 12:44:32 +05:30
Jannat Patel
9c26b011e4 chore: Swedish translations 2025-09-25 12:44:23 +05:30
Jannat Patel
821f125789 chore: Serbian (Cyrillic) translations 2025-09-25 12:44:22 +05:30
Jannat Patel
e0f376880a chore: Dutch translations 2025-09-25 12:44:18 +05:30
Jannat Patel
6da77bb3c7 chore: Hungarian translations 2025-09-25 12:44:15 +05:30
Jannat Patel
2fd660a93f chore: German translations 2025-09-25 12:44:14 +05:30
Jannat Patel
f3eefc748a chore: Danish translations 2025-09-25 12:44:12 +05:30
Jannat Patel
4852698d74 chore: Czech translations 2025-09-25 12:44:11 +05:30
Jannat Patel
36b24dc826 chore: Arabic translations 2025-09-25 12:44:09 +05:30
Jannat Patel
d60bc1d4b6 chore: Spanish translations 2025-09-25 12:44:08 +05:30
Jannat Patel
06a02c0877 chore: French translations 2025-09-25 12:44:06 +05:30
KerollesFathy
1a53a9f30b feat: Add work mode selection to job form 2025-09-24 14:24:08 +00:00
KerollesFathy
7ee81d4693 feat: Add work mode badge to job detail page 2025-09-24 14:23:45 +00:00
KerollesFathy
0a32d03fda feat: Display work mode badge on job card 2025-09-24 14:22:59 +00:00
KerollesFathy
6c43dfea18 feat: Add work mode filter and selection to job opportunities 2025-09-24 14:22:07 +00:00
KerollesFathy
7d1e226743 feat: Add work mode on job opportunity 2025-09-24 14:21:03 +00:00
Frappe PR Bot
8ea903b81a chore(release): Bumped to Version 2.36.0 2025-09-24 07:17:24 +00:00
Jannat Patel
24b08599b3 Merge pull request #1737 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-09-24 12:46:37 +05:30
Jannat Patel
a0f1c1f227 chore: Esperanto translations 2025-09-24 12:46:22 +05:30
Jannat Patel
efbb014588 chore: Serbian (Latin) translations 2025-09-24 12:46:20 +05:30
Jannat Patel
f881a0e1d5 chore: Norwegian Bokmal translations 2025-09-24 12:46:19 +05:30
Jannat Patel
acdb81e8a3 chore: Bosnian translations 2025-09-24 12:46:17 +05:30
Jannat Patel
6559a87323 chore: Croatian translations 2025-09-24 12:46:15 +05:30
Jannat Patel
8301bab768 chore: Thai translations 2025-09-24 12:46:14 +05:30
Jannat Patel
eb0b2010f9 chore: Persian translations 2025-09-24 12:46:12 +05:30
Jannat Patel
8158ea164d chore: Indonesian translations 2025-09-24 12:46:10 +05:30
Jannat Patel
beb3134af9 chore: Portuguese, Brazilian translations 2025-09-24 12:46:08 +05:30
Jannat Patel
5fdc6a21a5 chore: Vietnamese translations 2025-09-24 12:46:07 +05:30
Jannat Patel
e7516c57bc chore: Chinese Simplified translations 2025-09-24 12:46:05 +05:30
Jannat Patel
2169d81b73 chore: Turkish translations 2025-09-24 12:46:04 +05:30
Jannat Patel
1d6bb9f9f6 chore: Swedish translations 2025-09-24 12:46:02 +05:30
Jannat Patel
812bd07d03 chore: Serbian (Cyrillic) translations 2025-09-24 12:46:01 +05:30
Jannat Patel
19bb02f905 chore: Russian translations 2025-09-24 12:45:59 +05:30
Jannat Patel
58d750f726 chore: Portuguese translations 2025-09-24 12:45:58 +05:30
Jannat Patel
7f3bb58ec1 chore: Polish translations 2025-09-24 12:45:56 +05:30
Jannat Patel
08ceaf204f chore: Dutch translations 2025-09-24 12:45:54 +05:30
Jannat Patel
eced1221a8 chore: Italian translations 2025-09-24 12:45:52 +05:30
Jannat Patel
3a5bbde0cc chore: Hungarian translations 2025-09-24 12:45:51 +05:30
Jannat Patel
eb1a790485 chore: German translations 2025-09-24 12:45:49 +05:30
Jannat Patel
21e9c85bf7 chore: Danish translations 2025-09-24 12:45:47 +05:30
Jannat Patel
632f783d57 chore: Czech translations 2025-09-24 12:45:46 +05:30
Jannat Patel
3ba3908108 chore: Arabic translations 2025-09-24 12:45:44 +05:30
Jannat Patel
903a4e91b0 chore: Spanish translations 2025-09-24 12:45:43 +05:30
Jannat Patel
6496b129ce chore: French translations 2025-09-24 12:45:41 +05:30
Jannat Patel
9fff7a2ea8 Merge pull request #1736 from pateljannat/pwa-customizations
feat: PWA Customizations
2025-09-24 12:26:54 +05:30
Jannat Patel
6d94617e59 feat: PWA Customizations 2025-09-24 12:11:12 +05:30
Jannat Patel
b95d07babb Merge pull request #1732 from pateljannat/activation
chore: site data for analytics
2025-09-23 14:54:47 +05:30
Jannat Patel
9748d075fa chore: site data for analytics 2025-09-23 13:00:58 +05:30
Jannat Patel
aaeeb84ed3 Merge pull request #1726 from pateljannat/payment-settings-refactor
refactor: payment settings
2025-09-23 11:37:03 +05:30
Jannat Patel
c3702ee6d5 feat: transaction list 2025-09-23 11:26:46 +05:30
Jannat Patel
f239987043 feat: new payment gateway creation from settings 2025-09-22 14:19:27 +05:30
Jannat Patel
6aa67c3fae Merge pull request #1727 from frappe/pot_develop_2025-09-19
chore: update POT file
2025-09-22 11:16:45 +05:30
frappe-pr-bot
1fc5d75cc0 chore: update POT file 2025-09-19 16:04:26 +00:00
Frappe PR Bot
2e0cd1964c chore(release): Bumped to Version 2.35.0 2025-09-18 13:01:47 +00:00
Jannat Patel
bcfd3bb636 refactor: payment settings 2025-09-18 18:29:08 +05:30
Jannat Patel
9b893b56f4 Merge branch 'develop' of https://github.com/frappe/lms into develop 2025-09-17 16:00:52 +05:30
Jannat Patel
733254e2c0 fix: URL in iframe for programming exercises 2025-09-17 15:58:02 +05:30
Jannat Patel
1e351e60a5 Merge pull request #1725 from pateljannat/language-control
feat: switch language from profile
2025-09-17 15:55:37 +05:30
Jannat Patel
90c53e4b46 feat: switch language from profile 2025-09-17 15:46:28 +05:30
Jannat Patel
69f54a00c5 Merge pull request #1263 from FahidLatheef/feat/scorm-progress
feat: SCORM Course Resume Functionality
2025-09-17 12:50:22 +05:30
Jannat Patel
372c4d33f0 Merge pull request #1722 from pateljannat/issues-131
fix: misc issues
2025-09-17 12:48:42 +05:30
Jannat Patel
5ba77aae3d fix: moved lesson navigation buttons to navbar to keep them always visible 2025-09-17 12:15:51 +05:30
Jannat Patel
28f4de1b7f fix: check if question exists in local storage before adding to avoid duplicate 2025-09-17 11:45:09 +05:30
Jannat Patel
ed162e2546 fix: don't allow unnecessary attributes in profile bio 2025-09-16 11:34:09 +05:30
Jannat Patel
e16cecd149 Merge pull request #1718 from frappe/pot_develop_2025-09-12
chore: update POT file
2025-09-15 11:24:43 +05:30
frappe-pr-bot
3a26a5b4d8 chore: update POT file 2025-09-12 16:04:25 +00:00
Jannat Patel
3848af08fe Merge pull request #1711 from frappe/pot_develop_2025-09-05
chore: update POT file
2025-09-08 10:06:02 +05:30
Fahid Latheef Alungal
ae79e52486 fix: add a 300 ms debounce on saveProgress from 'cmi.suspend_data' SCORM API to prevent duplicate entries 2025-09-07 19:04:16 +05:30
Fahid Latheef Alungal
1968f5064d refactor: dropped typehints from whitelisted method to avoid unexpected errors 2025-09-07 17:55:54 +05:30
Fahid Latheef Alungal
9d21bcecbe fix: typehint for SCORMDetails 2025-09-07 17:07:07 +05:30
Fahid Latheef Alungal
7a84933b2f Merge remote-tracking branch 'origin/develop' into feat/scorm-progress 2025-09-07 02:00:32 +05:30
frappe-pr-bot
9a10240fcc chore: update POT file 2025-09-05 16:04:16 +00:00
Jannat Patel
99516421ac Merge pull request #1710 from vishwajeet-13/fix/installprompt-responsive-in-mobile
fix(ui): make install prompt message mobile responsive
2025-09-04 13:55:19 +05:30
vishwajeet
5eecffb75e fix(ui): make install prompt message mobile responsive 2025-09-04 13:35:14 +05:30
Jannat Patel
9d2627849a fix: scorm patch query 2025-09-03 15:44:10 +05:30
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
Fahid Latheef A
39cc83c1b8 Merge branch 'frappe:develop' into feat/scorm-progress 2025-03-02 14:17:26 +05:30
Fahid Latheef A
5dd1bb949c Merge branch 'develop' into feat/scorm-progress 2025-02-17 23:07:10 +05:30
Fahid Latheef Alungal
b76486e4dc fix: changed filters in calculation of completed LMS Course Progress count 2025-02-16 21:08:58 +05:30
FahidLatheef
2ce2df6390 style: change height of SCORM iframe so that user don't have to scroll 2025-01-22 15:25:39 +05:30
FahidLatheef
19e5136d64 feat: added logic to handle failure cases on SCORM courses failure (Whether to allow retake of final quiz or Reset the whole Course) 2025-01-22 15:25:39 +05:30
FahidLatheef
281e155480 feat: set cmi.launch_data from scorm_content for resumability
- Replaced already_completed with progress_already_exists variable
- Added scorm_details parameter to save_progress whitelisted function
- Added Separate logic for SCORM chapter progress in save_progress
2025-01-22 15:25:39 +05:30
FahidLatheef
70b2a11cb7 feat: added is_scorm_chapter and scorm_details fields used for tracking SCORM Data Model
feat: added is_scorm_chapter and scorm_content to track cmi.suspend_data for resuming scorm chapter
2025-01-22 15:25:39 +05:30
326 changed files with 73703 additions and 30383 deletions

124
.eslintrc Normal file
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
}
}

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

178
README.md
View File

@@ -1,178 +0,0 @@
<div align="center" markdown="1">
<img src=".github/lms-logo.png" alt="Frappe Learning logo" width="80" height="80"/>
<h1>Frappe Learning</h1>
**Easy to use, open source, Learning Management System**
![Tests](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/vandxn/main&style=flat&logo=cypress)
</div>
<div align="center">
<img src=".github/hero.png?v=5" alt="Hero Image" width="72%" />
</div>
<br />
<div align="center">
<a href="https://frappe.io/learning">Website</a>
-
<a href="https://docs.frappe.io/learning">Documentation</a>
</div>
## Frappe Learning
Frappe Learning is an easy-to-use learning system that helps you bring structure to your content.
### Motivation
In 2021, we were looking for a Learning Management System to launch [Mon.School](https://mon.school) for FOSS United. We checked out Moodle, but it didnt feel right. The forms were unnecessarily lengthy and the UI was confusing. It shouldn't be this hard to create a course right? So I started making a learning system for Mon.School which soon became a product in itself. The aim is to have a simple platform that anyone can use to launch a course of their own and make knowledge sharing easier.
### Key Features
- **Structured Learning**: Design a course with a 3-level hierarchy, where your courses have chapters and you can group your lessons within these chapters. This ensures that the context of the lesson is set by the chapter.
- **Live Classes**: Group learners into batches based on courses and duration. You can then create Zoom live class for these batches right from the app. Learners get to see the list of live classes they have to take as a part of this batch.
- **Quizzes and Assignments**: Create quizzes where questions can have single-choice, multiple-choice options, or can be open ended. Instructors can also add assignments which learners can submit as PDF's or Documents.
- **Getting Certified**: Once a learner has completed the course or batch, you can grant them a certificate. The app provides an inbuilt certificate template. You can use this or else create a template of your own and use that instead.
<details>
<summary>View Screenshots</summary>
![Batch](.github/batch.png)
<div align="center">
<sub>
Create batches to group your learners
</sub>
</div>
<br>
![Quiz](.github/quiz.png)
<div align="center">
<sub>
Evaluate their knowledge by quizzes
</sub>
</div>
<br>
![Cerficicate](.github/certificate.png)
<div align="center">
<sub>
Autenticate their work with certification
</sub>
</div>
</details>
### Under the Hood
- [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework.
- [**Frappe UI**](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface.
## Production Setup
### Managed Hosting
You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications with peace of mind.
It takes care of installation, setup, upgrades, monitoring, maintenance and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments.
<div>
<a href="https://frappecloud.com/lms/signup" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/try-on-fc-white.png">
<img src="https://frappe.io/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" />
</picture>
</a>
</div>
### Self Hosting
Follow these steps to set up Frappe Learning in production:
**Step 1**: Download the easy install script
```bash
wget https://frappe.io/easy-install.py
```
**Step 2**: Run the deployment command
```bash
python3 ./easy-install.py deploy \
--project=learning_prod_setup \
--email=your_email.example.com \
--image=ghcr.io/frappe/lms \
--version=stable \
--app=lms \
--sitename subdomain.domain.tld
```
Replace the following parameters with your values:
- `your_email.example.com`: Your email address
- `subdomain.domain.tld`: Your domain name where Learning will be hosted
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
You need Docker, docker-compose and git setup on your machine. Refer [Docker documentation](https://docs.docker.com/). After that, follow below steps:
**Step 1**: Setup folder and download the required files
mkdir frappe-learning
cd frappe-learning
# Download the docker-compose file
wget -O docker-compose.yml https://raw.githubusercontent.com/frappe/lms/develop/docker/docker-compose.yml
# Download the setup script
wget -O init.sh https://raw.githubusercontent.com/frappe/lms/develop/docker/init.sh
**Step 2**: Run the container and daemonize it
docker compose up -d
**Step 3**: The site [http://lms.localhost:8000/lms](http://lms.localhost:8000/lms) should now be available. The default credentials are:
- Username: Administrator
- Password: admin
### Local
To setup the repository locally follow the steps mentioned below:
1. Install bench and setup a `frappe-bench` directory by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation)
1. Start the server by running `bench start`
1. In a separate terminal window, create a new site by running `bench new-site learning.test`
1. Map your site to localhost with the command `bench --site learning.test add-to-hosts`
1. Get the Learning app. Run `bench get-app https://github.com/frappe/lms`
1. Run `bench --site learning.test install-app lms`.
1. Now open the URL `http://learning.test:8000/lms` in your browser, you should see the app running
## Learn and connect
- [Telegram Public Group](https://t.me/frappelms)
- [Discuss Forum](https://discuss.frappe.io/c/lms/70)
- [Documentation](https://docs.frappe.io/learning)
- [YouTube](https://www.youtube.com/channel/UCn3bV5kx77HsVwtnlCeEi_A)
<br>
<br>
<div align="center" style="padding-top: 0.75rem;">
<a href="https://frappe.io" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
</picture>
</a>
</div>

View File

@@ -10,11 +10,11 @@ describe("Batch Creation", () => {
cy.get("span").contains("Settings").click();
// Add a new member
cy.get('[id^="headlessui-dialog-panel-v-"]')
cy.get("[data-dismissable-layer]")
.find("span")
.contains(/^Members$/)
.click();
cy.get('[id^="headlessui-dialog-panel-v-"]')
cy.get("[data-dismissable-layer]")
.find("button")
.contains("New")
.click();
@@ -28,12 +28,12 @@ describe("Batch Creation", () => {
cy.get("button").contains("Add").click();
// Add evaluator
cy.get('[id^="headlessui-dialog-panel-v-"]')
cy.get("[data-dismissable-layer]")
.find("span")
.contains(/^Evaluators$/)
.click();
cy.get('[id^="headlessui-dialog-panel-v-"]')
cy.get("[data-dismissable-layer]")
.find("button")
.contains("New")
.click();
@@ -138,43 +138,31 @@ describe("Batch Creation", () => {
.contains("Test Batch Short Description to test the UI")
.should("be.visible");
cy.get("a").contains("Evaluator").should("be.visible");
cy.get("span")
cy.get("span:visible")
.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("div")
.contains("10")
.should("be.visible")
.get("span")
.contains("Seats Left")
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").contains("Manage Batch").click();
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('div[role="dialog"]').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.get("div")
.contains("9")
.should("be.visible")
.get("span")
.contains("Seats Left")
.should("be.visible");
cy.contains("div:visible", "9 Seats Left").should("be.visible");
});
});

View File

@@ -76,7 +76,7 @@ describe("Course Creation", () => {
cy.button("Add Chapter").click();
cy.wait(1000);
cy.get("[id^=headlessui-dialog-panel-")
cy.get("[data-dismissable-layer]")
.should("be.visible")
.within(() => {
cy.get("label").contains("Title").type("Test Chapter");
@@ -98,7 +98,7 @@ describe("Course Creation", () => {
// View Course
cy.wait(1000);
cy.visit("/lms");
cy.visit("/lms/courses");
cy.closeOnboardingModal();
cy.url().should("include", "/lms/courses");
@@ -140,9 +140,10 @@ 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(() => {
cy.get("[data-dismissable-layer]").within(() => {
cy.get("label").contains("Title").type("Test Discussion");
cy.get("div[contenteditable=true]").invoke(
"text",

1
frontend/.gitignore vendored
View File

@@ -2,4 +2,5 @@ node_modules
.DS_Store
dist
dist-ssr
dev-dist
*.local

10
frontend/auto-imports.d.ts vendored Normal file
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 {
}

View File

@@ -41,6 +41,7 @@ declare module 'vue' {
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']
ContactUsEmail: typeof import('./src/components/ContactUsEmail.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']
@@ -66,6 +67,8 @@ declare module 'vue' {
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default']
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
InlineLessonMenu: typeof import('./src/components/Notes/InlineLessonMenu.vue')['default']
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']
@@ -81,9 +84,11 @@ declare module 'vue' {
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
Notes: typeof import('./src/components/Notes/Notes.vue')['default']
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
PaymentGatewayDetails: typeof import('./src/components/Settings/PaymentGatewayDetails.vue')['default']
PaymentGateways: typeof import('./src/components/Settings/PaymentGateways.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']
@@ -102,6 +107,8 @@ declare module 'vue' {
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']
TransactionDetails: typeof import('./src/components/Settings/TransactionDetails.vue')['default']
Transactions: typeof import('./src/components/Settings/Transactions.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']

View File

@@ -3,6 +3,203 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="{{ favicon }}" />
<link rel="manifest" href="/api/method/lms.lms.api.get_pwa_manifest" />
<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 }}" />
@@ -16,7 +213,7 @@
<meta name="twitter:image" content="{{ meta.image }}" />
<meta name="twitter:description" content="{{ meta.description }}" />
</head>
<body>
<body class="sm:overscroll-y-none no-scrollbar">
<div id="app">
<div id="seo-content">
<h1>{{ meta.title }}</h1>

View File

@@ -30,8 +30,9 @@
"chart.js": "^4.4.1",
"codemirror": "^6.0.1",
"dayjs": "^1.11.6",
"dompurify": "^3.2.6",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.172",
"frappe-ui": "^0.1.201",
"highlight.js": "^11.11.1",
"lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0",
@@ -53,6 +54,7 @@
"@vitejs/plugin-vue": "^5.0.3",
"autoprefixer": "^10.4.2",
"postcss": "^8.4.5",
"vite": "^5.0.11"
"vite": "^5.0.11",
"vite-plugin-pwa": "^1.0.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512.001 512.001" xml:space="preserve">
<g>
<path style="fill:#A67C52;" d="M84.096,436.178l-49.312,37.686c-7.54,5.762-8.981,16.547-3.219,24.087
c3.383,4.425,8.494,6.751,13.666,6.751c3.638,0,7.306-1.151,10.421-3.532l49.312-37.686c7.54-5.762,8.981-16.547,3.219-24.087
C102.421,431.858,91.637,430.416,84.096,436.178z"/>
<path style="fill:#A67C52;" d="M441.194,473.864l-49.312-37.686c-7.541-5.762-18.325-4.32-24.087,3.219
c-5.762,7.541-4.321,18.325,3.219,24.087l49.312,37.686c3.115,2.38,6.782,3.532,10.421,3.532c5.171,0,10.284-2.326,13.665-6.751
C450.175,490.411,448.734,479.627,441.194,473.864z"/>
</g>
<path style="fill:#DBAD75;" d="M237.989,36.024c-131.227,0-237.989,106.761-237.989,237.989s106.761,237.989,237.989,237.989
S475.978,405.24,475.978,274.012S369.216,36.024,237.989,36.024z"/>
<path style="fill:#EABD81;" d="M237.989,36.024c-131.227,0-237.989,106.761-237.989,237.989s106.761,237.989,237.989,237.989V36.024
z"/>
<path style="fill:#BC2A46;" d="M237.989,80.411c-106.752,0-193.601,86.849-193.601,193.601s86.849,193.601,193.601,193.601
s193.601-86.849,193.601-193.601S344.742,80.411,237.989,80.411z"/>
<path style="fill:#D62D46;" d="M237.989,80.411c-106.752,0-193.601,86.849-193.601,193.601s86.849,193.601,193.601,193.601V80.411z"
/>
<path style="fill:#DBAD75;" d="M237.989,142.771c-72.367,0-131.241,58.874-131.241,131.241s58.874,131.241,131.241,131.241
S369.23,346.379,369.23,274.012S310.355,142.771,237.989,142.771z"/>
<path style="fill:#EABD81;" d="M237.989,142.771c-72.367,0-131.241,58.874-131.241,131.241s58.874,131.241,131.241,131.241V142.771z
"/>
<path style="fill:#BC2A46;" d="M237.989,209.763c-35.427,0-64.248,28.821-64.248,64.248s28.821,64.248,64.248,64.248
s64.248-28.821,64.248-64.248S273.416,209.763,237.989,209.763z"/>
<path style="fill:#D62D46;" d="M237.989,209.763c-35.427,0-64.248,28.821-64.248,64.248s28.821,64.248,64.248,64.248V209.763z"/>
<path style="fill:#CFCDD6;" d="M237.989,291.196c-4.398,0-8.796-1.677-12.15-5.034c-6.711-6.711-6.711-17.59,0-24.301
L448.687,39.014c6.71-6.711,17.59-6.711,24.301,0s6.711,17.59,0,24.301L250.14,286.162
C246.784,289.519,242.386,291.196,237.989,291.196z"/>
<path style="fill:#DEE1E7;" d="M237.989,291.196c-4.398,0-8.796-1.677-12.15-5.034c-6.711-6.711-6.711-17.59,0-24.301
l106.576-106.576l24.301,24.301L250.14,286.162C246.784,289.519,242.386,291.196,237.989,291.196z"/>
<path style="fill:#39B7B6;" d="M457.533,105.266h-33.615c-9.49,0-17.184-7.694-17.184-17.184V54.467
c0-9.49,7.694-17.184,17.184-17.184s17.184,7.694,17.184,17.184v16.432h16.431c9.49,0,17.184,7.694,17.184,17.184
S467.023,105.266,457.533,105.266z"/>
<path style="fill:#FBB03B;" d="M476.175,86.623h-33.614c-9.49,0-17.184-7.694-17.184-17.184V35.825
c0-9.49,7.694-17.184,17.184-17.184s17.184,7.694,17.184,17.184v16.431h16.431c9.49,0,17.184,7.694,17.184,17.184
S485.665,86.623,476.175,86.623z"/>
<path style="fill:#39B7B6;" d="M494.817,67.982h-33.614c-9.49,0-17.184-7.694-17.184-17.184V17.184
c0-9.49,7.694-17.184,17.184-17.184s17.184,7.694,17.184,17.184v16.431h16.431c9.49,0,17.184,7.694,17.184,17.184
S504.308,67.982,494.817,67.982z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--noto" preserveAspectRatio="xMidYMid meet"><path d="M68.05 7.23l13.46 30.7a7.047 7.047 0 0 0 5.82 4.19l32.79 2.94c3.71.54 5.19 5.09 2.5 7.71l-24.7 20.75c-2 1.68-2.91 4.32-2.36 6.87l7.18 33.61c.63 3.69-3.24 6.51-6.56 4.76L67.56 102a7.033 7.033 0 0 0-7.12 0l-28.62 16.75c-3.31 1.74-7.19-1.07-6.56-4.76l7.18-33.61c.54-2.55-.36-5.19-2.36-6.87L5.37 52.78c-2.68-2.61-1.2-7.17 2.5-7.71l32.79-2.94a7.047 7.047 0 0 0 5.82-4.19l13.46-30.7c1.67-3.36 6.45-3.36 8.11-.01z" fill="#fdd835"></path><path d="M67.07 39.77l-2.28-22.62c-.09-1.26-.35-3.42 1.67-3.42c1.6 0 2.47 3.33 2.47 3.33l6.84 18.16c2.58 6.91 1.52 9.28-.97 10.68c-2.86 1.6-7.08.35-7.73-6.13z" fill="#ffff8d"></path><path d="M95.28 71.51L114.9 56.2c.97-.81 2.72-2.1 1.32-3.57c-1.11-1.16-4.11.51-4.11.51l-17.17 6.71c-5.12 1.77-8.52 4.39-8.82 7.69c-.39 4.4 3.56 7.79 9.16 3.97z" fill="#f4b400"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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

View File

@@ -1,10 +1,9 @@
<template>
<FrappeUIProvider>
<Layout>
<div class="text-base">
<router-view />
</div>
<Layout class="isolate text-base">
<router-view />
</Layout>
<!--<InstallPrompt v-if="isMobile" />-->
<Dialogs />
</FrappeUIProvider>
</template>
@@ -13,14 +12,15 @@ import { FrappeUIProvider } from 'frappe-ui'
import { Dialogs } from '@/utils/dialogs'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useScreenSize } from './utils/composables'
import DesktopLayout from './components/DesktopLayout.vue'
import MobileLayout from './components/MobileLayout.vue'
import NoSidebarLayout from './components/NoSidebarLayout.vue'
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 InstallPrompt from './components/InstallPrompt.vue'
const screenSize = useScreenSize()
const { isMobile } = useScreenSize()
const router = useRouter()
const noSidebar = ref(false)
const { userResource } = usersStore()
@@ -38,10 +38,9 @@ const Layout = computed(() => {
if (noSidebar.value) {
return NoSidebarLayout
}
if (screenSize.width < 640) {
if (isMobile.value) {
return MobileLayout
}
return DesktopLayout
})

View File

@@ -9,12 +9,12 @@
>
<UserDropdown :isCollapsed="sidebarStore.isSidebarCollapsed" />
<div class="flex flex-col" v-if="sidebarSettings.data">
<SidebarLink
v-for="link in sidebarLinks"
:link="link"
:isCollapsed="sidebarStore.isSidebarCollapsed"
class="mx-2 my-0.5"
/>
<div v-for="link in sidebarLinks" class="mx-2 my-0.5">
<SidebarLink
:link="link"
:isCollapsed="sidebarStore.isSidebarCollapsed"
/>
</div>
</div>
<div
v-if="sidebarSettings.data?.web_pages?.length || isModerator"
@@ -54,15 +54,18 @@
class="flex flex-col transition-all duration-300 ease-in-out"
:class="!sidebarStore.isWebpagesCollapsed ? 'block' : 'hidden'"
>
<SidebarLink
<div
v-for="link in sidebarSettings.data.web_pages"
:link="link"
:isCollapsed="sidebarStore.isSidebarCollapsed"
class="mx-2 my-0.5"
:showControls="isModerator ? true : false"
@openModal="openPageModal"
@deletePage="deletePage"
/>
>
<SidebarLink
:link="link"
:isCollapsed="sidebarStore.isSidebarCollapsed"
:showControls="isModerator ? true : false"
@openModal="openPageModal"
@deletePage="deletePage"
/>
</div>
</div>
</div>
</div>
@@ -196,7 +199,7 @@ 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'
@@ -214,6 +217,7 @@ import {
Users,
BookText,
Zap,
Check,
} from 'lucide-vue-next'
import {
TrialBanner,
@@ -360,37 +364,49 @@ const addProgrammingExercises = () => {
}
}
const addPrograms = () => {
let activeFor = ['Programs', 'ProgramForm']
let index = 1
let canAddProgram = false
const addPrograms = async () => {
let canAddProgram = await checkIfCanAddProgram()
if (!canAddProgram) return
let activeFor = ['Programs', 'ProgramDetail']
let index = 2
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
}
sidebarLinks.value.splice(index, 0, {
label: 'Programs',
icon: 'Route',
to: 'Programs',
activeFor: activeFor,
})
}
if (canAddProgram) {
sidebarLinks.value.splice(index, 0, {
label: 'Programs',
icon: 'Route',
to: 'Programs',
activeFor: activeFor,
const addContactUsDetails = () => {
if (settingsStore.contactUsEmail?.data || settingsStore.contactUsURL?.data) {
sidebarLinks.value.push({
label: 'Contact Us',
icon: settingsStore.contactUsURL?.data ? 'Headset' : 'Mail',
to: settingsStore.contactUsURL?.data
? settingsStore.contactUsURL.data
: settingsStore.contactUsEmail?.data,
})
}
}
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
@@ -593,6 +609,11 @@ const articles = ref([
{ name: 'create-a-live-class', title: __('Create a live class') },
],
},
{
title: __('Learning Paths'),
opened: false,
subArticles: [{ name: 'add-a-program', title: __('Add a program') }],
},
{
title: __('Assessments'),
opened: false,
@@ -638,15 +659,121 @@ const setUpOnboarding = () => {
}
}
const addMyPoints = () => {
const roles = userResource.data?.roles || []
if (roles.includes('LMS Student') || roles.includes('LMS Schoolchild')) {
sidebarLinks.value.push({
label: __('My points'),
icon: 'Award',
to: 'MyPoints',
activeFor: [],
})
}
}
const addLeaderBoard = () => {
if (user) {
sidebarLinks.value.push({
label: __('Leader Board'),
icon: 'Trophy',
to: 'LeaderBoard',
activeFor: [],
})
}
}
const addChatGPT = () => {
const roles = userResource.data?.roles || []
let URL = ''
let nameLabel = ''
if (roles.includes('LMS Schoolchild') || roles.includes('LMS Student') || roles.includes('Course Creator')) {
if (roles.includes('LMS Schoolchild')) {
URL = 'chatgpt-schoolchild'
nameLabel = __('ChatGPT for Schoolers')
} else if (roles.includes('LMS Student')) {
URL = 'chatgpt-schoolchild'
nameLabel = __('ChatGPT for Students')
} else if (roles.includes('Course Creator')) {
URL = 'ai-teachers'
nameLabel = __('ChatGPT for Teachers')
}
sidebarLinks.value.push({
label: nameLabel,
icon: 'Cpu',
to: URL,
external: true,
activeFor: [],
})
}
}
const addMyChild = () => {
const roles = userResource.data?.roles || []
if (roles.includes('Parent')) {
sidebarLinks.value.push({
label: __('My Child'),
icon: 'User',
to: 'my-child',
activeFor: [],
external: true,
})
}
}
//test of new page
const addProfile = () => {
const roles = userResource.data?.roles || []
if (roles.includes('LMS Student')) {
sidebarLinks.value.push({
label: __('Student Profile'),
icon: 'Home',
to: 'StudentProfile',
activeFor: [],
})
} else if (roles.includes('LMS Schoolchild')) {
sidebarLinks.value.push({
label: __('Schoolchildren Profile'),
icon: 'Home',
to: 'SchoolchildrenProfile',
activeFor: [],
})
} else if (roles.includes('Course Creator')) {
sidebarLinks.value.push({
label: __('Course Creator Profile'),
icon: 'Home',
to: 'CourseCreatorProfile',
activeFor: [],
})
} else {
sidebarLinks.value.push({
label: __('Parent Profile'),
icon: 'Home',
to: 'ParentProfile',
activeFor: [],
})
}
}
watch(userResource, () => {
addContactUsDetails()
if (userResource.data) {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addHome()
addPrograms()
addProgrammingExercises()
addQuizzes()
addAssignments()
setUpOnboarding()
addMyPoints()
addLeaderBoard()
addChatGPT()
addMyChild()
addProfile()
}
})

View File

@@ -70,6 +70,9 @@
<FileUploader
v-if="!submissionFile"
:fileTypes="getType()"
:uploadArgs="{
private: true,
}"
:validateFile="validateFile"
@success="(file) => saveSubmission(file)"
>
@@ -127,6 +130,9 @@
@change="(val) => (answer = val)"
:editable="true"
:fixedMenu="true"
:uploadArgs="{
private: true,
}"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
/>
</div>

View File

@@ -56,7 +56,7 @@
</div>
<div v-if="!readOnlyMode">
<router-link
v-if="isModerator || isStudent"
v-if="canAccessBatch"
:to="{
name: 'Batch',
params: {
@@ -66,11 +66,11 @@
>
<Button variant="solid" class="w-full mt-4">
<template #prefix>
<Settings v-if="isModerator" class="size-4 stroke-1.5" />
<LogIn v-else class="size-4 stroke-1.5" />
<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>
@@ -204,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>

View File

@@ -5,7 +5,7 @@
{{ __('Statistics') }}
</div>
</div>
<div class="grid grid-cols-4 gap-5 mb-8">
<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 }"

View File

@@ -0,0 +1,67 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Contact Us'),
size: 'md',
}"
>
<template #body-content>
<div class="flex flex-col gap-4">
<FormControl
v-model="subject"
:label="__('Subject')"
type="text"
:required="true"
/>
<div>
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Message') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:fixedMenu="true"
@change="(val) => (message = val)"
editorClass="prose-sm py-2 px-2 min-h-[200px] border-outline-gray-2 hover:border-outline-gray-3 rounded-b-md bg-surface-gray-3"
/>
</div>
</div>
</template>
<template #actions="{ close }">
<div class="pb-5 float-right">
<Button variant="solid" @click="sendMail(close)">
{{ __('Send') }}
</Button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { Button, call, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { ref } from 'vue'
import { useSettings } from '@/stores/settings'
const show = defineModel<boolean>({ required: true, default: false })
const subject = ref('')
const message = ref('')
const settingsStore = useSettings()
const sendMail = (close: Function) => {
call('frappe.core.doctype.communication.email.make', {
recipients: settingsStore.contactUsEmail?.data,
subject: subject.value,
content: message.value,
send_email: true,
})
.then(() => {
toast.success(__('Email sent successfully'))
close()
subject.value = ''
message.value = ''
})
.catch(() => {
toast.error(__('Failed to send email'))
close()
})
}
</script>

View File

@@ -37,7 +37,7 @@
</slot>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div v-show="isOpen" class="">
<div
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
>

View File

@@ -1,11 +1,11 @@
<template>
<div
v-if="course.title"
class="flex flex-col h-full rounded-md border-2 overflow-auto text-ink-gray-9"
class="flex flex-col h-full rounded-md overflow-auto text-ink-gray-9"
style="min-height: 350px"
>
<div
class="w-[100%] h-[168px] bg-cover bg-center bg-no-repeat"
class="w-[100%] h-[168px] bg-cover bg-center bg-no-repeat border-t border-x rounded-t-md"
:style="
course.image
? { backgroundImage: `url('${encodeURI(course.image)}')` }
@@ -15,10 +15,10 @@
}
"
>
<div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
<!-- <div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
<div
v-if="course.featured"
class="flex items-center space-x-1 text-xs text-ink-amber-3 bg-surface-white px-2 py-0.5 rounded-md mr-1 mb-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"
>
<Star class="size-3 stroke-2" />
<span>
@@ -32,16 +32,22 @@
>
{{ tag }}
</div>
</div>
</div> -->
<div
v-if="!course.image"
class="flex items-center justify-center text-white flex-1 font-extrabold text-2xl my-auto"
:class="course.tags ? 'h-[80%]' : 'h-full'"
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')">
@@ -56,7 +62,7 @@
<Tooltip :text="__('Enrolled Students')">
<span class="flex items-center">
<Users class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.enrollments }}
{{ formatAmount(course.enrollments) }}
</span>
</Tooltip>
</div>
@@ -70,18 +76,16 @@
</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 v-if="course.image" class="text-xl font-semibold leading-6">
<div
v-if="course.image"
class="font-semibold leading-6"
:class="course.title.length > 32 ? 'text-lg' : 'text-xl'"
>
{{ course.title }}
</div>
@@ -112,27 +116,30 @@
<CourseInstructors :instructors="course.instructors" />
</div>
<div v-if="course.paid_course" class="font-semibold">
{{ course.price }}
</div>
<div class="flex items-center space-x-2">
<div v-if="course.paid_course" class="font-semibold">
{{ course.price }}
</div>
<Tooltip
v-if="course.paid_certificate || course.enable_certification"
:text="__('Get Certified')"
>
<GraduationCap class="size-5 stroke-1.5" />
</Tooltip>
<Tooltip
v-if="course.paid_certificate || course.enable_certification"
:text="__('Get Certified')"
>
<GraduationCap class="size-5 stroke-1.5 text-ink-gray-7" />
</Tooltip>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
import UserAvatar from '@/components/UserAvatar.vue'
import { Award, BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { Tooltip } from 'frappe-ui'
import { theme } from '@/utils/theme'
import { formatAmount } from '@/utils'
import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import ProgressBar from '@/components/ProgressBar.vue'
const { user } = sessionStore()

View File

@@ -169,6 +169,7 @@
</div>
</div>
<CourseProgressSummary
v-if="user.data?.is_moderator || is_instructor()"
v-model="showProgressModal"
:courseName="course.data.name"
:enrollments="course.data.enrollments"

View File

@@ -1,11 +1,12 @@
<template>
<div class="">
<div class="text-ink-gray-7">
<span v-if="instructors?.length == 1">
<router-link
:to="{
name: 'Profile',
params: { username: instructors[0].username },
}"
class="text-ink-gray-7 hover:text-ink-gray-9"
>
{{ instructors[0].full_name }}
</router-link>
@@ -16,6 +17,7 @@
name: 'Profile',
params: { username: instructors[0].username },
}"
class="text-ink-gray-7 hover:text-ink-gray-9"
>
{{ instructors[0].first_name }}
</router-link>
@@ -25,6 +27,7 @@
name: 'Profile',
params: { username: instructors[1].username },
}"
class="text-ink-gray-7 hover:text-ink-gray-9"
>
{{ instructors[1].first_name }}
</router-link>
@@ -35,6 +38,7 @@
name: 'Profile',
params: { username: instructors[0].username },
}"
class="text-ink-gray-7 hover:text-ink-gray-9"
>
{{ instructors[0].first_name }}
</router-link>

View File

@@ -208,6 +208,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
lessonProgress: {
type: Number,
default: 0,
},
})
const outline = createResource({
@@ -229,6 +233,13 @@ watch(
}
)
watch(
() => props.lessonProgress,
() => {
outline.reload()
}
)
const deleteLesson = createResource({
url: 'lms.lms.api.delete_lesson',
makeParams(values) {

View File

@@ -1,16 +1,10 @@
<template>
<div class="relative flex h-full flex-col">
<div class="h-full flex-1">
<div class="flex h-screen text-base bg-surface-white">
<div
class="relative block min-h-0 flex-shrink-0 overflow-hidden hover:overflow-auto"
>
<AppSidebar />
</div>
<div class="w-full overflow-auto" id="scrollContainer">
<slot />
</div>
</div>
<div class="flex h-screen w-screen">
<div class="h-full border-r bg-surface-menu-bar">
<AppSidebar />
</div>
<div class="flex-1 flex flex-col h-full overflow-auto bg-surface-white">
<slot />
</div>
</div>
</template>

View File

@@ -32,13 +32,13 @@
"
:options="[
{
label: 'Edit',
label: __('Edit'),
onClick() {
reply.editable = true
},
},
{
label: 'Delete',
label: __('Delete'),
onClick() {
deleteReply(reply)
},

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>
@@ -73,7 +76,7 @@ import { singularize, timeAgo } from '@/utils'
import { ref, onMounted, inject, onUnmounted } from 'vue'
import DiscussionReplies from '@/components/DiscussionReplies.vue'
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
import { MessageSquareText } from 'lucide-vue-next'
import { MessageSquareText, Plus } from 'lucide-vue-next'
import { getScrollContainer } from '@/utils/scrollContainer'
const showTopics = ref(true)
@@ -102,7 +105,7 @@ const props = defineProps({
},
emptyStateText: {
type: String,
default: 'Start a discussion',
default: 'Start a Discussion',
},
singleThread: {
type: Boolean,

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"
>
{{
__(

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="top-start">
<template #body>
<div
class="fixed top-[20rem] translate-x-1/3 z-20 flex flex-col gap-3 rounded bg-surface-white 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 class="leading-5">
{{
__(
'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>

View File

@@ -3,7 +3,7 @@
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">
<div class="flex flex-col space-y-2 flex-1 break-all">
<div class="text-lg font-semibold text-ink-gray-9">
{{ job.company_name }}
</div>
@@ -33,6 +33,9 @@
<Badge>
{{ job.type }}
</Badge>
<Badge v-if="job.work_mode">
{{ job.work_mode }}
</Badge>
<Badge>
{{ dayjs(job.creation).fromNow() }}
</Badge>

View File

@@ -9,6 +9,16 @@
allowfullscreen
></iframe>
</div>
<div v-if="rutube">
<iframe
class="rutube-video"
:src="getRutubeVideoSource(rutube.split('/').pop())"
width="100%"
:height="screenSize.width < 640 ? 200 : 400"
frameborder="0"
allowfullscreen
></iframe>
</div>
<div v-for="block in content?.split('\n\n')">
<div v-if="block.includes('{{ YouTubeVideo')">
<iframe
@@ -20,6 +30,16 @@
allowfullscreen
></iframe>
</div>
<div v-else-if="block.includes('{{ RutubeVideo')">
<iframe
class="rutube-video"
:src="getRutubeVideoSource(block)"
width="100%"
:height="screenSize.width < 640 ? 200 : 400"
frameborder="0"
allowfullscreen
></iframe>
</div>
<div v-else-if="block.includes('{{ Quiz')">
<Quiz :quiz="getId(block)" />
</div>
@@ -97,6 +117,13 @@ const getYouTubeVideoSource = (block) => {
return `https://www.youtube.com/embed/${block}`
}
const getRutubeVideoSource = (block) => {
if (block.includes('{{')) {
block = getId(block)
}
return `https://rutube.ru/play/embed/${block}`
}
const getPDFSource = (block) => {
return `${getId(block)}#toolbar=0`
}
@@ -105,3 +132,11 @@ const getId = (block) => {
return block.match(/\(["']([^"']+?)["']\)/)[1]
}
</script>
<style scoped>
.youtube-video,
.rutube-video {
display: block;
margin: 0 auto;
}
</style>

View File

@@ -52,9 +52,9 @@ const contentMap = {
'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?',
title: 'How to add a YouTube Video/RuTube?',
description:
'Copy the URL of the video from YouTube and paste it in the editor.',
'Copy the URL of the video from YouTube/RuTube and paste it in the editor.',
},
remove: {
title: 'How to remove an embed?',

View File

@@ -22,7 +22,10 @@
</span>
</Button>
</div>
<div v-if="liveClasses.data?.length" class="grid grid-cols-3 gap-5 mt-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 hover:border-outline-gray-3 p-3"
@@ -51,8 +54,8 @@
<div class="flex items-center space-x-2">
<Clock class="w-4 h-4 stroke-1.5" />
<span>
{{ formatTime(cls.time) }} -
{{ dayjs(getClassEnd(cls)).format('HH:mm') }}
{{ dayjs(getClassStart(cls)).format('hh:mm A') }} -
{{ dayjs(getClassEnd(cls)).format('hh:mm A') }}
</span>
</div>
<div
@@ -178,8 +181,12 @@ const canAccessClass = (cls) => {
return true
}
const getClassStart = (cls) => {
return new Date(`${cls.date}T${cls.time}`)
}
const getClassEnd = (cls) => {
const classStart = new Date(`${cls.date}T${cls.time}`)
const classStart = getClassStart(cls)
return new Date(classStart.getTime() + cls.duration * 60000)
}

View File

@@ -56,6 +56,7 @@
<script setup>
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'
@@ -71,17 +72,16 @@ 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) {
filterLinksToShow(data)
addOtherLinks()
},
}
)
// Вызываем addSideBar только если userResource уже загружен
if (userResource.data) {
addSideBar()
}
addOtherLinks()
filterLinksToShow(data)
})
const handleOutsideClick = (e) => {
@@ -110,55 +110,196 @@ const filterLinksToShow = (data) => {
})
}
const addSideBar = () => {
sidebarLinks.value = [] // Очищаем, чтобы избежать дублирования
// Проверяем роли пользователя
const roles = userResource.data?.roles || []
sidebarLinks.value.push({
label: __('Courses'),
icon: 'BookOpen',
to: 'Courses',
activeFor: [
'Courses',
'CourseDetail',
'Lesson',
'CourseForm',
'LessonForm',
],
})
sidebarLinks.value.push({
label: __('Leader Board'),
icon: 'Trophy',
to: 'LeaderBoard',
activeFor: [],
})
if (roles.includes('LMS Student') || roles.includes('LMS Schoolchild')) {
otherLinks.value.push({
label: __('My points'),
icon: 'Award',
to: 'MyPoints',
activeFor: [],
})
}
if (roles.includes('Parent')) {
otherLinks.value.push({
label: __('My Child'),
icon: 'User',
to: 'my-child',
external: true,
activeFor: [],
})
}
let chatGPTURL = ''
let chatGPTLabel = ''
if (roles.includes('LMS Schoolchild')) {
chatGPTURL = 'chatgpt-schoolchild'
chatGPTLabel = __('ChatGPT for Schoolers')
} else if (roles.includes('LMS Student')) {
chatGPTURL = 'chatgpt-schoolchild'
chatGPTLabel = __('ChatGPT for Students')
} else if (roles.includes('Course Creator')) {
chatGPTURL = 'ai-teachers'
chatGPTLabel = __('ChatGPT for Teachers')
}
if (chatGPTURL) {
sidebarLinks.value.push({
label: chatGPTLabel,
icon: 'Cpu',
to: chatGPTURL,
external: true,
activeFor: [],
})
}
}
const addOtherLinks = () => {
otherLinks.value = []
if (user) {
const roles = userResource.data?.roles || []
if (!userResource.data?.is_instructor && !userResource.data?.is_moderator) {
otherLinks.value.push({
label: __('Programs'),
icon: 'Route',
to: 'Programs',
activeFor: ['Programs', 'ProgramForm', 'CourseDetail', 'Lesson'],
})
} else if (userResource.data?.is_instructor || userResource.data?.is_moderator) {
otherLinks.value.push({
label: __('Programs'),
icon: 'Route',
to: 'Programs',
activeFor: ['Programs', 'ProgramForm'],
})
}
if (userResource.data?.is_moderator || userResource.data?.is_instructor) {
otherLinks.value.push({
label: __('Quizzes'),
icon: 'CircleHelp',
to: 'Quizzes',
activeFor: [
'Quizzes',
'QuizForm',
'QuizSubmissionList',
'QuizSubmission',
],
})
otherLinks.value.push({
label: __('Assignments'),
icon: 'Pencil',
to: 'Assignments',
activeFor: [
'Assignments',
'AssignmentForm',
'AssignmentSubmissionList',
'AssignmentSubmission',
],
}),
otherLinks.value.push({
label: 'Programming Exercises',
icon: 'Code',
to: 'ProgrammingExercises',
})
}
if (roles.includes('LMS Student') || roles.includes('LMS Schoolchild')) {
otherLinks.value.push({
label: __('My points'),
icon: 'Award',
to: 'my_points',
external: true,
activeFor: [],
})
}
let chatGPTURL = ''
let chatGPTLabel = ''
if (roles.includes('LMS Schoolchild')) {
chatGPTURL = 'chatgpt-schoolchild'
chatGPTLabel = __('ChatGPT for Schoolers')
} else if (roles.includes('LMS Student')) {
chatGPTURL = 'chatgpt-schoolchild'
chatGPTLabel = __('ChatGPT for Students')
} else if (roles.includes('Course Creator')) {
chatGPTURL = 'ai-teachers'
chatGPTLabel = __('ChatGPT for Teachers')
}
if (chatGPTURL) {
otherLinks.value.push({
label: chatGPTLabel,
icon: 'Cpu',
to: chatGPTURL,
external: true,
activeFor: [],
})
}
otherLinks.value.push({
label: 'Notifications',
icon: 'Bell',
to: 'Notifications',
label: __('Leader Board'),
icon: 'Trophy',
to: 'leaderboardsample',
external: true,
activeFor: [],
})
otherLinks.value.push({
label: 'Profile',
label: __('Profile'),
icon: 'UserRound',
to: 'Profile',
params: { username: userResource.data?.username },
})
otherLinks.value.push({
label: 'Log out',
label: __('Log out'),
icon: 'LogOut',
})
} else {
otherLinks.value.push({
label: 'Log in',
label: __('Log in'),
icon: 'LogIn',
})
}
}
watch(userResource, () => {
if (
userResource.data &&
(userResource.data.is_moderator || userResource.data.is_instructor)
) {
addQuizzes()
addAssignments()
if (userResource.data) {
addSideBar() // Обновляем sidebarLinks при изменении userResource
addOtherLinks() // Обновляем otherLinks
}
})
const addQuizzes = () => {
otherLinks.value.push({
label: 'Quizzes',
icon: 'CircleHelp',
to: 'Quizzes',
})
}
const addAssignments = () => {
otherLinks.value.push({
label: 'Assignments',
icon: 'Pencil',
to: 'Assignments',
})
}
let isActive = (tab) => {
return tab.activeFor?.includes(router.currentRoute.value.name)
}
@@ -176,6 +317,7 @@ const handleClick = (tab) => {
username: userResource.data?.username,
},
})
else if (tab.external) window.location.href = `/${tab.to}`
else router.push({ name: tab.to })
}

View File

@@ -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()

View File

@@ -113,6 +113,14 @@ watch(
{ flush: 'post' }
)
watch(show, (newVal) => {
if (newVal && props.assignmentID === 'new') {
assignment.title = ''
assignment.type = ''
assignment.question = ''
}
})
const saveAssignment = () => {
if (props.assignmentID == 'new') {
assignments.value.insert.submit(

View File

@@ -11,7 +11,7 @@
<Avatar :image="student.user_image" size="3xl" />
<div class="space-y-1">
<div class="flex items-center space-x-2">
<div class="text-xl font-semibold">
<div class="text-xl font-semibold text-ink-gray-9">
{{ student.full_name }}
</div>
<Badge
@@ -36,7 +36,9 @@
v-if="Object.keys(student.assessments).length"
class="space-y-2 text-sm"
>
<div class="flex items-center border-b pb-1 font-medium">
<div
class="flex items-center border-b pb-1 font-medium text-ink-gray-9"
>
<span class="flex-1">
{{ __('Assessment') }}
</span>
@@ -86,7 +88,9 @@
v-if="Object.keys(student.courses).length"
class="space-y-2 text-sm"
>
<div class="flex items-center border-b pb-1 font-medium">
<div
class="flex items-center border-b pb-1 font-medium text-ink-gray-9"
>
<span class="flex-1">
{{ __('Courses') }}
</span>

View File

@@ -50,7 +50,7 @@
<FileText class="h-5 w-5 stroke-1.5 text-ink-gray-7" />
</div>
<div class="flex flex-col">
<span>
<span class="text-ink-gray-9">
{{ chapter.scorm_package.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">

View File

@@ -7,12 +7,11 @@
}"
>
<template #body-content>
<div class="flex justify-between space-x-10 text-base mt-10">
<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">
<!-- <div class="text-xl font-semibold text-ink-gray-6">
{{ __('{0} Members').format(memberCount) }}
</div> -->
<FormControl
v-model="searchFilter"
:placeholder="__('Search by Member')"
@@ -90,7 +89,9 @@
</div>
</div>
<div class="mb-4 self-start w-full space-y-5">
<div class="flex items-center space-x-4">
<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="{
@@ -147,10 +148,11 @@ import {
import { computed, ref, watch } from 'vue'
import { theme } from '@/utils/theme'
const show = defineModel<boolean | undefined>()
const show = defineModel<boolean>({ default: false })
const searchFilter = ref<string | null>(null)
type Filters = {
course: string | undefined
member_name?: string[]
}
@@ -221,7 +223,6 @@ const progressColumns = computed(() => {
{
label: __('Progress'),
key: 'progress',
width: '30%',
align: 'right',
icon: 'trending-up',
},

View File

@@ -1,11 +1,11 @@
<template>
<Dialog
:options="{
title: 'Edit your profile',
size: 'xl',
title: __('Edit your profile'),
size: '3xl',
actions: [
{
label: 'Save',
label: __('Save'),
variant: 'solid',
onClick: (close) => saveProfile(close),
},
@@ -13,74 +13,81 @@
}"
>
<template #body-content>
<div>
<FileUploader
v-if="!profile.image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template v-slot="{ file, progress, uploading, openFileSelector }">
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading
? `Uploading ${progress}%`
: 'Upload a profile image'
}}
</Button>
<div class="grid grid-cols-2 gap-5">
<div class="space-y-4">
<!-- <Uploader
v-model="profile.image.file_url"
label="Profile Image"
description="Your profile image to help others recognize you."
/> -->
<div>
<div class="text-xs text-ink-gray-5 mb-1">
{{ __('Profile Image') }}
</div>
<FileUploader
v-if="!profile.image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading
? `Uploading ${progress}%`
: 'Upload a profile image'
}}
</Button>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="profile.image.file_url"
class="object-cover h-[50px] w-[50px] rounded-full border-4 border-white object-cover"
/>
<div class="text-base flex flex-col ml-2">
<span>
{{ profile.image.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">
{{ getFileSize(profile.image.file_size) }}
</span>
</div>
<X
@click="removeImage()"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
/>
</div>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="text-xs text-ink-gray-5 mb-1">
{{ __('Profile Image') }}
</div>
<div class="flex items-center">
<div class="border rounded-md p-2 mr-2">
<FileText class="h-5 w-5 stroke-1.5 text-ink-gray-7" />
<FormControl v-model="profile.first_name" :label="__('First Name')" />
<FormControl v-model="profile.last_name" :label="__('Last Name')" />
<FormControl v-model="profile.headline" :label="__('Headline')" />
<Link
:label="__('Language')"
v-model="profile.language"
doctype="Language"
/>
</div>
<div>
<div class="mb-4">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Bio') }}
</div>
<div class="text-base flex flex-col">
<span>
{{ profile.image.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">
{{ getFileSize(profile.image.file_size) }}
</span>
</div>
<X
@click="removeImage()"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
<TextEditor
:fixedMenu="true"
@change="(val) => (profile.bio = val)"
:content="profile.bio"
editorClass="prose-sm py-2 px-2 min-h-[200px] border-outline-gray-2 hover:border-outline-gray-3 rounded-b-md bg-surface-gray-3"
/>
</div>
</div>
<FormControl
v-model="profile.first_name"
:label="__('First Name')"
class="mb-4"
/>
<FormControl
v-model="profile.last_name"
:label="__('Last Name')"
class="mb-4"
/>
<FormControl
v-model="profile.headline"
:label="__('Headline')"
class="mb-4"
/>
<div class="mb-4">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Bio') }}
</div>
<TextEditor
:fixedMenu="true"
@change="(val) => (profile.bio = val)"
:content="profile.bio"
editorClass="prose-sm py-2 px-2 min-h-[200px] border-outline-gray-2 hover:border-outline-gray-3 rounded-md bg-surface-gray-3"
/>
</div>
</div>
</template>
</Dialog>
@@ -95,11 +102,14 @@ import {
TextEditor,
toast,
} from 'frappe-ui'
import { reactive, watch } from 'vue'
import { FileText, X } from 'lucide-vue-next'
import { getFileSize } from '@/utils'
import { ref, reactive, watch } from 'vue'
import { X } from 'lucide-vue-next'
import { getFileSize, decodeEntities } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import DOMPurify from 'dompurify'
const reloadProfile = defineModel('reloadProfile')
const hasLanguageChanged = ref(false)
const props = defineProps({
profile: {
@@ -147,12 +157,32 @@ const updateProfile = createResource({
})
const saveProfile = (close) => {
profile.bio = DOMPurify.sanitize(decodeEntities(profile.bio), {
ALLOWED_TAGS: [
'b',
'i',
'em',
'strong',
'a',
'p',
'br',
'ul',
'ol',
'li',
'img',
],
ALLOWED_ATTR: ['href', 'target', 'src'],
})
updateProfile.submit(
{},
{
onSuccess() {
close()
reloadProfile.value.reload()
if (hasLanguageChanged.value) {
hasLanguageChanged.value = false
window.location.reload()
}
},
onError(err) {
toast.error(err.messages?.[0] || err)
@@ -183,9 +213,19 @@ watch(
profile.first_name = newVal.first_name
profile.last_name = newVal.last_name
profile.headline = newVal.headline
profile.language = newVal.language
profile.bio = newVal.bio
if (newVal.user_image) imageResource.submit({ image: newVal.user_image })
}
}
)
watch(
() => profile.language,
(newVal, oldVal) => {
if (newVal !== oldVal) {
hasLanguageChanged.value = true
}
}
)
</script>

View File

@@ -66,7 +66,11 @@
</template>
{{ __('View Certificate') }}
</Button>
<Button v-else @click="openCallLink(event.venue)" class="w-full">
<Button
v-else-if="userIsEvaluator()"
@click="openCallLink(event.venue)"
class="w-full"
>
<template #prefix>
<Video class="h-4 w-4 stroke-1.5" />
</template>
@@ -83,21 +87,31 @@
class="flex flex-col space-y-4 p-5"
>
<div class="flex items-center justify-between">
<Rating v-model="evaluation.rating" :label="__('Rating')" />
<Rating
v-model="evaluation.rating"
:label="__('Rating')"
:disabled="!userIsEvaluator()"
/>
<FormControl
type="select"
:options="statusOptions"
v-model="evaluation.status"
:label="__('Status')"
class="w-1/2"
:disabled="!userIsEvaluator()"
/>
</div>
<Textarea
v-model="evaluation.summary"
:label="__('Summary')"
:rows="7"
:disabled="!userIsEvaluator()"
/>
<Button variant="solid" @click="saveEvaluation()">
<Button
v-if="userIsEvaluator()"
variant="solid"
@click="saveEvaluation()"
>
{{ __('Save') }}
</Button>
</div>
@@ -106,11 +120,13 @@
type="checkbox"
v-model="certificate.published"
:label="__('Published')"
:disabled="!userIsEvaluator()"
/>
<Link
v-model="certificate.template"
:label="__('Template')"
doctype="Print Format"
:disabled="!userIsEvaluator()"
:filters="{
doc_type: 'LMS Certificate',
}"
@@ -118,14 +134,20 @@
<FormControl
type="date"
v-model="certificate.issue_date"
:disabled="!userIsEvaluator()"
:label="__('Issue Date')"
/>
<FormControl
type="date"
v-model="certificate.expiry_date"
:disabled="!userIsEvaluator()"
:label="__('Expiry Date')"
/>
<Button variant="solid" @click="saveCertificate()">
<Button
v-if="userIsEvaluator()"
variant="solid"
@click="saveCertificate()"
>
{{ __('Save') }}
</Button>
</div>
@@ -163,6 +185,7 @@ import Rating from '@/components/Controls/Rating.vue'
import Link from '@/components/Controls/Link.vue'
const show = defineModel()
const user = inject('$user')
const dayjs = inject('$dayjs')
const tabIndex = ref(0)
const showCertification = ref(false)
@@ -175,9 +198,18 @@ const props = defineProps({
})
const evaluation = reactive({})
const certificate = reactive({})
watch(user, () => {
if (userIsEvaluator()) {
defaultTemplate.reload()
}
})
const userIsEvaluator = () => {
return user.data && user.data.name == props.event.evaluator
}
const defaultTemplate = createResource({
url: 'frappe.client.get_value',
makeParams(values) {
@@ -190,7 +222,6 @@ const defaultTemplate = createResource({
},
}
},
auto: true,
onSuccess(data) {
certificate.template = data.value
},

View File

@@ -18,7 +18,7 @@
>
<template #body-content>
<div class="flex flex-col gap-4">
<p>
<p class="text-ink-gray-9">
{{
__(
'Submit your resume to proceed with your application for this position. Upon submission, it will be shared with the job poster.'
@@ -51,7 +51,7 @@
<FileText class="h-5 w-5 stroke-1.5 text-ink-gray-7" />
</div>
<div class="flex flex-col">
<span>
<span class="text-ink-gray-9">
{{ resume.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">

View File

@@ -126,7 +126,7 @@ import {
Button,
toast,
} from 'frappe-ui'
import { computed, watch, reactive, ref, inject } from 'vue'
import { watch, reactive, ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue'
import { useOnboarding } from 'frappe-ui/frappe'
@@ -141,6 +141,7 @@ const existingQuestion = reactive({
question: '',
marks: 1,
})
const question = reactive({
question: '',
type: 'Choices',

View File

@@ -23,7 +23,9 @@
</div>
<div v-if="currentTab" class="mt-4">
<div class="grid grid-cols-[55%,40%] gap-5">
<div class="space-y-5 border rounded-md p-2 pt-4">
<div
class="space-y-5 border rounded-md p-2 pt-4 h-[70vh] overflow-y-auto"
>
<div class="grid grid-cols-[70%,30%] text-sm text-ink-gray-5">
<div class="px-4">
{{ __('Member') }}
@@ -59,7 +61,7 @@
</div>
</div>
<div class="text-center text-sm">
{{ convertToMinutes(row.watch_time) }}
{{ formatTimestamp(row.watch_time) }}
</div>
</div>
</router-link>
@@ -69,7 +71,7 @@
<NumberChart
class="border rounded-md"
:config="{
title: __('Average Watch Time (minutes)'),
title: __('Average Watch Time'),
value: averageWatchTime,
}"
/>
@@ -97,7 +99,7 @@ import {
TabButtons,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { enablePlyr, convertToMinutes } from '@/utils'
import { enablePlyr, formatTimestamp } from '@/utils'
import VideoBlock from '@/components/VideoBlock.vue'
const show = defineModel<boolean | undefined>()
@@ -185,7 +187,7 @@ const averageWatchTime = computed(() => {
totalWatchTime += parseFloat(item.watch_time)
})
return convertToMinutes(totalWatchTime / currentTabData.value.length)
return formatTimestamp(totalWatchTime / currentTabData.value.length)
})
const currentTabData = computed(() => {

View File

@@ -1,13 +1,13 @@
<template>
<div class="border rounded-md w-1/3 mx-auto my-32">
<div class="border-b px-5 py-3 font-medium">
<div class="border-b px-5 py-3 font-medium text-ink-gray-9">
<span
class="inline-flex items-center before:bg-surface-red-5 before:w-2 before:h-2 before:rounded-md before:mr-2"
></span>
{{ __('Not Permitted') }}
</div>
<div v-if="user.data" class="px-5 py-3">
<div>
<div class="text-ink-gray-7">
{{ __('You do not have permission to access this page.') }}
</div>
<router-link
@@ -21,7 +21,7 @@
</router-link>
</div>
<div class="px-5 py-3">
<div>
<div class="text-ink-gray-7">
{{ __('Please login to access this page.') }}
</div>
<Button @click="redirectToLogin()" class="mt-4">

View File

@@ -1,13 +1,13 @@
<template>
<div class="text-base border rounded-md w-1/3 mx-auto my-32">
<div class="border-b px-5 py-3 font-medium">
<div class="border-b px-5 py-3 font-medium text-ink-gray-9">
<span
class="inline-flex items-center before:bg-surface-red-5 before:w-2 before:h-2 before:rounded-md before:mr-2"
></span>
{{ __(title) }}
</div>
<div class="px-5 py-3">
<div class="mb-4 leading-6">
<div class="mb-4 leading-6 text-ink-gray-7">
{{ __(text) }}
</div>
<Button variant="solid" class="w-full" @click="redirect()">

View File

@@ -0,0 +1,241 @@
<template>
<div
class="text-sm absolute bg-white border rounded-md z-10 w-44"
:style="{
display: top > 0 ? 'block' : 'none',
top: top + 'px',
left: left + 'px',
}"
>
<div class="space-y-2 py-2">
<div class="text-xs text-ink-gray-5 font-medium px-3">
{{ __('Highlight') }}
</div>
<div class="">
<div
v-for="color in colors"
class="flex items-center space-x-2 px-3 py-2 cursor-pointer hover:bg-surface-gray-2"
@click="saveHighLight(color)"
>
<span
class="size-3 rounded-full"
:style="{
backgroundColor: theme.backgroundColor[color.toLowerCase()][400],
}"
></span>
<span>
{{ __(color) }}
</span>
</div>
</div>
</div>
<div class="border-t">
<div
@click="addToNotes()"
class="flex items-center space-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
>
<NotepadText class="size-3 stroke-1.5" />
<span>
{{ __('Add to Notes') }}
</span>
</div>
<div
v-if="highlightExists()"
@click="deleteHighlight"
class="flex items-center space-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
>
<Trash2 class="size-3 stroke-1.5" />
<span>
{{ __('Remove Highlight') }}
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, inject, ref, watch } from 'vue'
import { NotepadText, Trash2 } from 'lucide-vue-next'
import { theme } from '@/utils/theme'
import type { Note, Notes } from '@/components/Notes/types'
import { blockQuotesClick, highlightText } from '@/utils'
const user = inject<any>('$user')
const show = defineModel()
const notes = defineModel<Notes>('notes')
const top = ref(0)
const left = ref(0)
const currentSelection = ref<Selection | null>(null)
const selectedText = ref('')
const emit = defineEmits<{
(e: 'updateNotes'): void
}>()
const props = defineProps<{
lesson: string
}>()
watch(show, () => {
if (!show.value) {
return resetMenuPosition()
}
currentSelection.value = window.getSelection()
if (!currentSelection.value?.toString()) {
return resetMenuPosition()
}
updateMenuPosition()
})
const updateMenuPosition = () => {
selectedText.value = currentSelection.value?.toString() || ''
const range = currentSelection.value?.getRangeAt(0)
const rect = range?.getBoundingClientRect()
if (!rect) return
const offsetY = window.scrollY
const offsetX = window.scrollX
top.value = Math.floor(rect.top + offsetY - 40)
left.value = Math.floor(rect.right + offsetX + 10)
}
const resetMenuPosition = () => {
top.value = 0
left.value = 0
}
const colors = computed(() => {
return ['Red', 'Blue', 'Green', 'Yellow', 'Purple']
})
const highlightExists = () => {
return notes.value?.data?.some(
(note: Note) => note.highlighted_text === selectedText.value
)
}
const saveHighLight = (color: string) => {
if (!selectedText.value) return
notes.value?.insert.submit(
{
lesson: props.lesson,
member: user?.data?.name,
highlighted_text: selectedText.value,
color: color,
name: '',
},
{
onSuccess(data: Note) {
highlightText(data)
resetStates()
emit('updateNotes')
},
onError(err: any) {
console.error('Error saving highlight:', err)
resetStates()
},
}
)
}
const deleteHighlight = () => {
let notesToDelete = notes.value?.data.find(
(note: Note) => note.highlighted_text === selectedText.value
)
if (!notesToDelete) return
notes.value?.delete.submit(notesToDelete.name, {
onSuccess() {
resetStates()
document.querySelectorAll('.highlighted-text').forEach((el) => {
const element = el as HTMLElement
if (element.dataset.name === notesToDelete.name) {
element.style.backgroundColor = 'transparent'
}
})
},
onError(err: any) {
console.error('Error deleting highlight:', err)
resetStates()
},
})
}
const addToNotes = () => {
if (!selectedText.value) return
let noteToUpdate = notes.value?.data.find((note: Note) => {
return !note.highlighted_text && note.note !== ''
})
if (!noteToUpdate) {
createNote()
} else {
updateNote(noteToUpdate)
}
}
const createNote = () => {
notes.value?.insert.submit(
{
lesson: props.lesson,
member: user?.data?.name,
note: `<blockquote><p>${selectedText.value}</p></blockquote><br>`,
color: 'Yellow',
name: '',
},
{
onSuccess(data: Note) {
emit('updateNotes')
setTimeout(() => {
scrollToText(selectedText.value)
blockQuotesClick()
resetStates()
}, 100)
},
onError(err: any) {
console.error('Error creating note:', err)
resetStates()
},
}
)
}
const updateNote = (noteToUpdate: Note) => {
notes.value?.setValue.submit(
{
name: noteToUpdate.name,
note: `${noteToUpdate.note}\n\n<blockquote><p>${selectedText.value}</p></blockquote><br>`,
},
{
onSuccess(data: Note) {
emit('updateNotes')
setTimeout(() => {
scrollToText(selectedText.value)
blockQuotesClick()
resetStates()
}, 100)
},
onError(err: any) {
console.error('Error updating note:', err)
resetStates()
},
}
)
}
const scrollToText = (text: string) => {
const elements = document.querySelectorAll('blockquote p')
Array.from(elements).forEach((el) => {
const element = el as HTMLElement
if (element.textContent?.toLowerCase().includes(text.toLowerCase())) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
})
}
const resetStates = () => {
selectedText.value = ''
show.value = false
resetMenuPosition()
}
</script>

View File

@@ -0,0 +1,115 @@
<template>
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
{{ __('My Notes') }}
</div>
<TextEditor
:content="note"
:placeholder="__('Make notes for quick revision. Press / for menu.')"
@change="(val: string) => updateNoteText(val)"
:editable="true"
editorClass="prose prose-sm min-h-[200px] max-w-none"
/>
</template>
<script setup lang="ts">
import { TextEditor } from 'frappe-ui'
import { useDebounceFn } from '@vueuse/core'
import { inject, ref, onMounted, watch } from 'vue'
import type { Note, Notes } from '@/components/Notes/types'
import { blockQuotesClick } from '@/utils/'
const note = ref<string | null>(null)
const currentNoteName = ref<string | null>(null)
const user = inject<any>('$user')
const notes = defineModel<Notes>('notes')
const emit = defineEmits<{
(e: 'updateNotes'): void
}>()
const props = defineProps<{
lesson: string
}>()
onMounted(() => {
updateCurrentNote()
})
watch(
() => notes.value?.data,
() => {
updateCurrentNote()
blockQuotesClick()
}
)
const updateCurrentNote = () => {
const currentNote = notes.value?.data?.filter((row: Note) => {
return !row.highlighted_text && row.note !== ''
})
if (currentNote?.length === 0) {
note.value = null
currentNoteName.value = null
return
} else if (currentNote && currentNote.length > 0) {
currentNoteName.value = currentNote[0].name
note.value = currentNote[0].note || null
}
}
const updateNoteText = (val: string) => {
note.value = val
debouncedSave()
}
const debouncedSave = useDebounceFn(() => {
saveNotes()
}, 2000)
const saveNotes = () => {
if (currentNoteName.value) {
updateNote()
} else {
createNote()
}
}
const createNote = () => {
notes.value?.insert.submit(
{
lesson: props.lesson,
member: user?.data?.name,
note: note.value,
color: 'Yellow',
name: '',
},
{
onSuccess(data: Note) {
currentNoteName.value = data.name || null
emit('updateNotes')
},
onError(err: any) {
console.error('Error creating note:', err)
},
}
)
}
const updateNote = () => {
if (!currentNoteName.value) return
notes.value?.setValue.submit(
{
name: currentNoteName.value,
lesson: props.lesson,
member: user?.data?.name,
note: note.value,
},
{
onSuccess(data: Note) {
emit('updateNotes')
},
onError(err: any) {
console.error('Error updating note:', err)
},
}
)
}
</script>

View File

@@ -0,0 +1,32 @@
export type Note = {
highlighted_text?: string
color?: string
name: string
note?: string | null
lesson?: string
member?: string
}
export type Notes = {
data: Note[]
reload: () => void
insert: {
submit: (
data: Note,
options: { onSuccess: (data: Note) => void; onError: (err: any) => void }
) => void
}
setValue: {
submit: (
data: Note,
options: { onSuccess: (data: Note) => void; onError: (err: any) => void }
) => void
},
delete: {
submit: (
data: Note | string,
options?: { onSuccess: () => void; onError: (err: any) => void }
) => void
}
}

View File

@@ -55,8 +55,8 @@
<div v-if="quiz.data.duration" class="flex flex-col space-x-1 my-4">
<div class="mb-2">
<span class=""> {{ __('Time') }}: </span>
<span class="font-semibold">
<span class="text-ink-gray-9"> {{ __('Time') }}: </span>
<span class="font-semibold text-ink-gray-9">
{{ formatTimer(timer) }}
</span>
</div>
@@ -165,14 +165,14 @@
</div>
</div>
<span
class="ml-2"
class="ml-2 text-ink-gray-9"
v-html="questionDetails.data[`option_${index}`]"
>
</span>
</label>
<div
v-if="questionDetails.data[`explanation_${index}`]"
class="mt-2 text-xs"
class="mt-2 text-xs text-ink-gray-7"
v-show="showAnswers.length"
>
{{ questionDetails.data[`explanation_${index}`] }}
@@ -260,7 +260,7 @@
)
}}
</div>
<div v-else>
<div v-else class="text-ink-gray-7">
{{
__(
'You got {0}% correct answers with a score of {1} out of {2}'
@@ -574,7 +574,17 @@ const addToLocalStorage = () => {
return answer != undefined
}),
}
quizData ? quizData.push(questionData) : (quizData = [questionData])
if (quizData) {
let existingQuestion = quizData.find(
(q) => q.question_name == questionData.question_name
)
if (!existingQuestion) {
quizData.push(questionData)
}
} else {
quizData = [questionData]
}
localStorage.setItem(quiz.data.title, JSON.stringify(quizData))
}

View File

@@ -69,9 +69,12 @@ const update = () => {
let imageFields = ['favicon', 'banner_image']
props.fields.forEach((f) => {
if (imageFields.includes(f.name)) {
fieldsToSave[f.name] = f.value ? f.value.file_url : null
fieldsToSave[f.name] =
branding.data[f.name] && branding.data[f.name].file_url
? branding.data[f.name].file_url
: null
} else {
fieldsToSave[f.name] = f.value
fieldsToSave[f.name] = branding.data[f.name]
}
})

View File

@@ -141,9 +141,6 @@ const props = defineProps({
type: String,
default: '',
},
show: {
type: Boolean,
},
})
const evaluators = createListResource({

View File

@@ -117,14 +117,7 @@
</Dialog>
</template>
<script setup lang="ts">
import {
Avatar,
Badge,
Button,
createResource,
Dialog,
FormControl,
} from 'frappe-ui'
import { Avatar, Button, createResource, Dialog, FormControl } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, watch, reactive, inject } from 'vue'
import { RefreshCw, Plus, Search, Shield } from 'lucide-vue-next'
@@ -146,7 +139,6 @@ const start = ref(0)
const memberList = ref<Member[]>([])
const hasNextPage = ref(false)
const showForm = ref(false)
const dayjs = inject('$dayjs')
const user = inject<User | null>('$user')
const { updateOnboardingStep } = useOnboarding('learning')
@@ -164,9 +156,6 @@ const props = defineProps({
type: String,
default: '',
},
show: {
type: Boolean,
},
})
const members = createResource({

View File

@@ -0,0 +1,233 @@
<template>
<Dialog
v-model="show"
:options="{
title:
gatewayID === 'new'
? __('New Payment Gateway')
: __('Edit Payment Gateway'),
size: '3xl',
}"
>
<template #body-content>
<SettingFields
v-if="gatewayID != 'new' && paymentGateway.data"
:fields="paymentGateway.data.fields"
:data="paymentGateway.data.data"
class="pt-5 my-0"
/>
<div v-else>
<FormControl
v-model="newGateway"
:label="__('Select Payment Gateway')"
type="select"
:options="allGatewayOptions"
:required="true"
/>
<SettingFields
v-if="newGateway"
:fields="newGatewayFields"
:data="newGatewayData"
class="pt-5 my-0"
/>
</div>
</template>
<template #actions="{ close }">
<div class="pb-5 float-right">
<Button variant="solid" @click="saveSettings(close)">
{{ __('Save') }}
</Button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import {
Button,
call,
createListResource,
createResource,
Dialog,
FormControl,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import SettingFields from '@/components/Settings/SettingFields.vue'
const show = defineModel<boolean>({ required: true, default: false })
const paymentGateways = defineModel<any>('paymentGateways')
const newGateway = ref(null)
const newGatewayFields = ref([])
const newGatewayData = ref<Record<string, any>>({})
const props = defineProps<{
gatewayID: string | null
}>()
const paymentGateway = createResource({
url: 'lms.lms.api.get_payment_gateway_details',
makeParams(values: any) {
return {
payment_gateway: props.gatewayID,
}
},
transform(data: any) {
arrangeFields(data.fields)
return data
},
})
const allGateways = createListResource({
doctype: 'DocType',
filters: {
module: 'Payment Gateways',
},
fields: ['name', 'issingle'],
})
const gatewayFields = createResource({
url: 'lms.lms.api.get_new_gateway_fields',
makeParams(values: any) {
return {
doctype: values.doctype,
}
},
})
const arrangeFields = (fields: any[]) => {
fields = fields.sort((a, b) => {
if (a.type === 'Upload' && b.type !== 'Upload') {
return 1
} else if (a.type !== 'Upload' && b.type === 'Upload') {
return -1
}
return 0
})
fields.splice(3, 0, {
type: 'Column Break',
})
}
watch(
() => props.gatewayID,
() => {
if (props.gatewayID && props.gatewayID !== 'new') {
paymentGateway.reload()
} else if (props.gatewayID == 'new') {
allGateways.reload()
}
}
)
const getNewGateway = () => {
return allGateways.data?.find((gateway: any) =>
gateway.name.includes(newGateway.value)
)
}
watch(newGateway, () => {
let gatewayDoc = getNewGateway()
gatewayFields.reload({ doctype: gatewayDoc.name }).then(() => {
let fields = gatewayFields.data || []
arrangeFields(fields)
newGatewayFields.value = fields
prepareGatewayData()
})
})
const saveSettings = (close: () => void) => {
if (props.gatewayID === 'new') {
saveNewGateway(close)
} else {
saveExistingGateway(
paymentGateway.data.doctype,
paymentGateway.data.docname,
close
)
}
}
const saveNewGateway = (close: () => void) => {
let gatewayDoc = getNewGateway()
if (gatewayDoc.issingle) {
saveExistingGateway(gatewayDoc.name, gatewayDoc.name, close)
} else {
call('frappe.client.insert', {
doc: {
doctype: gatewayDoc.name,
...newGatewayData.value,
},
}).then((data: any) => {
paymentGateways.value.reload()
close()
})
}
}
const saveExistingGateway = (
doctype: string,
docname: string,
close: () => void
) => {
call('frappe.client.set_value', {
doctype: doctype,
name: docname,
fieldname: getGatewayFields(),
}).then(() => {
paymentGateways.value?.reload()
close()
})
}
const getGatewayFields = () => {
let data =
props.gatewayID == 'new' ? newGatewayData.value : paymentGateway.data.data
return Object.keys(data).reduce((fields: any, key: string) => {
if (data[key] && typeof data[key] === 'object') {
fields[key] = data[key].file_url
} else {
fields[key] = data[key]
}
return fields
}, {})
}
const createGatewayRecord = (gatewayDoc: any, data: any = {}) => {
call('frappe.client.insert', {
doc: {
doctype: 'Payment Gateway',
gateway: newGateway.value,
gateway_controller: gatewayDoc.issingle ? '' : gatewayDoc.name,
gateway_settings: gatewayDoc.issingle ? '' : data.name,
},
}).then(() => {
paymentGateways.value?.reload()
})
}
const allGatewayOptions = computed(() => {
let options: string[] = []
let gatewayList = allGateways.data?.map((gateway: any) => gateway.name) || []
gatewayList.forEach((gateway: any) => {
let gatewayName = gateway.split(' ')[0]
let existingGateways =
paymentGateways.value?.data?.map((pg: any) => pg.name) || []
if (
!options.includes(gatewayName) &&
!existingGateways.includes(gatewayName)
) {
options.push(gatewayName)
}
})
return options.map((gateway: string) => ({ label: gateway, value: gateway }))
})
const prepareGatewayData = () => {
newGatewayData.value = {}
if (newGatewayFields.value.length) {
newGatewayFields.value.forEach((field: any) => {
newGatewayData.value[field.fieldname] = field.default || ''
})
}
}
</script>

View File

@@ -0,0 +1,140 @@
<template>
<div class="flex min-h-0 flex-col text-base">
<div class="flex items-center justify-between mb-5">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
</div>
</div>
<Button @click="openForm('new')">
<template #prefix>
<Plus class="h-3 w-3 stroke-1.5" />
</template>
{{ __('New') }}
</Button>
</div>
<div v-if="paymentGateways.data?.length" class="overflow-y-scroll">
<ListView
:columns="columns"
:rows="paymentGateways.data"
row-key="name"
:options="{
showTooltip: false,
onRowClick: (row) => {
openForm(row.name)
},
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
<FeatherIcon
v-if="item.icon"
:name="item.icon"
class="h-4 w-4 stroke-1.5"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in paymentGateways.data">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div v-if="column.key == 'enabled'">
<Badge v-if="row[column.key]" theme="green">
{{ __('Enabled') }}
</Badge>
<Badge v-else theme="gray">
{{ __('Disabled') }}
</Badge>
</div>
<div v-else class="leading-5 text-sm">
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="removeAccount(selections, unselectAll)"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
</div>
</div>
<PaymentGatewayDetails
v-model="showForm"
:gatewayID="currentGateway"
v-model:paymentGateways="paymentGateways"
/>
</template>
<script setup>
import {
Badge,
Button,
createListResource,
FeatherIcon,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,
ListSelectBanner,
} from 'frappe-ui'
import { computed, ref } from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next'
import PaymentGatewayDetails from '@/components/Settings/PaymentGatewayDetails.vue'
const showForm = ref(false)
const currentGateway = ref(null)
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
})
const paymentGateways = createListResource({
doctype: 'Payment Gateway',
fields: ['name', 'gateway_settings', 'gateway_controller'],
auto: true,
orderBy: 'modified desc',
})
const openForm = (gatewayID) => {
currentGateway.value = gatewayID
showForm.value = true
}
const columns = computed(() => {
return [
{
label: __('Gateway'),
key: 'name',
icon: 'credit-card',
},
]
})
</script>

View File

@@ -1,128 +0,0 @@
<template>
<div class="flex flex-col h-full">
<div class="flex items-center justify-between">
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ label }}
</div>
<!-- <Badge
v-if="isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/> -->
</div>
<div class="overflow-y-scroll">
<div class="flex flex-col divide-y">
<SettingFields :fields="fields" :data="data.doc" />
<SettingFields
v-if="paymentGateway.data"
:fields="paymentGateway.data.fields"
:data="paymentGateway.data.data"
class="pt-5 my-0"
/>
</div>
</div>
<div class="flex flex-row-reverse mt-auto">
<Button variant="solid" @click="update">
{{ __('Update') }}
</Button>
</div>
</div>
</template>
<script setup>
import SettingFields from '@/components/Settings/SettingFields.vue'
import { createResource, Badge, Button } from 'frappe-ui'
import { watch } from 'vue'
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
fields: {
type: Array,
required: true,
},
})
const paymentGateway = createResource({
url: 'lms.lms.api.get_payment_gateway_details',
makeParams(values) {
return {
payment_gateway: props.data.doc.payment_gateway,
}
},
transform(data) {
arrangeFields(data.fields)
return data
},
auto: true,
})
const arrangeFields = (fields) => {
fields = fields.sort((a, b) => {
if (a.type === 'Upload' && b.type !== 'Upload') {
return 1
} else if (a.type !== 'Upload' && b.type === 'Upload') {
return -1
}
return 0
})
fields.splice(3, 0, {
type: 'Column Break',
})
}
const saveSettings = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
let fields = {}
Object.keys(paymentGateway.data.data).forEach((key) => {
if (
paymentGateway.data.data[key] &&
typeof paymentGateway.data.data[key] === 'object'
) {
fields[key] = paymentGateway.data.data[key].file_url
} else {
fields[key] = paymentGateway.data.data[key]
}
})
return {
doctype: paymentGateway.data.doctype,
name: paymentGateway.data.docname,
fieldname: fields,
}
},
auto: false,
onSuccess(data) {
paymentGateway.reload()
},
})
const update = () => {
props.fields.forEach((f) => {
if (f.type != 'Column Break') {
props.data.doc[f.name] = f.value
}
})
props.data.save.submit()
saveSettings.submit()
}
watch(
() => props.data.doc.payment_gateway,
() => {
paymentGateway.reload()
}
)
</script>

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