Compare commits
1310 Commits
fixes
...
copy-minor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9bf4e2c58 | ||
|
|
78698cfcbe | ||
|
|
64c02f14a9 | ||
|
|
2f68fc0d6e | ||
|
|
16570469e6 | ||
|
|
a62b754d28 | ||
|
|
93859d6635 | ||
|
|
20fab8dbd3 | ||
|
|
13536aac51 | ||
|
|
ccb8721674 | ||
|
|
fbe1423edd | ||
|
|
d2713d7824 | ||
|
|
f24a15b4f8 | ||
|
|
bf74868bd4 | ||
|
|
6d75b8a3a2 | ||
|
|
055f917c61 | ||
|
|
d892a28069 | ||
|
|
838bc1fac2 | ||
|
|
a98d36513b | ||
|
|
2cfa6771ac | ||
|
|
4960c47377 | ||
|
|
a46306720b | ||
|
|
efcdba3a29 | ||
|
|
e7263d0566 | ||
|
|
5de7f5e283 | ||
|
|
c7bdf68bc6 | ||
|
|
998ff51c58 | ||
|
|
4eb9d84d8b | ||
|
|
5f000d8017 | ||
|
|
0cf9ad5228 | ||
|
|
71a526e7aa | ||
|
|
c5d9adb7fd | ||
|
|
05fdf163a9 | ||
|
|
de0a983033 | ||
|
|
44ee9b644f | ||
|
|
bd116c3e7b | ||
|
|
02e8a97f85 | ||
|
|
3525e4c90b | ||
|
|
e6d3819092 | ||
|
|
f15862cef4 | ||
|
|
86748b301d | ||
|
|
cc07dd849c | ||
|
|
63bcbb6506 | ||
|
|
83a1b03bb7 | ||
|
|
2905a6af1a | ||
|
|
4cc27adb8b | ||
|
|
2126b4f657 | ||
|
|
0ce7c74778 | ||
|
|
b9f6a23412 | ||
|
|
9ae96bd1fa | ||
|
|
e863abe37c | ||
|
|
80e9984db0 | ||
|
|
8f504a8043 | ||
|
|
60a917e60c | ||
|
|
a5d000f702 | ||
|
|
5317aa8fb5 | ||
|
|
39aa1d443d | ||
|
|
36c4e2f4dc | ||
|
|
084eeba2ed | ||
|
|
4b4086afb3 | ||
|
|
6401497422 | ||
|
|
684601f31b | ||
|
|
d7d222842b | ||
|
|
b0bb7d32ca | ||
|
|
ff1bd91223 | ||
|
|
f59f6c617a | ||
|
|
af48ccfb57 | ||
|
|
4b9d3bd996 | ||
|
|
3dad3580bb | ||
|
|
53eb95612c | ||
|
|
8f687145be | ||
|
|
9c405edd09 | ||
|
|
fe791dc478 | ||
|
|
8f317d2f44 | ||
|
|
f4e581f6cb | ||
|
|
9671c4d63f | ||
|
|
42417621fa | ||
|
|
d3b3d85c84 | ||
|
|
b700013704 | ||
|
|
bac229c731 | ||
|
|
28043e634b | ||
|
|
b672108155 | ||
|
|
5e569ab0e6 | ||
|
|
b07940951c | ||
|
|
1f18ef4362 | ||
|
|
bf57a19e2c | ||
|
|
43a07e53a6 | ||
|
|
fbd83196fc | ||
|
|
465f4e1e96 | ||
|
|
43d409ce64 | ||
|
|
a5fc52ec29 | ||
|
|
a9b06575d0 | ||
|
|
3070cbed3c | ||
|
|
0845a6e2a3 | ||
|
|
041bae16e0 | ||
|
|
3313db844c | ||
|
|
3a5977a718 | ||
|
|
bcee74ce77 | ||
|
|
1a6a119f35 | ||
|
|
09ae61492f | ||
|
|
3a33f047f5 | ||
|
|
10cdd712d2 | ||
|
|
21959eef7b | ||
|
|
41c3522285 | ||
|
|
d712881e16 | ||
|
|
991dc7f8c8 | ||
|
|
7087fde686 | ||
|
|
6b6c8da785 | ||
|
|
e1d61c9eb9 | ||
|
|
afcb15148f | ||
|
|
f40fbaed3e | ||
|
|
5adb36deaf | ||
|
|
4973386dd0 | ||
|
|
13536b8bad | ||
|
|
caea7e334c | ||
|
|
4065b1b8cc | ||
|
|
b248774774 | ||
|
|
7a9d6325d5 | ||
|
|
b0d0b41502 | ||
|
|
30c89cb13c | ||
|
|
eb3afbbad1 | ||
|
|
9175737b9c | ||
|
|
7ae772205a | ||
|
|
00b0a20c83 | ||
|
|
6604866342 | ||
|
|
881c3d943a | ||
|
|
fbe219a888 | ||
|
|
5928b8e5f9 | ||
|
|
372425bed2 | ||
|
|
d2922fd361 | ||
|
|
d5118cc91f | ||
|
|
ac74cbdf72 | ||
|
|
01f7fc3cff | ||
|
|
85c850e5bf | ||
|
|
e7b6001e5f | ||
|
|
4053984ca2 | ||
|
|
a1e06bf316 | ||
|
|
67dfffdd58 | ||
|
|
c50f2147fd | ||
|
|
d4671fb888 | ||
|
|
ae4aadb8d3 | ||
|
|
e5dc2bad6a | ||
|
|
77cda10419 | ||
|
|
6de879cd2a | ||
|
|
0e2fabf139 | ||
|
|
c45a372e83 | ||
|
|
25f24b98c6 | ||
|
|
98ecb4c27c | ||
|
|
9023094326 | ||
|
|
497de05db2 | ||
|
|
11079dae00 | ||
|
|
d00da31f84 | ||
|
|
644fb698d8 | ||
|
|
92edb3a1bf | ||
|
|
cb3224664e | ||
|
|
9b532a5470 | ||
|
|
f1f9d9790b | ||
|
|
96190910a7 | ||
|
|
6484763d37 | ||
|
|
0e2feac81e | ||
|
|
6f1e7624ec | ||
|
|
eef5bd6062 | ||
|
|
63bcf15900 | ||
|
|
25bcd10e93 | ||
|
|
de60fbb25a | ||
|
|
fd9a638879 | ||
|
|
ddcb718a3a | ||
|
|
a17a7453e7 | ||
|
|
479be0b8ee | ||
|
|
6f40c357b3 | ||
|
|
81db6c544d | ||
|
|
be4e3aa963 | ||
|
|
6da0c07a3d | ||
|
|
b4ad10ca35 | ||
|
|
2388b878dc | ||
|
|
8cdaa7877a | ||
|
|
d314287883 | ||
|
|
b70dfc8e82 | ||
|
|
0a784766b4 | ||
|
|
a5a7184f9a | ||
|
|
4e019d0a43 | ||
|
|
8453b54360 | ||
|
|
9f9dfdb26d | ||
|
|
9fd4984247 | ||
|
|
9ebd64f47d | ||
|
|
4316a37ed6 | ||
|
|
2d745460e8 | ||
|
|
b5258b6d9f | ||
|
|
41b076c0db | ||
|
|
9d65e5e398 | ||
|
|
7250bf7d65 | ||
|
|
4d7b247378 | ||
|
|
0aaa58cd54 | ||
|
|
014b85f12c | ||
|
|
929f97cb72 | ||
|
|
de9cb935ee | ||
|
|
9aafc176e4 | ||
|
|
0488ae8305 | ||
|
|
60fd317d98 | ||
|
|
e54435d85d | ||
|
|
3a23b91c90 | ||
|
|
69591577bf | ||
|
|
e56afba6d3 | ||
|
|
98536ce4c7 | ||
|
|
05282178dd | ||
|
|
1af547288c | ||
|
|
b4af82acbc | ||
|
|
50fbe00d23 | ||
|
|
b44428677e | ||
|
|
d67faa1610 | ||
|
|
7b3f4c29d8 | ||
|
|
a49871c5b1 | ||
|
|
e4005792af | ||
|
|
8c0c09a21b | ||
|
|
a9b05f4256 | ||
|
|
cb6013a7a6 | ||
|
|
bb23b78a4f | ||
|
|
243277012f | ||
|
|
c9ed8a4b03 | ||
|
|
d413acaef3 | ||
|
|
d6aad6cd74 | ||
|
|
ca45e43003 | ||
|
|
ad39530705 | ||
|
|
a6c2378b56 | ||
|
|
c073d2201d | ||
|
|
6d70de2eb1 | ||
|
|
48982e8f4a | ||
|
|
397128f980 | ||
|
|
1d77fd3f94 | ||
|
|
60e78e8e74 | ||
|
|
4a9ccc6fde | ||
|
|
a707095fae | ||
|
|
d4f662f65e | ||
|
|
509b1365d9 | ||
|
|
d0b236e381 | ||
|
|
fe98265636 | ||
|
|
3f7d1b1e83 | ||
|
|
52cde329c1 | ||
|
|
68b2dd6147 | ||
|
|
5fa0d022dc | ||
|
|
d996a5c53f | ||
|
|
b6dfc6ed4d | ||
|
|
c7c2ba83f3 | ||
|
|
2bffabff05 | ||
|
|
697e81df10 | ||
|
|
f1b791845b | ||
|
|
6310845cdd | ||
|
|
230cca63f3 | ||
|
|
af9f4d4b1e | ||
|
|
0111ff9c99 | ||
|
|
12bec14c92 | ||
|
|
174ea1ddd4 | ||
|
|
038a7463e1 | ||
|
|
a702909216 | ||
|
|
8effd5614f | ||
|
|
a02365c223 | ||
|
|
2d589aefa2 | ||
|
|
bc2dc679a8 | ||
|
|
f2432d78ee | ||
|
|
f27eecce1f | ||
|
|
caf967f2e2 | ||
|
|
eecc9b53df | ||
|
|
8e12cae91f | ||
|
|
12c5ad54e7 | ||
|
|
c20fa7e093 | ||
|
|
4c83264c4a | ||
|
|
f592cf08d8 | ||
|
|
bf0cb25a88 | ||
|
|
2ff3d83d8f | ||
|
|
3f5c3e89c8 | ||
|
|
a1bb7962bc | ||
|
|
bf5cc5e1d1 | ||
|
|
d840d2fc18 | ||
|
|
1e458921e8 | ||
|
|
55feb41998 | ||
|
|
a7dbdd844b | ||
|
|
f3d6ad6c84 | ||
|
|
a0255e1743 | ||
|
|
1046d28092 | ||
|
|
affd2b47bd | ||
|
|
814870fd69 | ||
|
|
50c1a566a8 | ||
|
|
47783997c6 | ||
|
|
70d8505596 | ||
|
|
6e8cf9ca25 | ||
|
|
c9cda6c6f5 | ||
|
|
6c4d3ea37e | ||
|
|
8ad0e99b3c | ||
|
|
60277ed6e9 | ||
|
|
7b570420ca | ||
|
|
9205b59e29 | ||
|
|
685c5babe5 | ||
|
|
681dc8fbc1 | ||
|
|
7c623b1a8d | ||
|
|
277c089adc | ||
|
|
cfc3c231ff | ||
|
|
1000a22490 | ||
|
|
65c0ebac50 | ||
|
|
80843ec44b | ||
|
|
be23220e01 | ||
|
|
c82c10d17e | ||
|
|
5918b8be60 | ||
|
|
647a0a8ff1 | ||
|
|
bf3c6bc6be | ||
|
|
cc7832614b | ||
|
|
d5387a0d1a | ||
|
|
5d7ad973a2 | ||
|
|
0fcea692c7 | ||
|
|
db71f1271b | ||
|
|
3fde923190 | ||
|
|
4f97760e8a | ||
|
|
5614a6203f | ||
|
|
5727b7cd73 | ||
|
|
1c0644aa7a | ||
|
|
23e6ebe8ee | ||
|
|
5602c0b6c3 | ||
|
|
8e59c10b90 | ||
|
|
90587b0508 | ||
|
|
153a8428f7 | ||
|
|
3d00d96716 | ||
|
|
fb9824301f | ||
|
|
33f4e82399 | ||
|
|
0d99269109 | ||
|
|
8098532215 | ||
|
|
24e9f46e2f | ||
|
|
7c3b40f9d5 | ||
|
|
29860583f4 | ||
|
|
9c00a5561a | ||
|
|
82b8853f39 | ||
|
|
c4ab91a565 | ||
|
|
87e5096f5d | ||
|
|
1a07021bbf | ||
|
|
6ab4f15d0c | ||
|
|
f137f8e048 | ||
|
|
04501143ec | ||
|
|
07276f5c17 | ||
|
|
a9bd01b34e | ||
|
|
93db82305f | ||
|
|
ce09f27373 | ||
|
|
ffd9d56896 | ||
|
|
833e714a1f | ||
|
|
e402f322f6 | ||
|
|
2a0636b32b | ||
|
|
677dc59399 | ||
|
|
7678b89995 | ||
|
|
db408b21d2 | ||
|
|
fe08f4cf09 | ||
|
|
ccb7c1485b | ||
|
|
4a76f42e35 | ||
|
|
6f31d50a27 | ||
|
|
018c26b885 | ||
|
|
f9f70f208f | ||
|
|
b940ddca25 | ||
|
|
cd82527ef3 | ||
|
|
f600f016ae | ||
|
|
27101ffd31 | ||
|
|
9376b0b010 | ||
|
|
e2c1f6aa2d | ||
|
|
e2287cc73c | ||
|
|
c350ce1b60 | ||
|
|
09dbe0fed7 | ||
|
|
12b3d16662 | ||
|
|
1676329eb2 | ||
|
|
86434ea320 | ||
|
|
de6b4f7fb2 | ||
|
|
6e488cba3e | ||
|
|
03a6cc85fe | ||
|
|
b8b32681bf | ||
|
|
7f67c6c6d9 | ||
|
|
0487b2c987 | ||
|
|
d5f10db250 | ||
|
|
74df0f19cf | ||
|
|
47c19b4e3d | ||
|
|
b197e36ba7 | ||
|
|
4b049dcf71 | ||
|
|
04ed7f412f | ||
|
|
ac64e59c43 | ||
|
|
d5524a8d67 | ||
|
|
bc350a2661 | ||
|
|
575fb623ba | ||
|
|
b7783659c9 | ||
|
|
bddd4743a7 | ||
|
|
ce8ac15faa | ||
|
|
46e7c83fae | ||
|
|
3dcb55f603 | ||
|
|
fb3e0832d6 | ||
|
|
982d6c9045 | ||
|
|
a061a89ee7 | ||
|
|
00bb71f714 | ||
|
|
e23cfac5a7 | ||
|
|
ed651959c1 | ||
|
|
01a1632a5a | ||
|
|
c2a6697f72 | ||
|
|
211775dba5 | ||
|
|
2ba85ba6a7 | ||
|
|
3b3f1d692f | ||
|
|
d90bb1e5ea | ||
|
|
0c14a1ab4c | ||
|
|
ea27acc683 | ||
|
|
b2122e7707 | ||
|
|
9cdc8a50f6 | ||
|
|
03620be7bb | ||
|
|
55296cd9cc | ||
|
|
5fefea1434 | ||
|
|
66dbe68a15 | ||
|
|
066e2ddc69 | ||
|
|
59f08ad4da | ||
|
|
551936e7c4 | ||
|
|
4660240395 | ||
|
|
5d0a50242e | ||
|
|
141a778c9a | ||
|
|
d83d6cf2d8 | ||
|
|
71dc32098e | ||
|
|
6e47b4a941 | ||
|
|
8479e90aeb | ||
|
|
24276b779d | ||
|
|
47e254ed9b | ||
|
|
9c021ef3b1 | ||
|
|
c284e95dc8 | ||
|
|
39663a872c | ||
|
|
14cefca735 | ||
|
|
55c56207c2 | ||
|
|
79d9f31db7 | ||
|
|
845b906851 | ||
|
|
5d2b19cc43 | ||
|
|
a5bc30f776 | ||
|
|
cce77a475a | ||
|
|
13a26321f5 | ||
|
|
e7a2eb7373 | ||
|
|
1cc168404a | ||
|
|
cef3d21ab3 | ||
|
|
d0ac0e4523 | ||
|
|
abaa7754a6 | ||
|
|
02e875cbdc | ||
|
|
a218257952 | ||
|
|
7dfcabde5e | ||
|
|
6573602dfc | ||
|
|
3c374f48b3 | ||
|
|
2412ef0260 | ||
|
|
d4dcfcdbc6 | ||
|
|
60aec7c801 | ||
|
|
1862d726ad | ||
|
|
3d27d5f755 | ||
|
|
0b3f76590f | ||
|
|
294832834c | ||
|
|
e3338e0236 | ||
|
|
5c7ae55775 | ||
|
|
bc8827547e | ||
|
|
7990675c5c | ||
|
|
0182db8030 | ||
|
|
295feccb49 | ||
|
|
b5005f41fe | ||
|
|
37f06a8ba4 | ||
|
|
71d421898d | ||
|
|
bf5a69cee4 | ||
|
|
11e6b8a372 | ||
|
|
d763dba204 | ||
|
|
9e5cd84214 | ||
|
|
14cf0c9ae1 | ||
|
|
7d410e9ec8 | ||
|
|
07c3d423aa | ||
|
|
5d8003549f | ||
|
|
b286daad16 | ||
|
|
caa9144a12 | ||
|
|
3606902753 | ||
|
|
1abb75a58e | ||
|
|
d35c15c384 | ||
|
|
1888209027 | ||
|
|
f3830bfdd5 | ||
|
|
0e1b91f1ec | ||
|
|
8353aa24f3 | ||
|
|
99f1a8dfc3 | ||
|
|
3d8237008f | ||
|
|
22199da7d4 | ||
|
|
d8e11f69cc | ||
|
|
c9a5c0801e | ||
|
|
bb0abe27cd | ||
|
|
7d18e1d928 | ||
|
|
da72513f6a | ||
|
|
6f8c161e03 | ||
|
|
ac1f02971f | ||
|
|
d19538abd2 | ||
|
|
b1ab7e7783 | ||
|
|
35c080fcc2 | ||
|
|
43e89d9dc2 | ||
|
|
547b69dd31 | ||
|
|
1ff8514b22 | ||
|
|
5fffe51c4e | ||
|
|
af9aa3e37b | ||
|
|
d644ee7ccd | ||
|
|
08e3278ca0 | ||
|
|
85feaa00fc | ||
|
|
76ba5c188e | ||
|
|
9941e0e936 | ||
|
|
89206f94f0 | ||
|
|
43128d7ea3 | ||
|
|
6f1026434d | ||
|
|
db35e3e425 | ||
|
|
1d8de792a5 | ||
|
|
0db47dfee1 | ||
|
|
fc086fdbc3 | ||
|
|
8a86d19b79 | ||
|
|
9198302f7e | ||
|
|
d076451ea8 | ||
|
|
c2e9ef59d6 | ||
|
|
100f72de9d | ||
|
|
5a32109d5d | ||
|
|
f5e7934906 | ||
|
|
9800c24939 | ||
|
|
dcd81e0a3f | ||
|
|
6574b55440 | ||
|
|
8474d1c8c4 | ||
|
|
38ae9ab9f5 | ||
|
|
659b35f03e | ||
|
|
414f126f1e | ||
|
|
b336d769c8 | ||
|
|
c173953c6a | ||
|
|
afac45e65f | ||
|
|
f0aa5b8744 | ||
|
|
a101e7b089 | ||
|
|
85903d5385 | ||
|
|
fe80ef9b85 | ||
|
|
961f8c1627 | ||
|
|
6c7fc9b317 | ||
|
|
2afa14d68e | ||
|
|
f6cdac4826 | ||
|
|
bb39999b84 | ||
|
|
70a036e5a7 | ||
|
|
0432751050 | ||
|
|
36b3b1d086 | ||
|
|
6a783e540b | ||
|
|
daeeb693d6 | ||
|
|
a0b06be422 | ||
|
|
4b2ba96435 | ||
|
|
10510e204f | ||
|
|
032749dd01 | ||
|
|
dc65bff772 | ||
|
|
56266a3774 | ||
|
|
93f0f8ab44 | ||
|
|
611cc4d5a1 | ||
|
|
552b0c9616 | ||
|
|
1327b033e6 | ||
|
|
ae42828771 | ||
|
|
1df6319164 | ||
|
|
95012072fc | ||
|
|
c01c248202 | ||
|
|
0093025e5d | ||
|
|
20398cb934 | ||
|
|
2792eb53b7 | ||
|
|
b2f8f796b9 | ||
|
|
39bb3149c9 | ||
|
|
1e610f7fbb | ||
|
|
c760fd5776 | ||
|
|
2162963926 | ||
|
|
c3f3a110c0 | ||
|
|
0e444ab7d3 | ||
|
|
752fe5b4ba | ||
|
|
dce369638a | ||
|
|
911cfe9d2f | ||
|
|
f4e882ba3e | ||
|
|
211c69bb41 | ||
|
|
b592172b82 | ||
|
|
40445cbb94 | ||
|
|
20c93b3a6b | ||
|
|
e2bf324fb4 | ||
|
|
b47ff80e9d | ||
|
|
e10feb3c36 | ||
|
|
b8471dd753 | ||
|
|
125c06952a | ||
|
|
4336839932 | ||
|
|
bbdfaa32e9 | ||
|
|
4f52a73029 | ||
|
|
1a2f693fea | ||
|
|
ab8b76cada | ||
|
|
b5240f0eec | ||
|
|
7777bd02e3 | ||
|
|
fcdd70dcc7 | ||
|
|
4eb5390ad8 | ||
|
|
3b5c47222d | ||
|
|
f97ae4e4e2 | ||
|
|
33db16a1a2 | ||
|
|
6232f8703e | ||
|
|
e6621ad866 | ||
|
|
6a1533191a | ||
|
|
a0782c7bf7 | ||
|
|
2b6436915d | ||
|
|
4c220a67f2 | ||
|
|
110aab00d6 | ||
|
|
8e8111d272 | ||
|
|
53d2e288d4 | ||
|
|
262c1ea371 | ||
|
|
7be9eb09e8 | ||
|
|
5fb7e88318 | ||
|
|
d9c50714f4 | ||
|
|
0a91e5aa05 | ||
|
|
9b70b4212f | ||
|
|
9f525d69b6 | ||
|
|
b09c4753da | ||
|
|
e4b4556210 | ||
|
|
fafd132768 | ||
|
|
67dc6d1f29 | ||
|
|
969cb37cfe | ||
|
|
94c2be9919 | ||
|
|
5089285913 | ||
|
|
e80920ad6c | ||
|
|
d6606ab898 | ||
|
|
41599348c4 | ||
|
|
92a8ce6ef4 | ||
|
|
690a86bb69 | ||
|
|
cde4b61cea | ||
|
|
f361c42a30 | ||
|
|
065646ed5d | ||
|
|
de399166f1 | ||
|
|
a3a4d7fbd0 | ||
|
|
a7c1595978 | ||
|
|
1a8d113ad8 | ||
|
|
72cbbf147f | ||
|
|
a0e6462c13 | ||
|
|
b37f259804 | ||
|
|
2fbe5dacb2 | ||
|
|
3150cf2510 | ||
|
|
3b1b375d5b | ||
|
|
3c0a29d4c7 | ||
|
|
817bc4441f | ||
|
|
07e1aaaa66 | ||
|
|
35b77a8908 | ||
|
|
d5b4af95ff | ||
|
|
fc6a50b13f | ||
|
|
8415abec6e | ||
|
|
ea9ca67d1e | ||
|
|
8201506c5f | ||
|
|
5fc879b0ef | ||
|
|
bfde847045 | ||
|
|
d96e3f4f9f | ||
|
|
0593a9fb30 | ||
|
|
c03cca21e8 | ||
|
|
170b1b0dcc | ||
|
|
cb9c7966d9 | ||
|
|
b9c2222951 | ||
|
|
dfef5ca26c | ||
|
|
bce60a8657 | ||
|
|
2b244bb4f4 | ||
|
|
31b08eb545 | ||
|
|
9b7817a57f | ||
|
|
bb0f3d5962 | ||
|
|
23c78d5801 | ||
|
|
cd3236976f | ||
|
|
e6096bf9ed | ||
|
|
a502603915 | ||
|
|
7566565f55 | ||
|
|
f56f0b5366 | ||
|
|
34870b4625 | ||
|
|
3ee592a989 | ||
|
|
d6d7e05b51 | ||
|
|
07eaec2ded | ||
|
|
296a7e6023 | ||
|
|
54827edd7e | ||
|
|
d87fb81cf3 | ||
|
|
99a7c47798 | ||
|
|
080a02589c | ||
|
|
d1e7549da9 | ||
|
|
8e1ef1dc77 | ||
|
|
619a2f9d80 | ||
|
|
926444767b | ||
|
|
6bf4020ad1 | ||
|
|
cb63ad8ed2 | ||
|
|
458ed9ad95 | ||
|
|
a11df1a237 | ||
|
|
352d4b9ab9 | ||
|
|
275ded0658 | ||
|
|
c8f3350761 | ||
|
|
e3112d8dcf | ||
|
|
5009900c0e | ||
|
|
0f60f1a58b | ||
|
|
8f88518187 | ||
|
|
05bcead7d1 | ||
|
|
6268989306 | ||
|
|
43ba835b52 | ||
|
|
9240bc9130 | ||
|
|
fb70aee055 | ||
|
|
7bf69eb77d | ||
|
|
7ded9a23be | ||
|
|
281af15d65 | ||
|
|
ec31c96120 | ||
|
|
b970eb1541 | ||
|
|
7f6b90d5f4 | ||
|
|
d28096ede6 | ||
|
|
12b2b0d0eb | ||
|
|
a0e281fb30 | ||
|
|
37e8c3ab84 | ||
|
|
16cb564a6a | ||
|
|
cd88657bc9 | ||
|
|
d82a32b06a | ||
|
|
094bd943ee | ||
|
|
1564de346d | ||
|
|
a5268af269 | ||
|
|
565d9e093c | ||
|
|
cb75606b05 | ||
|
|
7c36294359 | ||
|
|
ad084389cc | ||
|
|
aa8c65a00a | ||
|
|
95ec31e238 | ||
|
|
b5f3a0fa98 | ||
|
|
36bf0d7fb9 | ||
|
|
1719887ac4 | ||
|
|
7f149bf3b1 | ||
|
|
f592e6655f | ||
|
|
46399c7b13 | ||
|
|
0ec66f0f2d | ||
|
|
96c8b64e26 | ||
|
|
bd0b7a8286 | ||
|
|
263b3ec476 | ||
|
|
46075130ab | ||
|
|
0f314bc4e3 | ||
|
|
2b45e7d77c | ||
|
|
987c3e5fe2 | ||
|
|
1e1580d58a | ||
|
|
da9667e1f7 | ||
|
|
9e42fdd786 | ||
|
|
de19abbc5e | ||
|
|
5448a99177 | ||
|
|
f93d7b0193 | ||
|
|
d3bda1b5d0 | ||
|
|
1c49e78b13 | ||
|
|
9d373d4091 | ||
|
|
26edfc7bb9 | ||
|
|
b527c9b160 | ||
|
|
0356b8e2d2 | ||
|
|
1bf093d1b9 | ||
|
|
a78d127702 | ||
|
|
028c0725c3 | ||
|
|
424d35bf42 | ||
|
|
1effdd8482 | ||
|
|
a780b7520f | ||
|
|
233bf36ef6 | ||
|
|
616a45ebc5 | ||
|
|
136f16c9cc | ||
|
|
609628b9cf | ||
|
|
1e0d2e0f2a | ||
|
|
da7e52f85f | ||
|
|
4640ce82f6 | ||
|
|
03ff9e0521 | ||
|
|
f566211d82 | ||
|
|
66ddea2031 | ||
|
|
8b5a9e1639 | ||
|
|
9a183d2ef1 | ||
|
|
807f77755e | ||
|
|
c0a851e045 | ||
|
|
96849afcba | ||
|
|
91e71091ef | ||
|
|
f691ebd146 | ||
|
|
f620c431a1 | ||
|
|
019743b245 | ||
|
|
1d8e3de136 | ||
|
|
0f46f6a1c1 | ||
|
|
b5078c1f1f | ||
|
|
88f9114dd6 | ||
|
|
cfcc1b3162 | ||
|
|
ed2fababff | ||
|
|
74210245cf | ||
|
|
a4ec058a81 | ||
|
|
ae2c15fe8c | ||
|
|
d9e44e0f91 | ||
|
|
3e1f29af48 | ||
|
|
7ac3f6d8ef | ||
|
|
e0f2b9c1ab | ||
|
|
3785e7066e | ||
|
|
157f6b45e9 | ||
|
|
c8dbc18179 | ||
|
|
1d5d86ef6c | ||
|
|
f1746978b2 | ||
|
|
e1f4dae832 | ||
|
|
94e7f6a1f4 | ||
|
|
d3ea923da7 | ||
|
|
dc27dd5d7e | ||
|
|
26f76acdd2 | ||
|
|
719a32e4a5 | ||
|
|
cdf004cdec | ||
|
|
9d5f495842 | ||
|
|
8204fa8ee7 | ||
|
|
9f9dd25f91 | ||
|
|
bd929bfa3f | ||
|
|
5916695d30 | ||
|
|
cca42dca49 | ||
|
|
33065c0ed3 | ||
|
|
603eddf878 | ||
|
|
cda26ab248 | ||
|
|
e73d59d538 | ||
|
|
893fac348d | ||
|
|
642b28bad9 | ||
|
|
b9db14ad44 | ||
|
|
0c23ead398 | ||
|
|
e9009dcb9c | ||
|
|
0d0e64f930 | ||
|
|
b1bdf6516e | ||
|
|
eca4f89d8b | ||
|
|
199cbcf33f | ||
|
|
1f4c7fcb0f | ||
|
|
59b8c781e3 | ||
|
|
9133a3f50b | ||
|
|
b9f4ba6527 | ||
|
|
7b5fe82de9 | ||
|
|
d74db85b82 | ||
|
|
915bbdf247 | ||
|
|
b3412b42b7 | ||
|
|
c6b917fa83 | ||
|
|
c286afa45f | ||
|
|
c74faa3340 | ||
|
|
fa4e8a1ea6 | ||
|
|
42c664a379 | ||
|
|
b84a955ee1 | ||
|
|
211ca3111f | ||
|
|
935e2a976e | ||
|
|
5d38b4dfa0 | ||
|
|
cbf852828c | ||
|
|
7d8b366a73 | ||
|
|
4ad457d568 | ||
|
|
2ab0cb2787 | ||
|
|
4feab27c59 | ||
|
|
3c1449c898 | ||
|
|
7d029c5305 | ||
|
|
13274c01d7 | ||
|
|
f37229c202 | ||
|
|
61f9ff6892 | ||
|
|
3df08b99b3 | ||
|
|
f9748cb723 | ||
|
|
437f637be9 | ||
|
|
7d2b86c548 | ||
|
|
c10ff68ee7 | ||
|
|
b2a8dd13f6 | ||
|
|
fb8ab132e9 | ||
|
|
7ce2e45719 | ||
|
|
c6d15d2605 | ||
|
|
8d57ea4216 | ||
|
|
fdf1bbd1e2 | ||
|
|
662813b378 | ||
|
|
8709ae7113 | ||
|
|
ac22b71171 | ||
|
|
328d1d6546 | ||
|
|
11277d2ef3 | ||
|
|
54dc6102a6 | ||
|
|
c0c9b50a5f | ||
|
|
e780a6fc7c | ||
|
|
fd89b288e5 | ||
|
|
a05c3a5e94 | ||
|
|
43e91c9275 | ||
|
|
26dd2935d7 | ||
|
|
f653871e3a | ||
|
|
89b91a081c | ||
|
|
246b26079b | ||
|
|
41a9e422d5 | ||
|
|
0704d7f4a4 | ||
|
|
1e66742d02 | ||
|
|
d55f827d93 | ||
|
|
40c68d4d63 | ||
|
|
5c58ddc800 | ||
|
|
94171cbc0e | ||
|
|
cf8549bb28 | ||
|
|
a291d73828 | ||
|
|
a6156ec863 | ||
|
|
be583cc86d | ||
|
|
00c55f5e82 | ||
|
|
c9fd1f5252 | ||
|
|
992d1065ac | ||
|
|
247017d19a | ||
|
|
b9fe05c27f | ||
|
|
b01a2cfd79 | ||
|
|
37f2e7eeda | ||
|
|
4ba6ea9190 | ||
|
|
7c1c061681 | ||
|
|
47e4175c9a | ||
|
|
023fd73b86 | ||
|
|
f7ba72217b | ||
|
|
47b9b1dcd6 | ||
|
|
f3adbfef55 | ||
|
|
bc9225f8ed | ||
|
|
62ac0089c9 | ||
|
|
af059141f0 | ||
|
|
efe73a21d9 | ||
|
|
adad299724 | ||
|
|
15b832eefb | ||
|
|
434b6fdc8f | ||
|
|
99c886a9cb | ||
|
|
7d18215bc9 | ||
|
|
7d7f674d9e | ||
|
|
46861486c9 | ||
|
|
d959cacd15 | ||
|
|
d8d0ce5b45 | ||
|
|
b94a4ef9d6 | ||
|
|
212ed8e428 | ||
|
|
6d14bc2f54 | ||
|
|
9dc10c3756 | ||
|
|
316e7be51e | ||
|
|
14842a6d56 | ||
|
|
ce7f3ffca9 | ||
|
|
4c969420f3 | ||
|
|
36aca1e664 | ||
|
|
3a08f57fb1 | ||
|
|
8f86a8aba6 | ||
|
|
e84e02ff07 | ||
|
|
7ef625c598 | ||
|
|
2819e55a61 | ||
|
|
25d9b08dd5 | ||
|
|
6a760c3019 | ||
|
|
8088a464ad | ||
|
|
e41c83a68c | ||
|
|
2d5b8430bd | ||
|
|
eabbe3e3ed | ||
|
|
c37cdbdc5f | ||
|
|
8a0df559c3 | ||
|
|
dfc1c9c228 | ||
|
|
ad20c1cbf1 | ||
|
|
6f4677b36b | ||
|
|
b3a9274cd3 | ||
|
|
7a7bc3e0db | ||
|
|
425bb54057 | ||
|
|
d82df84cf6 | ||
|
|
7d697f068f | ||
|
|
4499b34b51 | ||
|
|
9c6cc8ecd4 | ||
|
|
668130d443 | ||
|
|
5aba690318 | ||
|
|
7397bf389e | ||
|
|
e1b16e9ae3 | ||
|
|
eb50f6fd8f | ||
|
|
5d6dc16a19 | ||
|
|
64dc76d616 | ||
|
|
301664ce93 | ||
|
|
a4534d8f3e | ||
|
|
f161173cb6 | ||
|
|
c58f5efcdd | ||
|
|
65edd2ce22 | ||
|
|
0723f3091c | ||
|
|
a95204f02e | ||
|
|
6dd12e111d | ||
|
|
6e741bd800 | ||
|
|
d37e53bd8e | ||
|
|
fd3585697b | ||
|
|
e92e6ec1c6 | ||
|
|
f0c89cbbba | ||
|
|
d20f932316 | ||
|
|
e011ef469f | ||
|
|
384850b452 | ||
|
|
e467aba32f | ||
|
|
56c51b73b0 | ||
|
|
1aa82dd03e | ||
|
|
a3b7c67ef9 | ||
|
|
de0200f352 | ||
|
|
999c267dfe | ||
|
|
c9a6c2948e | ||
|
|
041157a85f | ||
|
|
cd009936e2 | ||
|
|
f43dfd47d0 | ||
|
|
7082467352 | ||
|
|
f05d3581c1 | ||
|
|
02e560cb18 | ||
|
|
b2d38ad9d6 | ||
|
|
2743f1adc9 | ||
|
|
5e875b6d71 | ||
|
|
6ccb5e8dbf | ||
|
|
ad42a3d41c | ||
|
|
b40e0068ab | ||
|
|
f8dbbc1d11 | ||
|
|
951070757c | ||
|
|
c2a93c6c02 | ||
|
|
9dc82963c4 | ||
|
|
148fce42ba | ||
|
|
92f1263314 | ||
|
|
5fad44369c | ||
|
|
eb3d7c9615 | ||
|
|
5437a98539 | ||
|
|
d9b26f31f8 | ||
|
|
9916ec132b | ||
|
|
1f3806a7bd | ||
|
|
55a770f129 | ||
|
|
d742d595b2 | ||
|
|
9f575902b0 | ||
|
|
6e49cf14fa | ||
|
|
4d96abb786 | ||
|
|
b6d9470a4e | ||
|
|
6ef86dce8c | ||
|
|
5a33f4c707 | ||
|
|
119c1cbb35 | ||
|
|
2be2646d95 | ||
|
|
0609174a14 | ||
|
|
227ca0ed41 | ||
|
|
cc90ccece2 | ||
|
|
9ab92165c8 | ||
|
|
e1fc0d08b7 | ||
|
|
21dad2d72e | ||
|
|
87fe3e72a2 | ||
|
|
c92220f1e5 | ||
|
|
eabeb612b4 | ||
|
|
900e61291a | ||
|
|
51d9f1943b | ||
|
|
282b07dffc | ||
|
|
6521415ae0 | ||
|
|
2819be4508 | ||
|
|
a60805a971 | ||
|
|
af21d33165 | ||
|
|
e465ad58ca | ||
|
|
cd56fc6179 | ||
|
|
1ef70dd4e4 | ||
|
|
a460ea5194 | ||
|
|
5ebf7b7992 | ||
|
|
c9b50e7db6 | ||
|
|
b9993796a0 | ||
|
|
4fb2241621 | ||
|
|
fa49dc08a3 | ||
|
|
3d939f18df | ||
|
|
0d38b9aab1 | ||
|
|
28207f45cd | ||
|
|
72a5e9eb05 | ||
|
|
91dc99d8ef | ||
|
|
305a34b26d | ||
|
|
67349bbce6 | ||
|
|
242ba89b95 | ||
|
|
295280f8cf | ||
|
|
eb0f4728c4 | ||
|
|
8017715bc7 | ||
|
|
503c74c488 | ||
|
|
32cdaa8baa | ||
|
|
83176a0293 | ||
|
|
5db3d14b17 | ||
|
|
bc6ae25aa6 | ||
|
|
8e3900fab7 | ||
|
|
51b3d59094 | ||
|
|
3585645580 | ||
|
|
1e3c20d118 | ||
|
|
f83f13af59 | ||
|
|
54d0bc56a4 | ||
|
|
c722673dbd | ||
|
|
88d34a9daf | ||
|
|
e91237ec05 | ||
|
|
430a2bc08d | ||
|
|
3bbe5d33d3 | ||
|
|
674c6a1684 | ||
|
|
f34519e3ff | ||
|
|
f047be816a | ||
|
|
593c20d62b | ||
|
|
a4bfad6138 | ||
|
|
89de3c5f7a | ||
|
|
81fb664ad9 | ||
|
|
b65597b884 | ||
|
|
d5423947f3 | ||
|
|
f80db23642 | ||
|
|
935906e328 | ||
|
|
3ed7f68054 | ||
|
|
c894b75f4b | ||
|
|
ef43d6905f | ||
|
|
ab7ebedcd5 | ||
|
|
8ca19afd87 | ||
|
|
70e1e550e3 | ||
|
|
cb0087c433 | ||
|
|
d867f86c08 | ||
|
|
1891029eed | ||
|
|
157090a122 | ||
|
|
0ddf2aad79 | ||
|
|
4f47a5032c | ||
|
|
ae957ab642 | ||
|
|
b69a7022b5 | ||
|
|
c19bd88a8e | ||
|
|
4967b70e7c | ||
|
|
cdfad8cc67 | ||
|
|
dcaccaca35 | ||
|
|
d954262184 | ||
|
|
208720545e | ||
|
|
2cbadc6946 | ||
|
|
ebfa49ed8e | ||
|
|
e70c3ef939 | ||
|
|
0523f701c2 | ||
|
|
c6b46295bc | ||
|
|
16ddfb2a20 | ||
|
|
ca5ea6032c | ||
|
|
b4eda0e7cd | ||
|
|
9d2a9bbd9c | ||
|
|
ef13c8fe8d | ||
|
|
aa6932f547 | ||
|
|
5d3c132e83 | ||
|
|
56f4f6c977 | ||
|
|
77df068fea | ||
|
|
b9a1bf9f24 | ||
|
|
2b348916f4 | ||
|
|
70bc5ea2ec | ||
|
|
eff9e553a1 | ||
|
|
e1bfca1883 | ||
|
|
190dbab3d0 | ||
|
|
a1895b1f78 | ||
|
|
1091774003 | ||
|
|
ae29eb35fd | ||
|
|
5ca0a89789 | ||
|
|
6c6e836f0d | ||
|
|
c971d34d67 | ||
|
|
1b04615bff | ||
|
|
40eb8dec68 | ||
|
|
2556e98c26 | ||
|
|
2204da25ed | ||
|
|
ab1eb6de6b | ||
|
|
16278a72e9 | ||
|
|
573a1f22dc | ||
|
|
da370a7dd2 | ||
|
|
6567660639 | ||
|
|
984054f00f | ||
|
|
9351edebe0 | ||
|
|
36465f0859 | ||
|
|
ef5cf4bd96 | ||
|
|
27e1aec001 | ||
|
|
aa9ef65375 | ||
|
|
c8af6d3672 | ||
|
|
ffe5370264 | ||
|
|
ef0156c068 | ||
|
|
00883170f3 | ||
|
|
fb72704a33 | ||
|
|
8f9cd72975 | ||
|
|
913e87c4f5 | ||
|
|
3bdf6df70d | ||
|
|
5e973b21ae | ||
|
|
23e2611725 | ||
|
|
f8784afd72 | ||
|
|
46a73c8ef1 | ||
|
|
bcd80b616d | ||
|
|
b5d870d885 | ||
|
|
f9ac77606e | ||
|
|
ce554ed53f | ||
|
|
27d48f85a9 | ||
|
|
059ff14e6d | ||
|
|
8cfb3569fe | ||
|
|
1ac3e9b364 | ||
|
|
22c6a3da4f | ||
|
|
737405d449 | ||
|
|
e805ae7757 | ||
|
|
d5cf0fee64 | ||
|
|
48e2ae6e52 | ||
|
|
351e784733 | ||
|
|
6c64181bf4 | ||
|
|
d64e6a12a7 | ||
|
|
ccddc9a404 | ||
|
|
aee35aa66d | ||
|
|
0cf953b3eb | ||
|
|
eb073ebe87 | ||
|
|
c3fac64280 | ||
|
|
28ef8e7db7 | ||
|
|
e3eda95b35 | ||
|
|
894a68823f | ||
|
|
dc3843087e | ||
|
|
aaae9fe8bc | ||
|
|
3dc9fc52a7 | ||
|
|
2d373ba509 | ||
|
|
f200e79dc8 | ||
|
|
ae9518a006 | ||
|
|
96c24e8e5f | ||
|
|
cc0c1cc320 | ||
|
|
9834c83126 | ||
|
|
4b8cb5a4e4 | ||
|
|
7e2f473e22 | ||
|
|
ee9f858933 | ||
|
|
69f322d34c | ||
|
|
91099188f2 | ||
|
|
1e7209bafa | ||
|
|
a23a356bf6 | ||
|
|
5218e134a9 | ||
|
|
55a87999e4 | ||
|
|
6cd7861b2c | ||
|
|
6dfaeddc0a | ||
|
|
4846549a90 | ||
|
|
942837b4d9 | ||
|
|
259b4aabcc | ||
|
|
6200b6a4b0 | ||
|
|
6ee1413996 | ||
|
|
08fff1700f | ||
|
|
fd5b7976e7 | ||
|
|
e17637d27b | ||
|
|
57c69a7d6c | ||
|
|
bf3a496ea3 | ||
|
|
3e46db9c11 | ||
|
|
50c624e305 | ||
|
|
6d4e64059a | ||
|
|
ec879b12b3 | ||
|
|
5ace8f5570 | ||
|
|
2fe5a6b248 | ||
|
|
19f5251736 | ||
|
|
a87e3d66a6 | ||
|
|
62acdde68f | ||
|
|
e214573cd1 | ||
|
|
8fff1cd817 | ||
|
|
20eb0c4d64 | ||
|
|
ff05292bdb | ||
|
|
326bc88f19 | ||
|
|
f0a7612369 | ||
|
|
515be5de33 | ||
|
|
e1e7354d85 | ||
|
|
2ec6a06204 | ||
|
|
9150def93e | ||
|
|
730f1a6c47 | ||
|
|
7c12d094c5 | ||
|
|
8aceaf0595 | ||
|
|
5a1bdc156d | ||
|
|
aa81426189 | ||
|
|
7a5d39cc94 | ||
|
|
b1b8f202c9 | ||
|
|
fcfda68d88 | ||
|
|
e966e4c7ed | ||
|
|
42455c72f9 | ||
|
|
ef5f49016e | ||
|
|
6417dfb4a1 | ||
|
|
40985da9ca | ||
|
|
e07c26c17d | ||
|
|
9f2cba49ee | ||
|
|
6e3de21f6e | ||
|
|
a6bdbd7cf1 | ||
|
|
b822428d52 | ||
|
|
03a7df1c98 | ||
|
|
08b7290070 | ||
|
|
6188c698e3 | ||
|
|
7d9b60c16a | ||
|
|
df3c5060e9 | ||
|
|
ff702c9d14 | ||
|
|
ea06fe8cf8 | ||
|
|
f8b0d9b180 | ||
|
|
d3a3e7066d | ||
|
|
45e3964285 | ||
|
|
4edf4936c1 | ||
|
|
067be4973d | ||
|
|
6a3442d058 | ||
|
|
0b83307f7d | ||
|
|
f36042f537 | ||
|
|
668a5d6334 | ||
|
|
eb59713b65 | ||
|
|
fbc6187062 | ||
|
|
274ecaa222 | ||
|
|
051196179d | ||
|
|
cddfc4550d | ||
|
|
465bc3b24a | ||
|
|
38087f0b02 | ||
|
|
3237a539f7 | ||
|
|
828d5764d0 | ||
|
|
15fc3742d2 | ||
|
|
8489b1d943 | ||
|
|
682188540c | ||
|
|
8f740d70e0 | ||
|
|
bc80c2200d | ||
|
|
fd524f627e | ||
|
|
59b3b68bde | ||
|
|
943c8eabbf | ||
|
|
b83918c2aa | ||
|
|
5b4db8fd54 | ||
|
|
c8d7ac48ea | ||
|
|
3cd4e64957 | ||
|
|
2f994628c3 | ||
|
|
f68fc02e57 | ||
|
|
0637b9c8f8 | ||
|
|
7001ddc96f | ||
|
|
fe31a64175 | ||
|
|
cd0cc2b501 | ||
|
|
ebcb3c5466 | ||
|
|
a1d0f3948a | ||
|
|
22f5508bea | ||
|
|
63c4f708c3 | ||
|
|
52fd891370 | ||
|
|
fb447a30e4 | ||
|
|
e0c73e26ee | ||
|
|
d5da5bd8aa | ||
|
|
d84302682e | ||
|
|
c963e93b52 | ||
|
|
c96e3ee2f9 | ||
|
|
a78019ccbc | ||
|
|
e014c94446 | ||
|
|
6c747ff8b4 | ||
|
|
ffd9e9d48e | ||
|
|
3328359ba4 | ||
|
|
66f6c1fd46 | ||
|
|
aaa845a0f8 | ||
|
|
f1157895db | ||
|
|
1277cfed64 | ||
|
|
102fa9c0a8 | ||
|
|
7cd57cadb2 | ||
|
|
2a56b7ade2 | ||
|
|
cf8be3ca21 | ||
|
|
715a1b5df5 | ||
|
|
0a374a0e78 | ||
|
|
36996a54a5 | ||
|
|
3f687e4c4a | ||
|
|
315d373d2d | ||
|
|
6ae346df44 | ||
|
|
9582a27dc6 | ||
|
|
6195d72c3c | ||
|
|
3f0309c36a | ||
|
|
08ea7401cc | ||
|
|
f3d7f09476 | ||
|
|
36150d5b09 | ||
|
|
e5deb2ddd7 | ||
|
|
d60aa54903 | ||
|
|
2627cee938 | ||
|
|
e7f47a4485 | ||
|
|
5bfe56c091 | ||
|
|
f08586ee12 | ||
|
|
af17270c52 | ||
|
|
06b4062003 | ||
|
|
6cba6972bc | ||
|
|
f7a23e629c | ||
|
|
e67b37be28 | ||
|
|
1fa7dfe41f | ||
|
|
e781071ebf | ||
|
|
1697c5d5b5 | ||
|
|
091fefb601 | ||
|
|
7939e6c619 | ||
|
|
f8788dc32c | ||
|
|
4db51bff79 | ||
|
|
6f96da74c7 | ||
|
|
f1692763d3 |
@@ -9,7 +9,7 @@ root = true
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_style = tab
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Python
|
||||
@@ -26,4 +26,4 @@ indent_style = tab
|
||||
|
||||
# HTML, CSS, javascript, JSON and YAML
|
||||
[*.{html,css,js,json,yml,yaml}]
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
|
||||
37
.flake8
Normal file
37
.flake8
Normal file
@@ -0,0 +1,37 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
E121,
|
||||
E126,
|
||||
E127,
|
||||
E128,
|
||||
E203,
|
||||
E225,
|
||||
E226,
|
||||
E231,
|
||||
E241,
|
||||
E251,
|
||||
E261,
|
||||
E265,
|
||||
E302,
|
||||
E303,
|
||||
E305,
|
||||
E402,
|
||||
E501,
|
||||
E741,
|
||||
W291,
|
||||
W292,
|
||||
W293,
|
||||
W391,
|
||||
W503,
|
||||
W504,
|
||||
F403,
|
||||
B007,
|
||||
B950,
|
||||
W191,
|
||||
E124, # closing bracket, irritating while writing QB code
|
||||
E131, # continuation line unaligned for hanging indent
|
||||
E123, # closing bracket does not match indentation of opening bracket's line
|
||||
E101, # ensured by use of black
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
74
.github/helper/flake8.conf
vendored
Normal file
74
.github/helper/flake8.conf
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
B001,
|
||||
B007,
|
||||
B009,
|
||||
B010,
|
||||
B950,
|
||||
E101,
|
||||
E111,
|
||||
E114,
|
||||
E116,
|
||||
E117,
|
||||
E121,
|
||||
E122,
|
||||
E123,
|
||||
E124,
|
||||
E125,
|
||||
E126,
|
||||
E127,
|
||||
E128,
|
||||
E131,
|
||||
E201,
|
||||
E202,
|
||||
E203,
|
||||
E211,
|
||||
E221,
|
||||
E222,
|
||||
E223,
|
||||
E224,
|
||||
E225,
|
||||
E226,
|
||||
E228,
|
||||
E231,
|
||||
E241,
|
||||
E242,
|
||||
E251,
|
||||
E261,
|
||||
E262,
|
||||
E265,
|
||||
E266,
|
||||
E271,
|
||||
E272,
|
||||
E273,
|
||||
E274,
|
||||
E301,
|
||||
E302,
|
||||
E303,
|
||||
E305,
|
||||
E306,
|
||||
E402,
|
||||
E501,
|
||||
E502,
|
||||
E701,
|
||||
E702,
|
||||
E703,
|
||||
E741,
|
||||
F401,
|
||||
F403,
|
||||
F405,
|
||||
W191,
|
||||
W291,
|
||||
W292,
|
||||
W293,
|
||||
W391,
|
||||
W503,
|
||||
W504,
|
||||
E711,
|
||||
E129,
|
||||
F841,
|
||||
E713,
|
||||
E712,
|
||||
|
||||
|
||||
max-line-length = 200
|
||||
46
.github/helper/install.sh
vendored
Normal file
46
.github/helper/install.sh
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd ~ || exit
|
||||
|
||||
echo "Setting Up Bench..."
|
||||
|
||||
pip install frappe-bench
|
||||
bench -v init frappe-bench --skip-assets --python "$(which python)"
|
||||
cd ./frappe-bench || exit
|
||||
|
||||
bench -v setup requirements
|
||||
|
||||
echo "Setting Up LMS App..."
|
||||
bench get-app lms "${GITHUB_WORKSPACE}"
|
||||
|
||||
echo "Setting Up Sites & Database..."
|
||||
|
||||
mkdir ~/frappe-bench/sites/lms.test
|
||||
cp "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/lms.test/site_config.json
|
||||
|
||||
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
||||
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE DATABASE test_lms";
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE USER 'test_lms'@'localhost' IDENTIFIED BY 'test_lms'";
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "GRANT ALL PRIVILEGES ON \`test_lms\`.* TO 'test_lms'@'localhost'";
|
||||
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "FLUSH PRIVILEGES";
|
||||
|
||||
echo "Setting Up Procfile..."
|
||||
|
||||
sed -i 's/^watch:/# watch:/g' Procfile
|
||||
sed -i 's/^schedule:/# schedule:/g' Procfile
|
||||
|
||||
echo "Starting Bench..."
|
||||
|
||||
bench start &> bench_start.log &
|
||||
|
||||
CI=Yes bench build &
|
||||
build_pid=$!
|
||||
|
||||
bench --site lms.test reinstall --yes
|
||||
bench --site lms.test install-app lms
|
||||
|
||||
wait $build_pid
|
||||
14
.github/helper/install_dependencies.sh
vendored
Normal file
14
.github/helper/install_dependencies.sh
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Setting Up System Dependencies..."
|
||||
|
||||
sudo apt update
|
||||
sudo apt remove mysql-server mysql-client
|
||||
sudo apt install libcups2-dev redis-server mariadb-client-10.6
|
||||
|
||||
install_wkhtmltopdf() {
|
||||
wget -q https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb
|
||||
sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb
|
||||
}
|
||||
install_wkhtmltopdf &
|
||||
20
.github/helper/site_config.json
vendored
Normal file
20
.github/helper/site_config.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_host": "127.0.0.1",
|
||||
"db_port": 3306,
|
||||
"db_name": "test_lms",
|
||||
"db_password": "test_lms",
|
||||
"allow_tests": true,
|
||||
"enable_ui_tests": true,
|
||||
"db_type": "mariadb",
|
||||
"auto_email_id": "test@example.com",
|
||||
"mail_server": "smtp.example.com",
|
||||
"mail_login": "test@example.com",
|
||||
"mail_password": "test",
|
||||
"admin_password": "admin",
|
||||
"root_login": "root",
|
||||
"root_password": "123",
|
||||
"host_name": "http://lms.test:8000",
|
||||
"monitor": 1,
|
||||
"server_script_enabled": true,
|
||||
"mute_emails": true
|
||||
}
|
||||
32
.github/try-on-f-cloud.svg
vendored
Normal file
32
.github/try-on-f-cloud.svg
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="4 2 193 52">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
|
||||
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
|
||||
<path d="M41.6982 35.5H45.0129V28.7109C45.0129 27.2344 46.0866 26.2188 47.5494 26.2188C48.0085 26.2188 48.6388 26.2969 48.95 26.3984V23.4453C48.6543 23.375 48.2419 23.3281 47.9074 23.3281C46.5691 23.3281 45.472 24.1094 45.0362 25.5938H44.9117V23.5H41.6982V35.5Z" fill="white"/>
|
||||
<path d="M52.8331 40C55.2996 40 56.6068 38.7344 57.2837 36.7969L61.9289 23.5156L58.4197 23.5L55.9221 32.3125H55.7976L53.3233 23.5H49.8374L54.1247 35.8437L53.9302 36.3516C53.4944 37.4766 52.6619 37.5312 51.4947 37.1719L50.7478 39.6562C51.2224 39.8594 51.9927 40 52.8331 40Z" fill="white"/>
|
||||
<path d="M73.6142 35.7344C77.2401 35.7344 79.4966 33.2422 79.4966 29.5469C79.4966 25.8281 77.2401 23.3438 73.6142 23.3438C69.9883 23.3438 67.7319 25.8281 67.7319 29.5469C67.7319 33.2422 69.9883 35.7344 73.6142 35.7344ZM73.6298 33.1562C71.9569 33.1562 71.101 31.6171 71.101 29.5233C71.101 27.4296 71.9569 25.8827 73.6298 25.8827C75.2715 25.8827 76.1274 27.4296 76.1274 29.5233C76.1274 31.6171 75.2715 33.1562 73.6298 33.1562Z" fill="white"/>
|
||||
<path d="M84.7253 28.5625C84.7331 27.0156 85.6512 26.1094 86.9895 26.1094C88.3201 26.1094 89.1215 26.9844 89.1137 28.4531V35.5H92.4284V27.8594C92.4284 25.0625 90.7945 23.3438 88.3046 23.3438C86.5306 23.3438 85.2466 24.2187 84.7097 25.6172H84.5697V23.5H81.4106V35.5H84.7253V28.5625Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.429 19.5H113.429V22.3141H102.429V19.5ZM102.429 35.5V26.6794H112.699V29.4982H105.94V35.5H102.429Z" fill="white"/>
|
||||
<path d="M131.584 24.9625C131.09 21.5057 128.345 19.5 124.785 19.5C120.589 19.5 117.429 22.463 117.429 27.4924C117.429 32.5142 120.55 35.4848 124.785 35.4848C128.604 35.4848 131.137 33.0916 131.584 30.1211L128.651 30.1059C128.282 31.9293 126.745 32.9549 124.824 32.9549C122.22 32.9549 120.354 31.0632 120.354 27.4924C120.354 23.9824 122.204 22.0299 124.832 22.0299C126.784 22.0299 128.314 23.1011 128.651 24.9625H131.584Z" fill="white"/>
|
||||
<path d="M136.409 19.7124H133.571V35.2718H136.409V19.7124Z" fill="white"/>
|
||||
<path d="M144.031 35.5001C147.56 35.5001 149.803 33.0917 149.803 29.483C149.803 25.8667 147.56 23.4507 144.031 23.4507C140.502 23.4507 138.259 25.8667 138.259 29.483C138.259 33.0917 140.502 35.5001 144.031 35.5001ZM144.047 33.2969C142.094 33.2969 141.137 31.6103 141.137 29.4754C141.137 27.3406 142.094 25.6312 144.047 25.6312C145.968 25.6312 146.925 27.3406 146.925 29.4754C146.925 31.6103 145.968 33.2969 144.047 33.2969Z" fill="white"/>
|
||||
<path d="M159.338 30.3641C159.338 32.1419 158.028 33.0232 156.773 33.0232C155.409 33.0232 154.499 32.0887 154.499 30.6072V23.6025H151.66V31.0327C151.66 33.8361 153.307 35.4239 155.675 35.4239C157.479 35.4239 158.749 34.5046 159.298 33.1979H159.424V35.272H162.176V23.6025H159.338V30.3641Z" fill="white"/>
|
||||
<path d="M169.014 35.4769C171.084 35.4769 172.017 34.2841 172.464 33.4332H172.637V35.2718H175.429V19.7124H172.582V25.532H172.464C172.033 24.6887 171.147 23.4503 169.022 23.4503C166.238 23.4503 164.05 25.5624 164.05 29.4522C164.05 33.2965 166.175 35.4769 169.014 35.4769ZM169.806 33.2205C167.931 33.2205 166.943 31.6251 166.943 29.437C166.943 27.2642 167.916 25.7067 169.806 25.7067C171.633 25.7067 172.637 27.173 172.637 29.437C172.637 31.701 171.617 33.2205 169.806 33.2205Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="0" y="0" width="201" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Run tests
|
||||
name: Server Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -32,11 +32,11 @@ jobs:
|
||||
- name: setup python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
python-version: '3.10'
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '18'
|
||||
check-latest: true
|
||||
- name: setup cache for bench
|
||||
uses: actions/cache@v2
|
||||
@@ -57,15 +57,15 @@ jobs:
|
||||
mkdir -p ~/bench-cache
|
||||
(cd && tar czf ~/bench-cache/bench.tgz frappe-bench)
|
||||
fi
|
||||
- name: add school app to bench
|
||||
- name: add lms app to bench
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: bench get-app school $GITHUB_WORKSPACE
|
||||
run: bench get-app lms $GITHUB_WORKSPACE
|
||||
- name: create bench site
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: bench new-site --mariadb-root-password root --admin-password admin frappe.local
|
||||
- name: install school app
|
||||
- name: install lms app
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: bench --verbose --site frappe.local install-app school
|
||||
run: bench --site frappe.local install-app lms
|
||||
- name: setup requirements
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: bench setup requirements --dev
|
||||
@@ -77,5 +77,4 @@ jobs:
|
||||
run: bench --site frappe.local build
|
||||
- name: run tests
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: bench --site frappe.local run-tests --app school
|
||||
|
||||
run: bench --site frappe.local run-tests --app lms
|
||||
33
.github/workflows/linters.yml
vendored
Normal file
33
.github/workflows/linters.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Linters
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
linters:
|
||||
name: Semantic Commits
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- name: Download semgrep
|
||||
run: pip install semgrep
|
||||
|
||||
- name: Run Semgrep rules
|
||||
run: semgrep ci --config ./frappe-semgrep-rules/rules
|
||||
19
.github/workflows/semantic.yml
vendored
Normal file
19
.github/workflows/semantic.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Semantic Pull Request
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request: {}
|
||||
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
semantic:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: zeke/semantic-pull-requests@main
|
||||
121
.github/workflows/ui-tests.yml
vendored
Normal file
121
.github/workflows/ui-tests.yml
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
name: UI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
# Do not change this as GITHUB_TOKEN is being used by roulette
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'frappe' }}
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: UI Tests (Cypress)
|
||||
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:10.6
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: 123
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
python -m compileall -q -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
run: |
|
||||
echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-ui-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-ui-
|
||||
|
||||
- name: Cache cypress binary
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
|
||||
TYPE: ui
|
||||
DB: mariadb
|
||||
|
||||
- name: Site Setup
|
||||
run: |
|
||||
cd ~/frappe-bench/
|
||||
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
|
||||
bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user
|
||||
|
||||
- name: cypress pre-requisites
|
||||
run: |
|
||||
cd ~/frappe-bench/apps/lms
|
||||
yarn add cypress@^10 --no-lockfile
|
||||
|
||||
- name: UI Tests
|
||||
run: cd ~/frappe-bench/ && bench --site lms.test run-ui-tests lms --headless
|
||||
env:
|
||||
CYPRESS_BASE_URL: http://lms.test:8000
|
||||
CYPRESS_RECORD_KEY: 095366ec-7b9f-41bd-aeec-03bb76d627fe
|
||||
|
||||
- name: Stop server and wait for coverage file
|
||||
run: |
|
||||
ps -ef | grep "[f]rappe serve" | awk '{print $2}' | xargs kill -s SIGINT
|
||||
sleep 5
|
||||
|
||||
- name: Show bench output
|
||||
if: ${{ always() }}
|
||||
run: cat ~/frappe-bench/bench_start.log || true
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -3,8 +3,12 @@
|
||||
*.egg-info
|
||||
*.swp
|
||||
tags
|
||||
school/docs/current
|
||||
school/public/dist
|
||||
lms/docs/current
|
||||
lms/public/dist
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
node_modules
|
||||
package-lock.json
|
||||
lms/public/frontend
|
||||
lms/www/lms.html
|
||||
57
.pre-commit-config.yaml
Normal file
57
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
exclude: 'node_modules|.git'
|
||||
default_stages: [commit]
|
||||
fail_fast: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: "lms.*"
|
||||
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
|
||||
- id: check-yaml
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.34.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ['--py310-plus']
|
||||
|
||||
- repo: https://github.com/adityahase/black
|
||||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
|
||||
hooks:
|
||||
- id: black
|
||||
additional_dependencies: ['click==8.0.4']
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.7.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, vue]
|
||||
# Ignore any files that might contain jinja / bundles
|
||||
exclude: |
|
||||
(?x)^(
|
||||
lms/public/dist/.*|
|
||||
.*node_modules.*|
|
||||
.*boilerplate.*|
|
||||
lms/www/website_script.js|
|
||||
lms/templates/includes/.*|
|
||||
lms/public/js/lib/.*
|
||||
)$
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: ['flake8-bugbear',]
|
||||
args: ['--config', '.github/helper/flake8.conf']
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
skip: []
|
||||
submodules: false
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
1. Go to the apps/school directory of your installation and execute git pull --unshallow to ensure that you have the full git repository. Also fork the frappe/school repository on GitHub.
|
||||
1. Go to the apps/lms directory of your installation and execute git pull --unshallow to ensure that you have the full git repository. Also fork the frappe/lms repository on GitHub.
|
||||
1. Check out a working branch in git (e.g. git checkout -b my-new-branch).
|
||||
1. Make your proposed changes to the source
|
||||
1. Run your local version (e.g. bench start in your bench installation). Make sure that your changes work the way you want them to.
|
||||
|
||||
24
MANIFEST.in
24
MANIFEST.in
@@ -4,15 +4,15 @@ include *.json
|
||||
include *.md
|
||||
include *.py
|
||||
include *.txt
|
||||
recursive-include school *.css
|
||||
recursive-include school *.csv
|
||||
recursive-include school *.html
|
||||
recursive-include school *.ico
|
||||
recursive-include school *.js
|
||||
recursive-include school *.json
|
||||
recursive-include school *.md
|
||||
recursive-include school *.png
|
||||
recursive-include school *.py
|
||||
recursive-include school *.svg
|
||||
recursive-include school *.txt
|
||||
recursive-exclude school *.pyc
|
||||
recursive-include lms *.css
|
||||
recursive-include lms *.csv
|
||||
recursive-include lms *.html
|
||||
recursive-include lms *.ico
|
||||
recursive-include lms *.js
|
||||
recursive-include lms *.json
|
||||
recursive-include lms *.md
|
||||
recursive-include lms *.png
|
||||
recursive-include lms *.py
|
||||
recursive-include lms *.svg
|
||||
recursive-include lms *.txt
|
||||
recursive-exclude lms *.pyc
|
||||
|
||||
110
README.md
110
README.md
@@ -1,25 +1,109 @@
|
||||
## School
|
||||
<p align="center">
|
||||
<a href="https://www.frappelms.com/">
|
||||
<img src="https://frappe.io/files/lms.png" alt="Frappe LMS" width="50px" height="50px">
|
||||
</a>
|
||||
<p align="center">Easy to use, open source, learning management system.</p>
|
||||
</p>
|
||||
|
||||
Create online courses without much hassle.
|
||||
|
||||

|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.producthunt.com/posts/frappe-lms?utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-frappe-lms" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=396079&theme=dark&period=weekly&topic_id=204" alt="Frappe LMS - Easy to use, 100% open source learning management system | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</p>
|
||||
|
||||
|
||||
<div align="center" style="max-height: 40px;">
|
||||
<a href="https://frappecloud.com/lms/signup">
|
||||
<img src=".github/try-on-f-cloud.svg" height="40">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://dashboard.cypress.io/projects/vandxn/runs">
|
||||
<img alt="cypress" src="https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/vandxn/main&style=flat&logo=cypress">
|
||||
</a>
|
||||
<a href="https://github.com/frappe/lms/blob/main/LICENSE">
|
||||
<img alt="license" src="https://img.shields.io/badge/license-AGPLv3-blue">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<img width="1402" alt="Lesson" src="https://frappelms.com/files/banner.png">
|
||||
|
||||
<details>
|
||||
<summary>Show more screenshots</summary>
|
||||
<img width="1520" alt="ss1" src="https://user-images.githubusercontent.com/31363128/210056046-584bc8aa-d28c-4514-b031-73817012837d.png">
|
||||
<img width="830" alt="ss2" src="https://user-images.githubusercontent.com/31363128/210056097-36849182-6db0-43a2-8c62-5333cd2aedf4.png">
|
||||
<img width="941" alt="ss3" src="https://user-images.githubusercontent.com/31363128/210056134-01a7c429-1ef4-434e-9d43-128dda35d7e5.png">
|
||||
</details>
|
||||
|
||||
Frappe LMS is an easy-to-use, open-source learning management system. You can use it to create and share online courses. The app has a clear UI that helps students focus only on what's important and assists in distraction-free learning.
|
||||
|
||||
You can create courses and lessons through simple forms. Lessons can be in the form of text, videos, quizzes or a combination of all these. You can keep your students engaged with quizzes to help revise and test the concepts learned. Course Instructors and Students can reach out to each other through the discussions section available for each lesson and get queries resolved.
|
||||
|
||||
## Features
|
||||
- Create online courses. 📚
|
||||
- Add detailed descriptions and preview videos to the course. 🎬
|
||||
- Add videos, quizzes, and assignments to your lessons and make them interesting and interactive 📝
|
||||
- Discussions section below each lesson where instructors and students can interact with each other. 💬
|
||||
- Create batches to group your students based on courses and track their progress 🏛
|
||||
- Statistics dashboard that provides all important numbers at a glimpse. 📈
|
||||
- Job Board where users can post and look for jobs. 💼
|
||||
- People directory with each person's profile page 👨👩👧👦
|
||||
- Set cover image, profile photo, short bio, and other professional information. 🦹🏼♀️
|
||||
- Simple layout that optimizes readability 🤓
|
||||
- Delightful user experience in overall usage ✨
|
||||
|
||||
1. Simple Backend Forms.
|
||||
1. The UI is clean and minimal.
|
||||
1. Lessons can be in the form of texts, videos, quizzes or a combination of all of these.
|
||||
## Tech Stack
|
||||
|
||||
## Development Setup
|
||||
Frappe LMS is built on [Frappe Framework](https://frappeframework.com) which is a batteries-included python web framework.
|
||||
These are some of the tools it's built on:
|
||||
- [Python](https://www.python.org)
|
||||
- [Redis](https://redis.io/)
|
||||
- [MariaDB](https://mariadb.org/)
|
||||
- [Socket.io](https://socket.io/)
|
||||
|
||||
1. [Through Docker](docker-installation.md)
|
||||
1. [Direct install through bench](bench-installation.md)
|
||||
## Local Setup
|
||||
|
||||
### Docker
|
||||
You need Docker, docker-compose, and git setup on your machine. Refer to [Docker documentation](https://docs.docker.com/). After that, run the following commands:
|
||||
```
|
||||
git clone https://github.com/frappe/lms
|
||||
cd apps/lms/docker
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Wait for some time until the setup script creates a site. After that, you can access `http://localhost:8000` in your browser and the app's login screen should show up.
|
||||
|
||||
### Frappe Bench
|
||||
|
||||
Currently, this app depends on the `develop` branch of [frappe](https://github.com/frappe/frappe).
|
||||
|
||||
1. Setup frappe-bench by following [this guide](https://frappeframework.com/docs/v14/user/en/installation)
|
||||
1. In the frappe-bench directory, run `bench start` and keep it running. Open a new terminal session and cd into the `frappe-bench` directory.
|
||||
1. Run the following commands:
|
||||
```sh
|
||||
bench new-site lms.test
|
||||
bench get-app lms
|
||||
bench --site lms.test install-app lms
|
||||
bench --site lms.test add-to-hosts
|
||||
|
||||
1. Now, you can access the site at `http://lms.test:8000`
|
||||
|
||||
|
||||
### Contributing
|
||||
## Deployment
|
||||
Frappe LMS is an app built on top of the Frappe Framework. So, you can follow any deployment guide for hosting a Frappe Framework-based site.
|
||||
|
||||
1. [Contribution Guidelines](Contribution.md)
|
||||
### Managed Hosting
|
||||
Frappe LMS can be deployed in a few clicks on [Frappe Cloud](https://frappecloud.com/marketplace/apps/lms).
|
||||
|
||||
### Self-hosting
|
||||
If you want to self-host, you can follow official [Frappe Bench Installation](https://github.com/frappe/bench#installation) instructions.
|
||||
|
||||
## Bugs and Feature Requests
|
||||
If you find any bugs or have a feature idea for the app, feel free to report them here on [GitHub Issues](https://github.com/frappe/lms/issues). Make sure you share enough information (app screenshots, browser console screenshots, stack traces, etc) for project maintainers.
|
||||
|
||||
## License
|
||||
|
||||
[GNU AFFERO GENERAL PUBLIC LICENSE](license.txt)
|
||||
Distributed under [GNU AFFERO GENERAL PUBLIC LICENSE](license.txt)
|
||||
|
||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
The Frappe team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security).
|
||||
|
||||
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly and will keep you updated throughout the process.
|
||||
@@ -2,9 +2,9 @@ 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 school.test.
|
||||
1. Fork the school app
|
||||
1. In a separate terminal window, create a new site by running bench new-site lms.test.
|
||||
1. Fork the LMS app
|
||||
1. Run bench get-app <url-of-your-form>.
|
||||
1. Run bench --site school.test install-app school.
|
||||
1. Map your site to localhost with the command ```bench --site school.test add-to-hosts```
|
||||
1. Now open the URL http://school.test:8000/ in your browser, you should see the app running.
|
||||
1. Run bench --site lms.test install-app lms.
|
||||
1. Map your site to localhost with the command ```bench --site lms.test add-to-hosts```
|
||||
1. Now open the URL http://lms.test:8000/ in your browser, you should see the app running.
|
||||
|
||||
18
cypress.config.js
Normal file
18
cypress.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const { defineConfig } = require("cypress");
|
||||
|
||||
module.exports = defineConfig({
|
||||
projectId: "vandxn",
|
||||
adminPassword: "admin",
|
||||
testUser: "frappe@example.com",
|
||||
defaultCommandTimeout: 20000,
|
||||
pageLoadTimeout: 15000,
|
||||
video: true,
|
||||
videoUploadOnPasses: false,
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
e2e: {
|
||||
baseUrl: "http://pyp:8000",
|
||||
},
|
||||
});
|
||||
133
cypress/e2e/course_creation.cy.js
Normal file
133
cypress/e2e/course_creation.cy.js
Normal file
@@ -0,0 +1,133 @@
|
||||
describe("Course Creation", () => {
|
||||
it("creates a new course", () => {
|
||||
cy.login();
|
||||
cy.visit("/courses");
|
||||
// Create a course
|
||||
cy.get("a.btn").contains("Create a Course").click();
|
||||
cy.wait(1000);
|
||||
cy.url().should("include", "/courses/new-course/edit");
|
||||
cy.get("#title").type("Test Course");
|
||||
cy.get("#intro").type("Test Course Short Introduction");
|
||||
cy.get("#description").type("Test Course Description");
|
||||
cy.get("#video-link").type("-LPmw2Znl2c");
|
||||
cy.get("#tags-input").type("Test");
|
||||
cy.get("#published").check();
|
||||
cy.wait(1000);
|
||||
cy.button("Save").click();
|
||||
|
||||
// Add Chapter
|
||||
cy.wait(1000);
|
||||
cy.link("Course Outline").click();
|
||||
|
||||
cy.wait(1000);
|
||||
cy.get(".edit-header .btn-add-chapter").click();
|
||||
cy.wait(500);
|
||||
cy.get("#chapter-title").type("Test Chapter");
|
||||
cy.get("#chapter-description").type("Test Chapter Description");
|
||||
cy.button("Save").click();
|
||||
|
||||
// Add Lesson
|
||||
cy.wait(1000);
|
||||
cy.link("Add Lesson").click();
|
||||
cy.wait(1000);
|
||||
cy.get("#lesson-title").type("Test Lesson");
|
||||
|
||||
// Content
|
||||
cy.get(".collapse-section.collapsed:first").click();
|
||||
cy.get("#lesson-content .ce-block")
|
||||
.click()
|
||||
.type(
|
||||
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. 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. {enter}"
|
||||
);
|
||||
cy.get("#lesson-content .ce-toolbar__plus").click();
|
||||
cy.get('#lesson-content [data-item-name="youtube"]').click();
|
||||
cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto");
|
||||
cy.button("Insert").click();
|
||||
cy.wait(1000);
|
||||
cy.button("Save").click();
|
||||
|
||||
// View Course
|
||||
cy.wait(1000);
|
||||
cy.visit("/courses");
|
||||
cy.get(".course-card-title:first").contains("Test Course");
|
||||
cy.get(".course-card:first").click();
|
||||
cy.url().should("include", "/courses/test-course");
|
||||
cy.get("#title").contains("Test Course");
|
||||
cy.get(".preview-video").should(
|
||||
"have.attr",
|
||||
"src",
|
||||
"https://www.youtube.com/embed/-LPmw2Znl2c"
|
||||
);
|
||||
cy.get("#intro").contains("Test Course Short Introduction");
|
||||
|
||||
// View Chapter
|
||||
cy.get(".chapter-title-main:first").contains("Test Chapter");
|
||||
cy.get(".chapter-description:first").contains(
|
||||
"Test Chapter Description"
|
||||
);
|
||||
cy.get(".lesson-info:first").contains("Test Lesson");
|
||||
cy.get(".lesson-info:first").click();
|
||||
|
||||
// View Lesson
|
||||
cy.wait(1000);
|
||||
cy.url().should("include", "learn/1.1");
|
||||
cy.get("#title").contains("Test Lesson");
|
||||
cy.get(".lesson-video iframe").should(
|
||||
"have.attr",
|
||||
"src",
|
||||
"https://www.youtube.com/embed/GoDtyItReto"
|
||||
);
|
||||
cy.get(".lesson-content-card").contains(
|
||||
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. 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."
|
||||
);
|
||||
|
||||
// Add Discussion
|
||||
cy.get(".reply").click();
|
||||
cy.wait(500);
|
||||
cy.get(".discussion-modal").should("be.visible");
|
||||
|
||||
// Enter title
|
||||
cy.get(".modal .topic-title")
|
||||
.type("Discussion from tests")
|
||||
.should("have.value", "Discussion from tests");
|
||||
|
||||
// Enter comment
|
||||
cy.get(".modal .discussions-comment").type(
|
||||
"This is a discussion from the cypress ui tests."
|
||||
);
|
||||
|
||||
// Submit
|
||||
cy.get(".modal .submit-discussion").click();
|
||||
cy.wait(2000);
|
||||
|
||||
// Check if discussion is added to page and content is visible
|
||||
cy.get(".sidebar-parent:first .discussion-topic-title").should(
|
||||
"have.text",
|
||||
"Discussion from tests"
|
||||
);
|
||||
cy.get(".sidebar-parent:first .discussion-topic-title").click();
|
||||
cy.get(".discussion-on-page:visible").should("have.class", "show");
|
||||
cy.get(
|
||||
".discussion-on-page:visible .reply-card .reply-text .ql-editor p"
|
||||
).should(
|
||||
"have.text",
|
||||
"This is a discussion from the cypress ui tests."
|
||||
);
|
||||
|
||||
cy.get(".discussion-form:visible .discussions-comment").type(
|
||||
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
|
||||
);
|
||||
|
||||
cy.get(".discussion-form:visible .submit-discussion").click();
|
||||
cy.wait(3000);
|
||||
cy.get(".discussion-on-page:visible").should("have.class", "show");
|
||||
cy.get(".discussion-on-page:visible")
|
||||
.children(".reply-card")
|
||||
.eq(1)
|
||||
.find(".reply-text")
|
||||
.should(
|
||||
"have.text",
|
||||
"This is a discussion from the cypress ui tests. This comment was entered through the commentbox on the page.\n"
|
||||
);
|
||||
});
|
||||
});
|
||||
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
55
cypress/support/commands.js
Normal file
55
cypress/support/commands.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
Cypress.Commands.add("login", (email, password) => {
|
||||
if (!email) {
|
||||
email = Cypress.config("testUser") || "Administrator";
|
||||
}
|
||||
if (!password) {
|
||||
password = Cypress.config("adminPassword");
|
||||
}
|
||||
cy.request({
|
||||
url: "/api/method/login",
|
||||
method: "POST",
|
||||
body: { usr: email, pwd: password },
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("button", (text) => {
|
||||
return cy.get(`button:contains("${text}")`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("link", (text) => {
|
||||
return cy.get(`a:contains("${text}")`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("iconButton", (text) => {
|
||||
return cy.get(`button[aria-label="${text}"]`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("dialog", (selector) => {
|
||||
return cy.get(`[role=dialog] ${selector}`);
|
||||
});
|
||||
20
cypress/support/e2e.js
Normal file
20
cypress/support/e2e.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "./commands";
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
@@ -1,25 +0,0 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb
|
||||
volumes:
|
||||
- mariadb-storage:/var/lib/mysql
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
bench:
|
||||
image: anandology/frappe-bench:2021.10
|
||||
volumes:
|
||||
- .:/opt/frappe-bench/apps/school
|
||||
environment:
|
||||
- FRAPPE_APPS=school
|
||||
- FRAPPE_ALLOW_TESTS=true
|
||||
- FRAPPE_SITE_NAME=frappe.localhost
|
||||
depends_on:
|
||||
- mariadb
|
||||
ports:
|
||||
- 8000:8000
|
||||
- 9000:9000
|
||||
volumes:
|
||||
mariadb-storage: {}
|
||||
@@ -1,9 +1,11 @@
|
||||
**Step 1:** Clone the repo
|
||||
|
||||
```
|
||||
$ git clone https://github.com/frappe/school.git
|
||||
$ git clone https://github.com/frappe/lms.git
|
||||
|
||||
$ cd school
|
||||
$ cd lms
|
||||
|
||||
$ cd docker
|
||||
```
|
||||
|
||||
**Step 2:** Run docker-compose
|
||||
|
||||
32
docker/docker-compose.yml
Normal file
32
docker/docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: "3.7"
|
||||
name: lms
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:10.6
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
- --skip-character-set-client-handshake
|
||||
- --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: 123
|
||||
volumes:
|
||||
- mariadb-data:/var/lib/mysql
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
|
||||
frappe:
|
||||
image: frappe/bench:latest
|
||||
command: bash /workspace/init.sh
|
||||
environment:
|
||||
- SHELL=/bin/bash
|
||||
working_dir: /home/frappe
|
||||
volumes:
|
||||
- .:/workspace
|
||||
ports:
|
||||
- 8000:8000
|
||||
- 9000:9000
|
||||
|
||||
volumes:
|
||||
mariadb-data:
|
||||
41
docker/init.sh
Normal file
41
docker/init.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!bin/bash
|
||||
|
||||
if [ -d "/home/frappe/frappe-bench/apps/frappe" ]; then
|
||||
echo "Bench already exists, skipping init"
|
||||
cd frappe-bench
|
||||
bench start
|
||||
else
|
||||
echo "Creating new bench..."
|
||||
fi
|
||||
|
||||
export PATH="${NVM_DIR}/versions/node/v${NODE_VERSION_DEVELOP}/bin/:${PATH}"
|
||||
|
||||
bench init --skip-redis-config-generation frappe-bench
|
||||
|
||||
cd frappe-bench
|
||||
|
||||
# Use containers instead of localhost
|
||||
bench set-mariadb-host mariadb
|
||||
bench set-redis-cache-host redis:6379
|
||||
bench set-redis-queue-host redis:6379
|
||||
bench set-redis-socketio-host redis:6379
|
||||
|
||||
# Remove redis, watch from Procfile
|
||||
sed -i '/redis/d' ./Procfile
|
||||
sed -i '/watch/d' ./Procfile
|
||||
|
||||
bench get-app lms
|
||||
|
||||
bench new-site lms.localhost \
|
||||
--force \
|
||||
--mariadb-root-password 123 \
|
||||
--admin-password admin \
|
||||
--no-mariadb-socket
|
||||
|
||||
bench --site lms.localhost install-app lms
|
||||
bench --site lms.localhost set-config developer_mode 1
|
||||
bench --site lms.localhost clear-cache
|
||||
bench --site lms.localhost set-config mute_emails 1
|
||||
bench use lms.localhost
|
||||
|
||||
bench start
|
||||
1
frappe-ui
Submodule
1
frappe-ui
Submodule
Submodule frappe-ui added at c5faaae38e
5
frontend/.gitignore
vendored
Normal file
5
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
4
frontend/.prettierrc.json
Normal file
4
frontend/.prettierrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
42
frontend/README.md
Normal file
42
frontend/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Frappe UI Starter
|
||||
|
||||
This template should help get you started developing custom frontend for Frappe
|
||||
apps with Vue 3 and the Frappe UI package.
|
||||
|
||||
This boilerplate sets up Vue 3, Vue Router, TailwindCSS, and Frappe UI out of
|
||||
the box.
|
||||
|
||||
## Usage
|
||||
|
||||
This template is meant to be cloned inside an existing Frappe App. Assuming your
|
||||
apps name is `todo`. Clone this template in the root folder of your app using `degit`.
|
||||
|
||||
```
|
||||
cd apps/todo
|
||||
npx degit netchampfaris/frappe-ui-starter frontend
|
||||
cd frontend
|
||||
yarn
|
||||
yarn dev
|
||||
```
|
||||
|
||||
In a development environment, you need to put the below key-value pair in your `site_config.json` file:
|
||||
|
||||
```
|
||||
"ignore_csrf": 1
|
||||
```
|
||||
|
||||
This will prevent `CSRFToken` errors while using the vite dev server. In production environment, the `csrf_token` is attached to the `window` object in `index.html` for you.
|
||||
|
||||
The Vite dev server will start on the port `8080`. This can be changed from `vite.config.js`.
|
||||
The development server is configured to proxy your frappe app (usually running on port `8000`). If you have a site named `todo.test`, open `http://todo.test:8080` in your browser. If you see a button named "Click to send 'ping' request", congratulations!
|
||||
|
||||
If you notice the browser URL is `/frontend`, this is the base URL where your frontend app will run in production.
|
||||
To change this, open `src/router.js` and change the base URL passed to `createWebHistory`.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Vue 3](https://v3.vuejs.org/guide/introduction.html)
|
||||
- [Vue Router](https://next.router.vuejs.org/guide/)
|
||||
- [Frappe UI](https://github.com/frappe/frappe-ui)
|
||||
- [TailwindCSS](https://tailwindcss.com/docs/utility-first)
|
||||
- [Vite](https://vitejs.dev/guide/)
|
||||
49
frontend/index.html
Normal file
49
frontend/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Frappe Learning</title>
|
||||
<meta name="title" content="{{ meta.title }}" />
|
||||
<meta name="image" content="{{ meta.image }}" />
|
||||
<meta name="description" content="{{ meta.description }}" />
|
||||
<meta name="keywords" content="{{ meta.keywords }}" />
|
||||
<meta property="og:title" content="{{ meta.title }}" />
|
||||
<meta property="og:image" content="{{ meta.image }}" />
|
||||
<meta property="og:description" content="{{ meta.description }}" />
|
||||
<meta name="twitter:title" content="{{ meta.title }}" />
|
||||
<meta name="twitter:image" content="{{ meta.image }}" />
|
||||
<meta name="twitter:description" content="{{ meta.description }}" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="seo-content">
|
||||
<h1>{{ meta.title }}</h1>
|
||||
<p>
|
||||
{{ meta.description }}
|
||||
</p>
|
||||
<p>
|
||||
The content here is just for seo purposes. The actual content will be loaded in a few seconds.
|
||||
</p>
|
||||
<p>
|
||||
Seo checks if a page has more than 300 words. So, here are some more words to make it more than 300 words.
|
||||
Page descriptions are the HTML meta tags that provide a brief summary of a web page.
|
||||
Search engines use meta descriptions to help identify the page's topic - they don't use them to rank the page, but they do use them to determine whether or not to display the page in search results.
|
||||
Meta descriptions are important because they're often the first thing people see when they're deciding which search result to click on.
|
||||
They're also important because they can help improve your click-through rate (CTR) from search results.
|
||||
A good meta description can entice people to click on your page instead of someone else's.
|
||||
</p>
|
||||
<a href="{{ meta.link }}">Know More</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modals"></div>
|
||||
<div id="popovers"></div>
|
||||
|
||||
<script>
|
||||
window.csrf_token = '{{ csrf_token }}'
|
||||
document.getElementById('seo-content').style.display = 'none';
|
||||
</script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
frontend/package.json
Normal file
38
frontend/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "frappe-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"serve": "vite preview",
|
||||
"build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry",
|
||||
"copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/lms.html"
|
||||
},
|
||||
"dependencies": {
|
||||
"@editorjs/checklist": "^1.6.0",
|
||||
"@editorjs/editorjs": "^2.29.0",
|
||||
"@editorjs/embed": "^2.7.0",
|
||||
"@editorjs/header": "^2.8.1",
|
||||
"@editorjs/image": "^2.9.0",
|
||||
"@editorjs/nested-list": "^1.4.2",
|
||||
"@editorjs/paragraph": "^2.11.3",
|
||||
"chart.js": "^4.4.1",
|
||||
"dayjs": "^1.11.6",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-ui": "^0.1.50",
|
||||
"lucide-vue-next": "^0.309.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"pinia": "^2.0.33",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"vue": "^3.2.25",
|
||||
"vue-chartjs": "^5.3.0",
|
||||
"vue-router": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"postcss": "^8.4.5",
|
||||
"vite": "^5.0.11"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
frontend/public/favicon.png
Normal file
BIN
frontend/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 440 B |
25
frontend/src/App.vue
Normal file
25
frontend/src/App.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<router-view />
|
||||
</Layout>
|
||||
<Dialogs />
|
||||
<Toasts />
|
||||
</template>
|
||||
<script setup>
|
||||
import { Toasts } from 'frappe-ui'
|
||||
import { Dialogs } from '@/utils/dialogs'
|
||||
import { computed, defineAsyncComponent } from 'vue'
|
||||
import { useScreenSize } from './utils/composables'
|
||||
import DesktopLayout from './components/DesktopLayout.vue'
|
||||
import MobileLayout from './components/MobileLayout.vue'
|
||||
|
||||
const screenSize = useScreenSize()
|
||||
|
||||
const Layout = computed(() => {
|
||||
if (screenSize.width < 640) {
|
||||
return MobileLayout
|
||||
} else {
|
||||
return DesktopLayout
|
||||
}
|
||||
})
|
||||
</script>
|
||||
BIN
frontend/src/assets/Inter/Inter-Black.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-Black.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Black.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-Black.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-BlackItalic.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-BlackItalic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-BlackItalic.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-BlackItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Bold.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-Bold.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Bold.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-Bold.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-BoldItalic.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-BoldItalic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-BoldItalic.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ExtraBold.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-ExtraBold.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ExtraBold.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ExtraLight.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-ExtraLight.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ExtraLight.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ExtraLightItalic.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-ExtraLightItalic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Italic.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-Italic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Italic.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-Italic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Light.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-Light.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Light.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-Light.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-LightItalic.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-LightItalic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-LightItalic.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-LightItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Medium.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-Medium.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Medium.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-Medium.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-MediumItalic.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-MediumItalic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-MediumItalic.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Regular.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-Regular.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Regular.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-Regular.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-SemiBold.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-SemiBold.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-SemiBold.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-SemiBold.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-SemiBoldItalic.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-SemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Thin.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-Thin.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-Thin.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-Thin.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ThinItalic.woff
Normal file
BIN
frontend/src/assets/Inter/Inter-ThinItalic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-ThinItalic.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-ThinItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-italic.var.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-italic.var.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter-roman.var.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter-roman.var.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/Inter/Inter.var.woff2
Normal file
BIN
frontend/src/assets/Inter/Inter.var.woff2
Normal file
Binary file not shown.
152
frontend/src/assets/Inter/inter.css
Normal file
152
frontend/src/assets/Inter/inter.css
Normal file
@@ -0,0 +1,152 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url("Inter-Thin.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-Thin.woff?v=3.12") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-ThinItalic.woff?v=3.12") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-ExtraLight.woff?v=3.12") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-ExtraLightItalic.woff?v=3.12") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("Inter-Light.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-Light.woff?v=3.12") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-LightItalic.woff?v=3.12") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("Inter-Regular.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-Regular.woff?v=3.12") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("Inter-Italic.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-Italic.woff?v=3.12") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("Inter-Medium.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-Medium.woff?v=3.12") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-MediumItalic.woff?v=3.12") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-SemiBold.woff?v=3.12") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-SemiBoldItalic.woff?v=3.12") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("Inter-Bold.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-Bold.woff?v=3.12") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-BoldItalic.woff?v=3.12") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-ExtraBold.woff?v=3.12") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("Inter-Black.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-Black.woff?v=3.12") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"),
|
||||
url("Inter-BlackItalic.woff?v=3.12") format("woff");
|
||||
}
|
||||
62
frontend/src/components/Annoucements.vue
Normal file
62
frontend/src/components/Annoucements.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div v-if="communications.data?.length">
|
||||
<div v-for="comm in communications.data">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<Avatar :label="comm.sender_full_name" size="lg" />
|
||||
<div class="ml-2">
|
||||
{{ comm.sender_full_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ timeAgo(comm.communication_date) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="prose prose-sm bg-gray-50 !min-w-full px-4 py-2 rounded-md"
|
||||
v-html="comm.content"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm italic text-gray-600">
|
||||
{{ __('No announcements') }}
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createListResource, Avatar } from 'frappe-ui'
|
||||
import { timeAgo } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const communications = createListResource({
|
||||
doctype: 'Communication',
|
||||
fields: [
|
||||
'subject',
|
||||
'content',
|
||||
'recipients',
|
||||
'cc',
|
||||
'communication_date',
|
||||
'sender',
|
||||
'sender_full_name',
|
||||
],
|
||||
filters: {
|
||||
reference_doctype: 'LMS Batch',
|
||||
reference_name: props.batch,
|
||||
},
|
||||
orderBy: 'communication_date desc',
|
||||
auto: true,
|
||||
cache: ['batch', props.batch],
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.prose-sm p {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
</style>
|
||||
55
frontend/src/components/AppSidebar.vue
Normal file
55
frontend/src/components/AppSidebar.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-50"
|
||||
:class="isSidebarCollapsed ? 'w-14' : 'w-56'"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col overflow-hidden"
|
||||
:class="isSidebarCollapsed ? 'items-center' : ''"
|
||||
>
|
||||
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<SidebarLink
|
||||
v-for="link in links"
|
||||
:link="link"
|
||||
:isCollapsed="isSidebarCollapsed"
|
||||
class="mx-2 my-0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarLink
|
||||
:link="{
|
||||
label: isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||
}"
|
||||
:isCollapsed="isSidebarCollapsed"
|
||||
@click="isSidebarCollapsed = !isSidebarCollapsed"
|
||||
class="m-2"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||
<CollapseSidebar
|
||||
class="h-4.5 w-4.5 text-gray-700 duration-300 ease-in-out"
|
||||
:class="{ '[transform:rotateY(180deg)]': isSidebarCollapsed }"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SidebarLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import UserDropdown from '@/components/UserDropdown.vue'
|
||||
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
|
||||
import SidebarLink from '@/components/SidebarLink.vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import { getSidebarLinks } from '../utils'
|
||||
|
||||
const links = getSidebarLinks()
|
||||
|
||||
const getSidebarFromStorage = () => {
|
||||
return useStorage('sidebar_is_collapsed', false)
|
||||
}
|
||||
|
||||
let isSidebarCollapsed = ref(getSidebarFromStorage())
|
||||
</script>
|
||||
98
frontend/src/components/Assessments.vue
Normal file
98
frontend/src/components/Assessments.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Assessments') }}
|
||||
</div>
|
||||
<div v-if="assessments.data?.length">
|
||||
<ListView
|
||||
:columns="getAssessmentColumns()"
|
||||
:rows="assessments.data"
|
||||
row-key="name"
|
||||
:options="{
|
||||
selectable: false,
|
||||
showTooltip: false,
|
||||
getRowRoute: (row) => {
|
||||
if (row.submission) {
|
||||
return {
|
||||
name: 'AssignmentSubmission',
|
||||
params: {
|
||||
assignmentName: row.assessment_name,
|
||||
submissionName: row.submission.name,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
name: 'AssignmentSubmission',
|
||||
params: {
|
||||
assignmentName: row.assessment_name,
|
||||
submissionName: 'new',
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}"
|
||||
>
|
||||
</ListView>
|
||||
</div>
|
||||
<div v-else class="text-sm italic text-gray-600">
|
||||
{{ __('No Assessments') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ListView, createResource } from 'frappe-ui'
|
||||
import { inject } from 'vue'
|
||||
|
||||
const user = inject('$user')
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
selectable: true,
|
||||
totalCount: 0,
|
||||
rowCount: 0,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const assessments = createResource({
|
||||
url: 'lms.lms.utils.get_assessments',
|
||||
params: {
|
||||
batch: props.batch,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const getAssessmentColumns = () => {
|
||||
let columns = [
|
||||
{
|
||||
label: 'Assessment',
|
||||
key: 'title',
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
key: 'assessment_type',
|
||||
},
|
||||
]
|
||||
|
||||
if (!user.data?.is_moderator) {
|
||||
columns.push({
|
||||
label: 'Status/Score',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
})
|
||||
}
|
||||
return columns
|
||||
}
|
||||
</script>
|
||||
75
frontend/src/components/BatchCard.vue
Normal file
75
frontend/src/components/BatchCard.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col border border-gray-200 rounded-md p-4 h-full"
|
||||
style="min-height: 150px"
|
||||
>
|
||||
<Badge
|
||||
v-if="batch.seat_count && batch.seats_left > 0"
|
||||
theme="green"
|
||||
class="self-start mb-2"
|
||||
>
|
||||
{{ batch.seats_left }} {{ __('Seat Left') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="batch.seat_count && batch.seats_left <= 0"
|
||||
theme="red"
|
||||
class="self-start mb-2"
|
||||
>
|
||||
{{ __('Sold Out') }}
|
||||
</Badge>
|
||||
<div class="text-xl font-semibold mb-1">
|
||||
{{ batch.title }}
|
||||
</div>
|
||||
<div class="short-introduction">
|
||||
{{ batch.description }}
|
||||
</div>
|
||||
<div class="mt-auto">
|
||||
<div v-if="batch.amount" class="font-semibold text-lg mb-4">
|
||||
{{ batch.price }}
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
||||
<span> {{ batch.courses.length }} {{ __('Courses') }} </span>
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<Calendar class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
||||
<span>
|
||||
{{ dayjs(batch.start_date).format('DD MMM YYYY') }} -
|
||||
{{ dayjs(batch.end_date).format('DD MMM YYYY') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
||||
<span>
|
||||
{{ formatTime(batch.start_time) }} - {{ formatTime(batch.end_time) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Calendar, Clock, BookOpen } from 'lucide-vue-next'
|
||||
import { inject } from 'vue'
|
||||
import { Badge } from 'frappe-ui'
|
||||
import { formatTime } from '../utils'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.short-introduction {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0.25rem 0 1.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
141
frontend/src/components/BatchCourses.vue
Normal file
141
frontend/src/components/BatchCourses.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="text-xl font-semibold">
|
||||
{{ __('Courses') }}
|
||||
</div>
|
||||
<Button
|
||||
v-if="user.data?.is_moderator"
|
||||
variant="solid"
|
||||
@click="openCourseModal()"
|
||||
>
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4" />
|
||||
</template>
|
||||
{{ __('Add Course') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="courses.data?.length">
|
||||
<ListView
|
||||
:columns="getCoursesColumns()"
|
||||
:rows="courses.data"
|
||||
row-key="name"
|
||||
:options="{ showTooltip: false }"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in getCoursesColumns()">
|
||||
<template #prefix="{ item }">
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="h-4 w-4 stroke-1.5 ml-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow :row="row" v-for="row in courses.data">
|
||||
<template #default="{ column, item }">
|
||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||
<div>
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
<ListSelectBanner>
|
||||
<template #actions="{ unselectAll, selections }">
|
||||
<div class="flex gap-2">
|
||||
<Button variant="ghost" @click="removeCourses(selections)">
|
||||
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
</div>
|
||||
<BatchCourseModal
|
||||
v-model="showCourseModal"
|
||||
:batch="batch"
|
||||
v-model:courses="courses"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue'
|
||||
import BatchCourseModal from '@/components/Modals/BatchCourseModal.vue'
|
||||
import {
|
||||
createResource,
|
||||
Button,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListSelectBanner,
|
||||
ListRow,
|
||||
ListRows,
|
||||
ListView,
|
||||
ListRowItem,
|
||||
} from 'frappe-ui'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
|
||||
const showCourseModal = ref(false)
|
||||
const user = inject('$user')
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const courses = createResource({
|
||||
url: 'lms.lms.utils.get_batch_courses',
|
||||
params: {
|
||||
batch: props.batch,
|
||||
},
|
||||
cache: ['batchCourses', props.batchName],
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const openCourseModal = () => {
|
||||
showCourseModal.value = true
|
||||
}
|
||||
|
||||
const getCoursesColumns = () => {
|
||||
return [
|
||||
{
|
||||
label: 'Title',
|
||||
key: 'title',
|
||||
},
|
||||
{
|
||||
label: 'Lessons',
|
||||
key: 'lesson_count',
|
||||
},
|
||||
{
|
||||
label: 'Enrollments',
|
||||
key: 'enrollment_count',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const removeCourse = createResource({
|
||||
url: 'frappe.client.delete',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'Batch Course',
|
||||
name: values.course,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const removeCourses = (selections) => {
|
||||
console.log(selections)
|
||||
selections.forEach(async (course) => {
|
||||
removeCourse.submit({ course })
|
||||
await setTimeout(1000)
|
||||
})
|
||||
courses.reload()
|
||||
}
|
||||
</script>
|
||||
26
frontend/src/components/BatchDashboard.vue
Normal file
26
frontend/src/components/BatchDashboard.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<UpcomingEvaluations
|
||||
:batch="batch.data.name"
|
||||
:endDate="batch.data.evaluation_end_date"
|
||||
:courses="batch.data.courses"
|
||||
:isStudent="isStudent"
|
||||
/>
|
||||
<Assessments :batch="batch.data.name" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
||||
import Assessments from '@/components/Assessments.vue'
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isStudent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
115
frontend/src/components/BatchOverlay.vue
Normal file
115
frontend/src/components/BatchOverlay.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div v-if="batch.data" class="shadow rounded-md p-5" style="width: 300px">
|
||||
<Badge
|
||||
v-if="batch.data.seat_count && seats_left > 0"
|
||||
theme="green"
|
||||
class="self-start mb-2 float-right"
|
||||
>
|
||||
{{ seats_left }} {{ __('Seat Left') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="batch.data.seat_count && seats_left <= 0"
|
||||
theme="red"
|
||||
class="self-start mb-2 float-right"
|
||||
>
|
||||
{{ __('Sold Out') }}
|
||||
</Badge>
|
||||
<div v-if="batch.data.amount" class="text-lg font-semibold mb-3">
|
||||
{{ formatNumberIntoCurrency(batch.data.amount, batch.data.currency) }}
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
||||
<span> {{ batch.data.courses.length }} {{ __('Courses') }} </span>
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<Calendar class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
||||
<span>
|
||||
{{ dayjs(batch.data.start_date).format('DD MMM YYYY') }} -
|
||||
{{ dayjs(batch.data.end_date).format('DD MMM YYYY') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
||||
<span>
|
||||
{{ formatTime(batch.data.start_time) }} -
|
||||
{{ formatTime(batch.data.end_time) }}
|
||||
</span>
|
||||
</div>
|
||||
<router-link
|
||||
v-if="user?.data?.is_moderator"
|
||||
:to="{
|
||||
name: 'Batch',
|
||||
params: {
|
||||
batchName: batch.data.name,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button variant="solid" class="w-full mt-4">
|
||||
<span>
|
||||
{{ __('Manage Batch') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Billing',
|
||||
params: {
|
||||
type: 'batch',
|
||||
name: batch.data.name,
|
||||
},
|
||||
}"
|
||||
v-else-if="batch.data.paid_batch"
|
||||
>
|
||||
<Button class="w-full mt-4" variant="solid">
|
||||
<span>
|
||||
{{ __('Register Now') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
<Button
|
||||
variant="solid"
|
||||
class="w-full mt-2"
|
||||
v-else-if="batch.data.allow_self_enrollment"
|
||||
>
|
||||
{{ __('Enroll Now') }}
|
||||
</Button>
|
||||
<router-link
|
||||
v-if="user?.data?.is_moderator"
|
||||
:to="{
|
||||
name: 'BatchCreation',
|
||||
params: {
|
||||
batchName: batch.data.name,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button class="w-full mt-2">
|
||||
<span>
|
||||
{{ __('Edit') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { formatNumberIntoCurrency, formatTime } from '@/utils'
|
||||
import { BookOpen, Calendar, Clock } from 'lucide-vue-next'
|
||||
import { inject, computed } from 'vue'
|
||||
import { Badge, Button } from 'frappe-ui'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const user = inject('$user')
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const seats_left = computed(() => {
|
||||
if (props.batch.data?.seat_count) {
|
||||
return props.batch.data?.seat_count - props.batch.data?.students?.length
|
||||
}
|
||||
return null
|
||||
})
|
||||
</script>
|
||||
151
frontend/src/components/BatchStudents.vue
Normal file
151
frontend/src/components/BatchStudents.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<Button class="float-right mb-3" variant="solid" @click="openStudentModal()">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4" />
|
||||
</template>
|
||||
{{ __('Add Student') }}
|
||||
</Button>
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Students') }}
|
||||
</div>
|
||||
<div v-if="students.data?.length">
|
||||
<ListView
|
||||
:columns="getStudentColumns()"
|
||||
:rows="students.data"
|
||||
row-key="name"
|
||||
:options="{ showTooltip: false }"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in getStudentColumns()">
|
||||
<template #prefix="{ item }">
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="h-4 w-4 stroke-1.5 ml-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow :row="row" v-for="row in students.data">
|
||||
<template #default="{ column, item }">
|
||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||
<template #prefix>
|
||||
<div v-if="column.key == 'full_name'">
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="row['user_image']"
|
||||
:label="item"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
<ListSelectBanner>
|
||||
<template #actions="{ unselectAll, selections }">
|
||||
<div class="flex gap-2">
|
||||
<Button variant="ghost" @click="removeStudents(selections)">
|
||||
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
</div>
|
||||
<div v-else class="text-sm italic text-gray-600">
|
||||
{{ __('There are no students in this batch.') }}
|
||||
</div>
|
||||
<StudentModal
|
||||
:batch="props.batch"
|
||||
v-model="showStudentModal"
|
||||
v-model:reloadStudents="students"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
createResource,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListSelectBanner,
|
||||
ListRow,
|
||||
ListRows,
|
||||
ListView,
|
||||
ListRowItem,
|
||||
Avatar,
|
||||
Button,
|
||||
} from 'frappe-ui'
|
||||
import { Trash2, Plus } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import StudentModal from '@/components/Modals/StudentModal.vue'
|
||||
|
||||
const showStudentModal = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const students = createResource({
|
||||
url: 'lms.lms.utils.get_batch_students',
|
||||
cache: ['students', props.batch],
|
||||
params: {
|
||||
batch: props.batch,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const getStudentColumns = () => {
|
||||
return [
|
||||
{
|
||||
label: 'Full Name',
|
||||
key: 'full_name',
|
||||
},
|
||||
{
|
||||
label: 'Courses Done',
|
||||
key: 'courses_completed',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: 'Assessments Done',
|
||||
key: 'assessments_completed',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: 'Last Active',
|
||||
key: 'last_active',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const openStudentModal = () => {
|
||||
showStudentModal.value = true
|
||||
}
|
||||
|
||||
const removeStudent = createResource({
|
||||
url: 'frappe.client.delete',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'Batch Student',
|
||||
name: values.student,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const removeStudents = (selections) => {
|
||||
selections.forEach(async (student) => {
|
||||
removeStudent.submit({ student })
|
||||
await setTimeout(1000)
|
||||
})
|
||||
students.reload()
|
||||
}
|
||||
</script>
|
||||
277
frontend/src/components/Controls/Autocomplete.vue
Normal file
277
frontend/src/components/Controls/Autocomplete.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
|
||||
<Popover class="w-full" v-model:show="showOptions">
|
||||
<template #target="{ open: openPopover, togglePopover }">
|
||||
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
|
||||
<div class="w-full">
|
||||
<button
|
||||
class="flex w-full items-center justify-between focus:outline-none"
|
||||
:class="inputClasses"
|
||||
@click="() => togglePopover()"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<slot name="prefix" />
|
||||
<span
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
|
||||
v-if="selectedValue"
|
||||
>
|
||||
{{ displayValue(selectedValue) }}
|
||||
</span>
|
||||
<span class="text-base leading-5 text-gray-500" v-else>
|
||||
{{ placeholder || '' }}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown class="h-4 w-4 stroke-1.5" />
|
||||
</button>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<template #body="{ isOpen }">
|
||||
<div v-show="isOpen">
|
||||
<div class="mt-1 rounded-lg bg-white py-1 text-base shadow-2xl">
|
||||
<div class="relative px-1.5 pt-0.5">
|
||||
<ComboboxInput
|
||||
ref="search"
|
||||
class="form-input w-full"
|
||||
type="text"
|
||||
@change="
|
||||
(e) => {
|
||||
query = e.target.value
|
||||
}
|
||||
"
|
||||
:value="query"
|
||||
autocomplete="off"
|
||||
placeholder="Search"
|
||||
/>
|
||||
<button
|
||||
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
|
||||
@click="selectedValue = null"
|
||||
>
|
||||
<X class="h-4 w-4 stroke-1.5" />
|
||||
</button>
|
||||
</div>
|
||||
<ComboboxOptions
|
||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
||||
static
|
||||
>
|
||||
<div
|
||||
class="mt-1.5"
|
||||
v-for="group in groups"
|
||||
:key="group.key"
|
||||
v-show="group.items.length > 0"
|
||||
>
|
||||
<div
|
||||
v-if="group.group && !group.hideLabel"
|
||||
class="px-2.5 py-1.5 text-sm font-medium text-gray-500"
|
||||
>
|
||||
{{ group.group }}
|
||||
</div>
|
||||
<ComboboxOption
|
||||
as="template"
|
||||
v-for="option in group.items"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'flex items-center rounded px-2.5 py-1.5 text-base',
|
||||
{ 'bg-gray-100': active },
|
||||
]"
|
||||
>
|
||||
<slot
|
||||
name="item-prefix"
|
||||
v-bind="{ active, selected, option }"
|
||||
/>
|
||||
<slot
|
||||
name="item-label"
|
||||
v-bind="{ active, selected, option }"
|
||||
>
|
||||
{{ option.label }}
|
||||
</slot>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</div>
|
||||
<li
|
||||
v-if="groups.length == 0"
|
||||
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-gray-600"
|
||||
>
|
||||
No results found
|
||||
</li>
|
||||
</ComboboxOptions>
|
||||
<div v-if="slots.footer" class="border-t p-1.5 pb-0.5">
|
||||
<slot
|
||||
name="footer"
|
||||
v-bind="{ value: search?.el._value, close }"
|
||||
></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
} from '@headlessui/vue'
|
||||
import { Popover, Button } from 'frappe-ui'
|
||||
import { ChevronDown, X } from 'lucide-vue-next'
|
||||
import { ref, computed, useAttrs, useSlots, watch, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'subtle',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'update:query', 'change'])
|
||||
|
||||
const query = ref('')
|
||||
const showOptions = ref(false)
|
||||
const search = ref(null)
|
||||
|
||||
const attrs = useAttrs()
|
||||
const slots = useSlots()
|
||||
|
||||
const valuePropPassed = computed(() => 'value' in attrs)
|
||||
|
||||
const selectedValue = computed({
|
||||
get() {
|
||||
return valuePropPassed.value ? attrs.value : props.modelValue
|
||||
},
|
||||
set(val) {
|
||||
query.value = ''
|
||||
if (val) {
|
||||
showOptions.value = false
|
||||
}
|
||||
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val)
|
||||
},
|
||||
})
|
||||
|
||||
function close() {
|
||||
showOptions.value = false
|
||||
}
|
||||
|
||||
const groups = computed(() => {
|
||||
if (!props.options || props.options.length == 0) return []
|
||||
|
||||
let groups = props.options[0]?.group
|
||||
? props.options
|
||||
: [{ group: '', items: props.options }]
|
||||
|
||||
return groups
|
||||
.map((group, i) => {
|
||||
return {
|
||||
key: i,
|
||||
group: group.group,
|
||||
hideLabel: group.hideLabel || false,
|
||||
items: props.filterable ? filterOptions(group.items) : group.items,
|
||||
}
|
||||
})
|
||||
.filter((group) => group.items.length > 0)
|
||||
})
|
||||
|
||||
function filterOptions(options) {
|
||||
if (!query.value) {
|
||||
return options
|
||||
}
|
||||
return options.filter((option) => {
|
||||
let searchTexts = [option.label, option.value]
|
||||
return searchTexts.some((text) =>
|
||||
(text || '').toString().toLowerCase().includes(query.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function displayValue(option) {
|
||||
if (typeof option === 'string') {
|
||||
let allOptions = groups.value.flatMap((group) => group.items)
|
||||
let selectedOption = allOptions.find((o) => o.value === option)
|
||||
return selectedOption?.label || option
|
||||
}
|
||||
return option?.label
|
||||
}
|
||||
|
||||
watch(query, (q) => {
|
||||
emit('update:query', q)
|
||||
})
|
||||
|
||||
watch(showOptions, (val) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
search.value.el.focus()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const textColor = computed(() => {
|
||||
return props.disabled ? 'text-gray-600' : 'text-gray-800'
|
||||
})
|
||||
|
||||
const inputClasses = computed(() => {
|
||||
let sizeClasses = {
|
||||
sm: 'text-base rounded h-7',
|
||||
md: 'text-base rounded h-8',
|
||||
lg: 'text-lg rounded-md h-10',
|
||||
xl: 'text-xl rounded-md h-10',
|
||||
}[props.size]
|
||||
|
||||
let paddingClasses = {
|
||||
sm: 'py-1.5 px-2',
|
||||
md: 'py-1.5 px-2.5',
|
||||
lg: 'py-1.5 px-3',
|
||||
xl: 'py-1.5 px-3',
|
||||
}[props.size]
|
||||
|
||||
let variant = props.disabled ? 'disabled' : props.variant
|
||||
let variantClasses = {
|
||||
subtle:
|
||||
'border border-gray-100 bg-gray-100 placeholder-gray-500 hover:border-gray-200 hover:bg-gray-200 focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400',
|
||||
outline:
|
||||
'border border-gray-300 bg-white placeholder-gray-500 hover:border-gray-400 hover:shadow-sm focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400',
|
||||
disabled: [
|
||||
'border bg-gray-50 placeholder-gray-400',
|
||||
props.variant === 'outline' ? 'border-gray-300' : 'border-transparent',
|
||||
],
|
||||
}[variant]
|
||||
|
||||
return [
|
||||
sizeClasses,
|
||||
paddingClasses,
|
||||
variantClasses,
|
||||
textColor.value,
|
||||
'transition-colors w-full',
|
||||
]
|
||||
})
|
||||
|
||||
defineExpose({ query })
|
||||
</script>
|
||||
146
frontend/src/components/Controls/Link.vue
Normal file
146
frontend/src/components/Controls/Link.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="space-y-1.5">
|
||||
<label class="block" :class="labelClasses" v-if="attrs.label">
|
||||
{{ attrs.label }}
|
||||
</label>
|
||||
<Autocomplete
|
||||
ref="autocomplete"
|
||||
:options="options.data"
|
||||
v-model="value"
|
||||
:size="attrs.size || 'sm'"
|
||||
:variant="attrs.variant"
|
||||
:placeholder="attrs.placeholder"
|
||||
:filterable="false"
|
||||
>
|
||||
<template #target="{ open, togglePopover }">
|
||||
<slot name="target" v-bind="{ open, togglePopover }" />
|
||||
</template>
|
||||
|
||||
<template #prefix>
|
||||
<slot name="prefix" />
|
||||
</template>
|
||||
|
||||
<template #item-prefix="{ active, selected, option }">
|
||||
<slot name="item-prefix" v-bind="{ active, selected, option }" />
|
||||
</template>
|
||||
|
||||
<template #item-label="{ active, selected, option }">
|
||||
<slot name="item-label" v-bind="{ active, selected, option }" />
|
||||
</template>
|
||||
|
||||
<template v-if="attrs.onCreate" #footer="{ value, close }">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
label="Create New"
|
||||
@click="attrs.onCreate(value, close)"
|
||||
>
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Autocomplete from '@/components/Controls/Autocomplete.vue'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { createResource, Button } from 'frappe-ui'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { useAttrs, computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const valuePropPassed = computed(() => 'value' in attrs)
|
||||
|
||||
const value = computed({
|
||||
get: () => (valuePropPassed.value ? attrs.value : props.modelValue),
|
||||
set: (val) => {
|
||||
return (
|
||||
val?.value &&
|
||||
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val?.value)
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const autocomplete = ref(null)
|
||||
const text = ref('')
|
||||
|
||||
watchDebounced(
|
||||
() => autocomplete.value?.query,
|
||||
(val) => {
|
||||
val = val || ''
|
||||
if (text.value === val) return
|
||||
text.value = val
|
||||
reload(val)
|
||||
},
|
||||
{ debounce: 300, immediate: true }
|
||||
)
|
||||
|
||||
watchDebounced(
|
||||
() => props.doctype,
|
||||
() => reload(''),
|
||||
{ debounce: 300, immediate: true }
|
||||
)
|
||||
|
||||
const options = createResource({
|
||||
url: 'frappe.desk.search.search_link',
|
||||
cache: [props.doctype, text.value],
|
||||
method: 'POST',
|
||||
params: {
|
||||
txt: text.value,
|
||||
doctype: props.doctype,
|
||||
filters: props.filters,
|
||||
},
|
||||
transform: (data) => {
|
||||
return data.map((option) => {
|
||||
return {
|
||||
label: option.value,
|
||||
value: option.value,
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
function reload(val) {
|
||||
options.update({
|
||||
params: {
|
||||
txt: val,
|
||||
doctype: props.doctype,
|
||||
filters: props.filters,
|
||||
},
|
||||
})
|
||||
options.reload()
|
||||
}
|
||||
|
||||
const labelClasses = computed(() => {
|
||||
return [
|
||||
{
|
||||
sm: 'text-xs',
|
||||
md: 'text-base',
|
||||
}[attrs.size || 'sm'],
|
||||
'text-gray-600',
|
||||
]
|
||||
})
|
||||
</script>
|
||||
38
frontend/src/components/Controls/Rating.vue
Normal file
38
frontend/src/components/Controls/Rating.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="flex text-center">
|
||||
<div v-for="index in 5">
|
||||
<Star
|
||||
:class="index <= rating ? 'fill-orange-500' : ''"
|
||||
class="h-6 w-6 fill-gray-400 text-gray-50 mr-1 cursor-pointer"
|
||||
@click="markRating(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
let rating = ref(props.modelValue)
|
||||
|
||||
let emitChange = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
function markRating(index) {
|
||||
emitChange(index)
|
||||
rating.value = index
|
||||
}
|
||||
</script>
|
||||
187
frontend/src/components/CourseCard.vue
Normal file
187
frontend/src/components/CourseCard.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="course.title"
|
||||
class="flex flex-col h-full rounded-md shadow-md text-base overflow-auto"
|
||||
style="min-height: 320px"
|
||||
>
|
||||
<div
|
||||
class="course-image"
|
||||
:class="{ 'default-image': !course.image }"
|
||||
:style="{ backgroundImage: 'url(' + encodeURI(course.image) + ')' }"
|
||||
>
|
||||
<div class="flex relative top-4 left-4 w-fit flex-wrap">
|
||||
<Badge theme="gray" size="md" class="mr-2" v-for="tag in course.tags">
|
||||
{{ tag }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div v-if="!course.image" class="image-placeholder">
|
||||
{{ course.title[0] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col flex-auto p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div v-if="course.lesson_count">
|
||||
<Tooltip :text="__('Lessons')">
|
||||
<span class="flex items-center">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
|
||||
{{ course.lesson_count }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="course.enrollment_count">
|
||||
<Tooltip :text="__('Enrolled Students')">
|
||||
<span class="flex items-center">
|
||||
<Users class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
|
||||
{{ course.enrollment_count }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="course.avg_rating">
|
||||
<Tooltip :text="__('Average Rating')">
|
||||
<span class="flex items-center">
|
||||
<Star class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
|
||||
{{ course.avg_rating }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="course.status != 'Approved'">
|
||||
<Badge
|
||||
variant="solid"
|
||||
:theme="course.status === 'Under Review' ? 'orange' : 'blue'"
|
||||
size="sm"
|
||||
>
|
||||
{{ course.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xl font-semibold leading-6">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
|
||||
<div class="short-introduction">
|
||||
{{ course.short_introduction }}
|
||||
</div>
|
||||
<div
|
||||
v-if="user && course.membership"
|
||||
class="w-full bg-gray-200 rounded-full h-1 mb-2"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-900 h-1 rounded-full"
|
||||
:style="{ width: Math.ceil(course.membership.progress) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div v-if="user && course.membership" class="text-sm mb-4">
|
||||
{{ Math.ceil(course.membership.progress) }}% completed
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-auto">
|
||||
<div class="flex avatar-group overlap">
|
||||
<div
|
||||
class="mr-1"
|
||||
:class="{ 'avatar-group overlap': course.instructors.length > 1 }"
|
||||
>
|
||||
<UserAvatar
|
||||
v-for="instructor in course.instructors"
|
||||
:user="instructor"
|
||||
/>
|
||||
</div>
|
||||
<span v-if="course.instructors.length == 1">
|
||||
{{ course.instructors[0].full_name }}
|
||||
</span>
|
||||
<span v-if="course.instructors.length == 2">
|
||||
{{ course.instructors[0].first_name }} and
|
||||
{{ course.instructors[1].first_name }}
|
||||
</span>
|
||||
<span v-if="course.instructors.length > 2">
|
||||
{{ course.instructors[0].first_name }} and
|
||||
{{ course.instructors.length - 1 }} others
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="font-semibold">
|
||||
{{ course.price }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Badge, Tooltip } from 'frappe-ui'
|
||||
|
||||
const { user } = sessionStore()
|
||||
|
||||
const props = defineProps({
|
||||
course: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.course-image {
|
||||
height: 168px;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.course-card-pills {
|
||||
background: #ffffff;
|
||||
margin-left: 0;
|
||||
margin-right: 0.5rem;
|
||||
padding: 3.5px 8px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
letter-spacing: 0.011em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.default-image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: theme('colors.orange.100');
|
||||
color: theme('colors.orange.600');
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-group .avatar {
|
||||
transition: margin 0.1s ease-in-out;
|
||||
}
|
||||
.image-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
font-size: 5rem;
|
||||
color: theme('colors.gray.700');
|
||||
font-weight: 600;
|
||||
}
|
||||
.avatar-group.overlap .avatar + .avatar {
|
||||
margin-left: calc(-8px);
|
||||
}
|
||||
|
||||
.short-introduction {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0.25rem 0 1.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
164
frontend/src/components/CourseCardOverlay.vue
Normal file
164
frontend/src/components/CourseCardOverlay.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="shadow rounded-md min-w-80">
|
||||
<iframe
|
||||
v-if="course.data.video_link"
|
||||
:src="video_link"
|
||||
class="rounded-t-md min-h-56 w-full"
|
||||
/>
|
||||
<div class="p-5">
|
||||
<div v-if="course.data.price" class="text-2xl font-semibold mb-3">
|
||||
{{ course.data.price }}
|
||||
</div>
|
||||
<router-link
|
||||
v-if="course.data.membership"
|
||||
:to="{
|
||||
name: 'Lesson',
|
||||
params: {
|
||||
courseName: course.name,
|
||||
chapterNumber: course.data.current_lesson
|
||||
? course.data.current_lesson.split('.')[0]
|
||||
: 1,
|
||||
lessonNumber: course.data.current_lesson
|
||||
? course.data.current_lesson.split('.')[1]
|
||||
: 1,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button variant="solid" size="md" class="w-full">
|
||||
<span>
|
||||
{{ __('Continue Learning') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-else-if="course.data.paid_course"
|
||||
:to="{
|
||||
name: 'Billing',
|
||||
params: {
|
||||
type: 'course',
|
||||
name: course.data.name,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button variant="solid" size="md" class="w-full">
|
||||
<span>
|
||||
{{ __('Buy this course') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
<Button
|
||||
v-else
|
||||
@click="enrollStudent()"
|
||||
variant="solid"
|
||||
class="w-full"
|
||||
size="md"
|
||||
>
|
||||
<span>
|
||||
{{ __('Start Learning') }}
|
||||
</span>
|
||||
</Button>
|
||||
<router-link
|
||||
v-if="user?.data?.is_moderator || is_instructor()"
|
||||
:to="{
|
||||
name: 'CreateCourse',
|
||||
params: {
|
||||
courseName: course.data.name,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button variant="subtle" class="w-full mt-2" size="md">
|
||||
<span>
|
||||
{{ __('Edit') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
<div class="mt-8 mb-4 font-medium">
|
||||
{{ __('This course has:') }}
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<BookOpen class="h-5 w-5 stroke-1.5 text-gray-600" />
|
||||
<span class="ml-2">
|
||||
{{ course.data.lesson_count }} {{ __('Lessons') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<Users class="h-5 w-5 stroke-1.5 text-gray-600" />
|
||||
<span class="ml-2">
|
||||
{{ course.data.enrollment_count_formatted }}
|
||||
{{ __('Enrolled Students') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Star class="h-5 w-5 stroke-1.5 fill-orange-500 text-gray-50" />
|
||||
<span class="ml-2">
|
||||
{{ course.data.avg_rating }} {{ __('Rating') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
||||
import { computed, inject } from 'vue'
|
||||
import { Button, createResource } from 'frappe-ui'
|
||||
import { createToast } from '@/utils/'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
|
||||
const props = defineProps({
|
||||
course: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const video_link = computed(() => {
|
||||
if (props.course.data.video_link) {
|
||||
return 'https://www.youtube.com/embed/' + props.course.data.video_link
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
function enrollStudent() {
|
||||
if (!user.data) {
|
||||
createToast({
|
||||
title: 'Please Login',
|
||||
icon: 'alert-circle',
|
||||
iconClasses: 'text-yellow-600 bg-yellow-100',
|
||||
})
|
||||
setTimeout(() => {
|
||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||
}, 3000)
|
||||
} else {
|
||||
const enrollStudentResource = createResource({
|
||||
url: 'lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership',
|
||||
})
|
||||
console.log(props.course)
|
||||
enrollStudentResource
|
||||
.submit({
|
||||
course: props.course.data.name,
|
||||
})
|
||||
.then(() => {
|
||||
createToast({
|
||||
title: 'Enrolled Successfully',
|
||||
icon: 'check',
|
||||
iconClasses: 'text-green-600 bg-green-100',
|
||||
})
|
||||
setTimeout(() => {
|
||||
router.push({
|
||||
name: 'Lesson',
|
||||
params: {
|
||||
courseName: props.course.data.name,
|
||||
chapterNumber: 1,
|
||||
lessonNumber: 1,
|
||||
},
|
||||
})
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const is_instructor = () => {}
|
||||
</script>
|
||||
170
frontend/src/components/CourseOutline.vue
Normal file
170
frontend/src/components/CourseOutline.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="text-base">
|
||||
<div
|
||||
v-if="title && (outline.data?.length || allowEdit)"
|
||||
class="flex items-center justify-between mb-4"
|
||||
>
|
||||
<div class="font-semibold" :class="allowEdit ? 'text-base' : 'text-lg'">
|
||||
{{ __(title) }}
|
||||
</div>
|
||||
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
|
||||
{{ __('Add Chapter') }}
|
||||
</Button>
|
||||
<!-- <span class="font-medium cursor-pointer" @click="expandAllChapters()">
|
||||
{{ expandAll ? __("Collapse all chapters") : __("Expand all chapters") }}
|
||||
</span> -->
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
'shadow rounded-md pt-2 px-2': showOutline && outline.data?.length,
|
||||
}"
|
||||
>
|
||||
<Disclosure
|
||||
v-slot="{ open }"
|
||||
v-for="(chapter, index) in outline.data"
|
||||
:key="chapter.name"
|
||||
:defaultOpen="openChapterDetail(chapter.idx)"
|
||||
>
|
||||
<DisclosureButton ref="" class="flex w-full px-2 py-3">
|
||||
<ChevronRight
|
||||
:class="{
|
||||
'rotate-90 transform duration-200': open,
|
||||
'duration-200': !open,
|
||||
open: index == 1,
|
||||
}"
|
||||
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
|
||||
/>
|
||||
<div class="text-base text-left font-medium leading-5">
|
||||
{{ chapter.title }}
|
||||
</div>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel class="pb-2">
|
||||
<div v-for="lesson in chapter.lessons" :key="lesson.name">
|
||||
<div class="outline-lesson pl-8 py-2">
|
||||
<router-link
|
||||
:to="{
|
||||
name: allowEdit ? 'CreateLesson' : 'Lesson',
|
||||
params: {
|
||||
courseName: courseName,
|
||||
chapterNumber: lesson.number.split('.')[0],
|
||||
lessonNumber: lesson.number.split('.')[1],
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center text-sm leading-5">
|
||||
<MonitorPlay
|
||||
v-if="lesson.icon === 'icon-youtube'"
|
||||
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
|
||||
/>
|
||||
<HelpCircle
|
||||
v-else-if="lesson.icon === 'icon-quiz'"
|
||||
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
|
||||
/>
|
||||
<FileText
|
||||
v-else-if="lesson.icon === 'icon-list'"
|
||||
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
|
||||
/>
|
||||
{{ lesson.title }}
|
||||
<Check
|
||||
v-if="lesson.is_complete"
|
||||
class="h-4 w-4 text-green-500 stroke-1.5 ml-2"
|
||||
/>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="allowEdit" class="flex mt-2 pl-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'CreateLesson',
|
||||
params: {
|
||||
courseName: courseName,
|
||||
chapterNumber: chapter.idx,
|
||||
lessonNumber: chapter.lessons.length + 1,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
{{ __('Add Lesson') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
<Button class="ml-2" @click="openChapterModal(chapter)">
|
||||
{{ __('Edit Chapter') }}
|
||||
</Button>
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
</div>
|
||||
<ChapterModal
|
||||
v-model="showChapterModal"
|
||||
v-model:outline="outline"
|
||||
:course="courseName"
|
||||
:chapterDetail="getCurrentChapter()"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, createResource } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
|
||||
import {
|
||||
ChevronRight,
|
||||
MonitorPlay,
|
||||
HelpCircle,
|
||||
FileText,
|
||||
Check,
|
||||
} from 'lucide-vue-next'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ChapterModal from '@/components/Modals/ChapterModal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const expandAll = ref(true)
|
||||
const showChapterModal = ref(false)
|
||||
const currentChapter = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showOutline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const outline = createResource({
|
||||
url: 'lms.lms.utils.get_course_outline',
|
||||
cache: ['course_outline', props.courseName],
|
||||
params: {
|
||||
course: props.courseName,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const openChapterDetail = (index) => {
|
||||
return index == route.params.chapterNumber || index == 1
|
||||
}
|
||||
|
||||
const openChapterModal = (chapter = null) => {
|
||||
currentChapter.value = chapter
|
||||
showChapterModal.value = true
|
||||
}
|
||||
|
||||
const getCurrentChapter = () => {
|
||||
return currentChapter.value
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.outline-lesson:has(.router-link-active) {
|
||||
background-color: theme('colors.gray.100');
|
||||
}
|
||||
</style>
|
||||
103
frontend/src/components/CourseReviews.vue
Normal file
103
frontend/src/components/CourseReviews.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div v-if="reviews.data" class="mt-20 mb-10">
|
||||
<Button
|
||||
v-if="membership && !hasReviewed.data"
|
||||
@click="openReviewModal()"
|
||||
class="float-right"
|
||||
>
|
||||
{{ __('Write a Review') }}
|
||||
</Button>
|
||||
<div class="flex items-center font-semibold text-2xl">
|
||||
<Star class="h-6 w-6 stroke-1 text-gray-50 fill-orange-500 mr-1" />
|
||||
{{ avg_rating }} {{ __('ratings and ') }} {{ reviews.data.length }}
|
||||
{{ __('reviews') }}
|
||||
</div>
|
||||
<div class="grid gap-8 mt-10">
|
||||
<div v-for="(review, index) in reviews.data">
|
||||
<div class="flex items-center">
|
||||
<UserAvatar :user="review.owner_details" :size="'2xl'" />
|
||||
<div class="mx-4">
|
||||
<span class="text-lg font-medium mr-4">
|
||||
{{ review.owner_details.full_name }}
|
||||
</span>
|
||||
<span>
|
||||
{{ review.creation }}
|
||||
</span>
|
||||
<div class="flex mt-2">
|
||||
<Star
|
||||
v-for="index in 5"
|
||||
class="h-5 w-5 text-gray-100 bg-gray-200 rounded-sm mr-2"
|
||||
:class="
|
||||
index <= Math.ceil(review.rating)
|
||||
? 'fill-orange-500'
|
||||
: 'fill-gray-600'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="review.review" class="mt-4 leading-5">
|
||||
{{ review.review }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReviewModal
|
||||
v-model="showReviewModal"
|
||||
v-model:reloadReviews="reviews"
|
||||
v-model:hasReviewed="hasReviewed"
|
||||
:courseName="courseName"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { createResource, Button } from 'frappe-ui'
|
||||
import { computed, ref, inject } from 'vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import ReviewModal from '@/components/Modals/ReviewModal.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
avg_rating: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
membership: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const hasReviewed = createResource({
|
||||
url: 'frappe.client.get_count',
|
||||
cache: ['eligible_to_review', props.courseName, props.membership?.member],
|
||||
params: {
|
||||
doctype: 'LMS Course Review',
|
||||
filters: {
|
||||
course: props.courseName,
|
||||
owner: props.membership?.member,
|
||||
},
|
||||
},
|
||||
auto: user.data?.name ? true : false,
|
||||
})
|
||||
|
||||
const reviews = createResource({
|
||||
url: 'lms.lms.utils.get_reviews',
|
||||
cache: ['course_reviews', props.courseName],
|
||||
params: {
|
||||
course: props.courseName,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const showReviewModal = ref(false)
|
||||
|
||||
function openReviewModal() {
|
||||
showReviewModal.value = true
|
||||
}
|
||||
</script>
|
||||
30
frontend/src/components/CreateOutline.vue
Normal file
30
frontend/src/components/CreateOutline.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div v-if="course">
|
||||
<div class="text-xl font-semibold">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
<div v-if="course.chapters.length">
|
||||
{{ course.chapters }}
|
||||
</div>
|
||||
<div v-else class="border bg-white rounded-md p-5 text-center mt-4">
|
||||
<div>
|
||||
{{
|
||||
__(
|
||||
'There are no chapters in this course. Create and manage chapters from here.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<Button class="mt-4">
|
||||
{{ __('Add Chapter') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
course: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
20
frontend/src/components/DesktopLayout.vue
Normal file
20
frontend/src/components/DesktopLayout.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="relative flex h-full flex-col">
|
||||
<div class="h-full flex-1">
|
||||
<div class="flex h-screen text-base">
|
||||
<div
|
||||
class="relative block min-h-0 flex-shrink-0 overflow-hidden hover:overflow-auto"
|
||||
>
|
||||
<slot name="sidebar" />
|
||||
<AppSidebar />
|
||||
</div>
|
||||
<div class="w-full overflow-auto" id="scrollContainer">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import AppSidebar from './AppSidebar.vue'
|
||||
</script>
|
||||
231
frontend/src/components/DiscussionReplies.vue
Normal file
231
frontend/src/components/DiscussionReplies.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="mt-6">
|
||||
<div v-if="!singleThread" class="flex items-center mb-5">
|
||||
<Button variant="outline" @click="showTopics = true">
|
||||
<template #icon>
|
||||
<ChevronLeft class="w-5 h-5 stroke-1.5 text-gray-700" />
|
||||
</template>
|
||||
</Button>
|
||||
<span class="text-lg font-semibold ml-2">
|
||||
{{ topic.title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-for="(reply, index) in replies.data">
|
||||
<div
|
||||
class="py-3"
|
||||
:class="{ 'border-b': index + 1 != replies.data.length }"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<UserAvatar :user="reply.user" class="mr-2" />
|
||||
<span>
|
||||
{{ reply.user.full_name }}
|
||||
</span>
|
||||
<span class="text-sm ml-2">
|
||||
{{ timeAgo(reply.creation) }}
|
||||
</span>
|
||||
</div>
|
||||
<Dropdown
|
||||
v-if="user.data.name == reply.owner && !reply.editable"
|
||||
:options="[
|
||||
{
|
||||
label: 'Edit',
|
||||
onClick() {
|
||||
reply.editable = true
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick() {
|
||||
deleteReply(reply)
|
||||
},
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template v-slot="{ open }">
|
||||
<MoreHorizontal class="w-4 h-4 stroke-1.5 cursor-pointer" />
|
||||
</template>
|
||||
</Dropdown>
|
||||
<div v-if="reply.editable">
|
||||
<Button variant="ghost" @click="postEdited(reply)">
|
||||
{{ __('Post') }}
|
||||
</Button>
|
||||
<Button variant="ghost" @click="reply.editable = false">
|
||||
{{ __('Discard') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TextEditor
|
||||
:content="reply.reply"
|
||||
@change="(val) => (reply.reply = val)"
|
||||
:editable="reply.editable || false"
|
||||
:fixedMenu="reply.editable || false"
|
||||
:editorClass="
|
||||
reply.editable
|
||||
? 'ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none'
|
||||
: 'prose-sm'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TextEditor
|
||||
class="mt-5"
|
||||
:content="newReply"
|
||||
@change="(val) => (newReply = val)"
|
||||
placeholder="Type your reply here..."
|
||||
:fixedMenu="true"
|
||||
editorClass="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none border border-gray-300 rounded-b-md min-h-[7rem] py-1 px-2"
|
||||
/>
|
||||
<div class="flex justify-between mt-2">
|
||||
<span> </span>
|
||||
<Button @click="postReply()">
|
||||
<span>
|
||||
{{ __('Post') }}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, TextEditor, Button, Dropdown } from 'frappe-ui'
|
||||
import { timeAgo } from '../utils'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
|
||||
import { ref, inject, onMounted } from 'vue'
|
||||
import { createToast } from '../utils'
|
||||
|
||||
const showTopics = defineModel('showTopics')
|
||||
const newReply = ref('')
|
||||
const socket = inject('$socket')
|
||||
const user = inject('$user')
|
||||
|
||||
const props = defineProps({
|
||||
topic: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
singleThread: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
socket.on('publish_message', (data) => {
|
||||
replies.reload()
|
||||
})
|
||||
socket.on('update_message', (data) => {
|
||||
replies.reload()
|
||||
})
|
||||
socket.on('delete_message', (data) => {
|
||||
replies.reload()
|
||||
})
|
||||
})
|
||||
|
||||
const replies = createResource({
|
||||
url: 'lms.lms.utils.get_discussion_replies',
|
||||
cache: ['replies', props.topic],
|
||||
makeParams(values) {
|
||||
return {
|
||||
topic: props.topic.name,
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const newReplyResource = createResource({
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'Discussion Reply',
|
||||
reply: newReply.value,
|
||||
topic: props.topic.name,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const postReply = () => {
|
||||
newReplyResource.submit(
|
||||
{},
|
||||
{
|
||||
validate() {
|
||||
if (!newReply.value) {
|
||||
return 'Reply cannot be empty'
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
newReply.value = ''
|
||||
replies.reload()
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'bg-red-600 text-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const editReplyResource = createResource({
|
||||
url: 'frappe.client.set_value',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'Discussion Reply',
|
||||
name: values.name,
|
||||
fieldname: 'reply',
|
||||
value: values.reply,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const postEdited = (reply) => {
|
||||
editReplyResource.submit(
|
||||
{
|
||||
name: reply.name,
|
||||
reply: reply.reply,
|
||||
},
|
||||
{
|
||||
validate() {
|
||||
if (!reply.reply) {
|
||||
return 'Reply cannot be empty'
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
reply.editable = false
|
||||
replies.reload()
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const deleteReplyResource = createResource({
|
||||
url: 'frappe.client.delete',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'Discussion Reply',
|
||||
name: values.name,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const deleteReply = (reply) => {
|
||||
deleteReplyResource.submit(
|
||||
{
|
||||
name: reply.name,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
replies.reload()
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
132
frontend/src/components/Discussions.vue
Normal file
132
frontend/src/components/Discussions.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div>
|
||||
<Button v-if="!singleThread" class="float-right" @click="openTopicModal()">
|
||||
{{ __('New {0}').format(title) }}
|
||||
</Button>
|
||||
<div class="text-xl font-semibold">
|
||||
{{ __(title) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="topics.data?.length && !singleThread">
|
||||
<div v-if="showTopics" v-for="(topic, index) in topics.data">
|
||||
<div
|
||||
@click="showReplies(topic)"
|
||||
class="flex items-center cursor-pointer py-5 w-full"
|
||||
:class="{ 'border-b': index + 1 != topics.data.length }"
|
||||
>
|
||||
<UserAvatar :user="topic.user" size="2xl" class="mr-4" />
|
||||
<div>
|
||||
<div class="text-lg font-semibold mb-1">
|
||||
{{ topic.title }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span>
|
||||
{{ topic.user.full_name }}
|
||||
</span>
|
||||
<span class="text-sm ml-3">
|
||||
{{ timeAgo(topic.creation) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<DiscussionReplies
|
||||
:topic="currentTopic"
|
||||
v-model:showTopics="showTopics"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="singleThread && topics.data">
|
||||
<DiscussionReplies :topic="topics.data" :singleThread="singleThread" />
|
||||
</div>
|
||||
<div v-else class="flex justify-center border mt-5 p-5 rounded-md">
|
||||
<MessageSquareIcon class="w-10 h-10 stroke-1.5 text-gray-800 mr-2" />
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-2">
|
||||
{{ __(emptyStateTitle) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ __(emptyStateText) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DiscussionModal
|
||||
v-model="showTopicModal"
|
||||
:title="__('New {0}').format(title)"
|
||||
:doctype="props.doctype"
|
||||
:docname="props.docname"
|
||||
v-model:reloadTopics="topics"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, Button, TextEditor } from 'frappe-ui'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { timeAgo } from '../utils'
|
||||
import { ref, onMounted, inject } from 'vue'
|
||||
import DiscussionReplies from '@/components/DiscussionReplies.vue'
|
||||
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
|
||||
import { MessageSquareIcon } from 'lucide-vue-next'
|
||||
|
||||
const showTopics = ref(true)
|
||||
const currentTopic = ref(null)
|
||||
const socket = inject('$socket')
|
||||
const user = inject('$user')
|
||||
const showTopicModal = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
doctype: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
docname: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyStateTitle: {
|
||||
type: String,
|
||||
default: 'No topics yet',
|
||||
},
|
||||
emptyStateText: {
|
||||
type: String,
|
||||
default: 'Be the first to start a discussion',
|
||||
},
|
||||
singleThread: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (user.data) topics.reload()
|
||||
|
||||
socket.on('new_discussion_topic', (data) => {
|
||||
topics.refresh()
|
||||
})
|
||||
})
|
||||
|
||||
const topics = createResource({
|
||||
url: 'lms.lms.utils.get_discussion_topics',
|
||||
cache: ['topics', props.doctype, props.docname],
|
||||
makeParams() {
|
||||
return {
|
||||
doctype: props.doctype,
|
||||
docname: props.docname,
|
||||
single_thread: props.singleThread,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const showReplies = (topic) => {
|
||||
showTopics.value = false
|
||||
currentTopic.value = topic
|
||||
}
|
||||
|
||||
const openTopicModal = () => {
|
||||
showTopicModal.value = true
|
||||
}
|
||||
</script>
|
||||
23
frontend/src/components/Icons/BatchIcon.vue
Normal file
23
frontend/src/components/Icons/BatchIcon.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_1584_1676)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.17474 0.625C2.34632 0.625 1.67474 1.29657 1.67474 2.125V7.475C1.67474 8.30343 2.34632 8.975 3.17474 8.975H14.8247C15.6532 8.975 16.3247 8.30343 16.3247 7.475V2.125C16.3247 1.29657 15.6532 0.625 14.8247 0.625H3.17474ZM2.67474 2.125C2.67474 1.84886 2.8986 1.625 3.17474 1.625H14.8247C15.1009 1.625 15.3247 1.84886 15.3247 2.125V7.475C15.3247 7.75114 15.1009 7.975 14.8247 7.975H3.17474C2.8986 7.975 2.67474 7.75114 2.67474 7.475V2.125ZM4.27478 10.0749C3.99864 10.0749 3.77478 10.2987 3.77478 10.5749V12.6749C3.77478 12.951 3.99864 13.1749 4.27478 13.1749C4.55092 13.1749 4.77478 12.951 4.77478 12.6749V11.0749H6.92478V12.6749C6.92478 12.951 7.14864 13.1749 7.42478 13.1749C7.70092 13.1749 7.92478 12.951 7.92478 12.6749V10.5749C7.92478 10.2987 7.70092 10.0749 7.42478 10.0749H4.27478ZM10.0749 10.5749C10.0749 10.2987 10.2987 10.0749 10.5749 10.0749H13.7249C14.001 10.0749 14.2249 10.2987 14.2249 10.5749V12.6749C14.2249 12.951 14.001 13.1749 13.7249 13.1749C13.4487 13.1749 13.2249 12.951 13.2249 12.6749V11.0749H11.0749V12.6749C11.0749 12.951 10.851 13.1749 10.5749 13.1749C10.2987 13.1749 10.0749 12.951 10.0749 12.6749V10.5749ZM1.125 14.275C0.848858 14.275 0.625 14.4988 0.625 14.775V16.875C0.625 17.1511 0.848858 17.375 1.125 17.375C1.40114 17.375 1.625 17.1511 1.625 16.875V15.275H3.775V16.875C3.775 17.1511 3.99886 17.375 4.275 17.375C4.55114 17.375 4.775 17.1511 4.775 16.875V14.775C4.775 14.4988 4.55114 14.275 4.275 14.275H1.125ZM13.2252 14.775C13.2252 14.4988 13.4491 14.275 13.7252 14.275H16.8752C17.1514 14.275 17.3752 14.4988 17.3752 14.775V16.875C17.3752 17.1511 17.1514 17.375 16.8752 17.375C16.5991 17.375 16.3752 17.1511 16.3752 16.875V15.275H14.2252V16.875C14.2252 17.1511 14.0014 17.375 13.7252 17.375C13.4491 17.375 13.2252 17.1511 13.2252 16.875V14.775ZM7.42511 14.275C7.14897 14.275 6.92511 14.4988 6.92511 14.775V16.875C6.92511 17.1511 7.14897 17.375 7.42511 17.375C7.70125 17.375 7.92511 17.1511 7.92511 16.875V15.275H10.0751V16.875C10.0751 17.1511 10.299 17.375 10.5751 17.375C10.8513 17.375 11.0751 17.1511 11.0751 16.875V14.775C11.0751 14.4988 10.8513 14.275 10.5751 14.275H7.42511Z"
|
||||
fill="#525252"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1584_1676">
|
||||
<rect width="18" height="18" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
27
frontend/src/components/Icons/CollapseSidebar.vue
Normal file
27
frontend/src/components/Icons/CollapseSidebar.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.875 9.06223L3 9.06232"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M6.74537 5.31699L3 9.06236L6.74527 12.8076"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.1423 4L14.1423 14.125"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
18
frontend/src/components/Icons/IndicatorIcon.vue
Normal file
18
frontend/src/components/Icons/IndicatorIcon.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="8"
|
||||
cy="8"
|
||||
r="4.5"
|
||||
fill="transparent"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
36
frontend/src/components/Icons/LMSLogo.vue
Normal file
36
frontend/src/components/Icons/LMSLogo.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<svg
|
||||
width="118"
|
||||
height="118"
|
||||
viewBox="0 0 118 118"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M93.9278 0H23.1013C10.3428 0 0 10.3428 0 23.1013V93.9278C0 106.686 10.3428 117.029 23.1013 117.029H93.9278C106.686 117.029 117.029 106.686 117.029 93.9278V23.1013C117.029 10.3428 106.686 0 93.9278 0Z"
|
||||
fill="url(#paint0_radial_174_336)"
|
||||
/>
|
||||
<path
|
||||
d="M93.9278 0H23.1013C10.3428 0 0 10.3428 0 23.1013V93.9278C0 106.686 10.3428 117.029 23.1013 117.029H93.9278C106.686 117.029 117.029 106.686 117.029 93.9278V23.1013C117.029 10.3428 106.686 0 93.9278 0Z"
|
||||
fill="#0B3D3D"
|
||||
fill-opacity="0.8"
|
||||
/>
|
||||
<path
|
||||
d="M95.1879 33.1294L91.4077 32.0268C80.1721 28.7716 67.9389 30.9242 58.5409 37.7496C52.083 33.0769 43.9975 30.5042 36.1746 30.5042H21.8938V41.0048H36.2796C42.2649 41.0048 48.1978 42.9999 52.923 46.6226L58.5934 50.9279L64.2637 46.6226C70.144 42.1599 77.5469 40.2698 84.7923 41.2673V76.1818C75.5518 75.2367 66.2063 77.7044 58.6459 83.2172C51.0854 77.7044 41.6349 75.2367 32.4994 76.1818V52.8705H21.9988V86.4724H95.3454V33.1294H95.1879Z"
|
||||
fill="#58FF9B"
|
||||
/>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial_174_336"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(117.24 -101.5) rotate(105.042) scale(226.282)"
|
||||
>
|
||||
<stop offset="0.445162" stop-color="#1F7676" />
|
||||
<stop offset="1" stop-color="#0A4B4B" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
81
frontend/src/components/JobCard.vue
Normal file
81
frontend/src/components/JobCard.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="flex shadow rounded-md p-4 h-full">
|
||||
<img
|
||||
:src="job.company_logo"
|
||||
class="w-12 h-12 rounded-lg object-contain mr-4"
|
||||
:alt="job.company_name"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-2">
|
||||
{{ job.job_title }}
|
||||
</div>
|
||||
<div>
|
||||
{{ __('posted by') }}
|
||||
<span class="font-medium">
|
||||
{{ job.company_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center my-4">
|
||||
<Badge :label="job.type" theme="green" size="lg" class="mr-4" />
|
||||
<Badge :label="job.location.split(' ')[0]" theme="gray" size="lg">
|
||||
<template #prefix>
|
||||
<MapPin class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
{{ __('posted on') }}
|
||||
<span class="font-medium">
|
||||
{{ dayjs(job.creation).format('DD MMM YYYY') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="flex flex-col shadow rounded-md p-4 h-full">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-2">
|
||||
{{ job.job_title }}
|
||||
</div>
|
||||
<div>
|
||||
{{ __("posted by") }}
|
||||
<span class="font-medium">
|
||||
{{ job.company_name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
:src="job.company_logo"
|
||||
class="w-12 h-12 rounded-lg object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-between mt-8">
|
||||
<div class="flex items-center">
|
||||
<Badge :label="job.type" theme="green" size="lg" class="mr-4"/>
|
||||
<Badge :label="job.location" theme="gray" size="lg">
|
||||
<template #prefix>
|
||||
<MapPin class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">
|
||||
{{ dayjs(job.creation).format('DD MMM YYYY') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</template>
|
||||
<script setup>
|
||||
import { MapPin } from 'lucide-vue-next'
|
||||
import { Badge } from 'frappe-ui'
|
||||
import { inject } from 'vue'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const props = defineProps({
|
||||
job: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user