CLASSROOMIO-T-21: Course landing page + editor

This commit is contained in:
Rotimi Best
2022-12-30 13:08:54 +01:00
parent 38eae9dafb
commit 963d899adf
80 changed files with 2543 additions and 653 deletions
+174 -7
View File
@@ -1541,11 +1541,27 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
}, },
"@types/lodash": {
"version": "4.14.191",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"@types/node": { "@types/node": {
"version": "15.6.0", "version": "15.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.6.0.tgz",
"integrity": "sha512-gCYSfQpy+LYhOFTKAeE8BkyGqaxmlFxe+n4DKM6DR0wzw/HISUE/hAmkC/KT8Sw5PCJblqg062b3z9gucv3k0A==" "integrity": "sha512-gCYSfQpy+LYhOFTKAeE8BkyGqaxmlFxe+n4DKM6DR0wzw/HISUE/hAmkC/KT8Sw5PCJblqg062b3z9gucv3k0A=="
}, },
"@types/paystack": {
"version": "2.0.1",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/@types/paystack/-/paystack-2.0.1.tgz",
"integrity": "sha512-pM5xu4ahqFSpxNFi5fm/QJtuplUuSg3PzjnMy7XjDapebUJ+Ixw1soUCV7UbqMra9No2WFXn81AcNdtPvc1niw=="
},
"@types/pluralize": {
"version": "0.0.29",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/@types/pluralize/-/pluralize-0.0.29.tgz",
"integrity": "sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA=="
},
"@types/pug": { "@types/pug": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.4.tgz",
@@ -1680,6 +1696,11 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
}, },
"asap": {
"version": "2.0.6",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
},
"asn1": { "asn1": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@@ -1874,7 +1895,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2" "get-intrinsic": "^1.0.2"
@@ -1945,6 +1965,15 @@
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.4.0.tgz", "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.4.0.tgz",
"integrity": "sha512-S18o4Y9PqI/uabdlT/jI3MY7XBJjNxnfapFIkjkMwpz6qNxLFZOm2b22OMf4ZYDL9lpNWI+Ih4fEMVPwO1KHFQ==" "integrity": "sha512-S18o4Y9PqI/uabdlT/jI3MY7XBJjNxnfapFIkjkMwpz6qNxLFZOm2b22OMf4ZYDL9lpNWI+Ih4fEMVPwO1KHFQ=="
}, },
"carbon-components-svelte": {
"version": "0.70.13",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/carbon-components-svelte/-/carbon-components-svelte-0.70.13.tgz",
"integrity": "sha512-p6wX8004BM6DrrLH1JJWbkR/VybU0gra+jDWlSW1V2aO8XmAz81evnDLZuTrvbHNkEXPiaC+athSjNsBOf0SjA==",
"dev": true,
"requires": {
"flatpickr": "4.6.9"
}
},
"carbon-icons-svelte": { "carbon-icons-svelte": {
"version": "10.45.1", "version": "10.45.1",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/carbon-icons-svelte/-/carbon-icons-svelte-10.45.1.tgz", "resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/carbon-icons-svelte/-/carbon-icons-svelte-10.45.1.tgz",
@@ -2046,6 +2075,11 @@
} }
} }
}, },
"clone": {
"version": "2.1.2",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
},
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@@ -2331,6 +2365,19 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
}, },
"deep-equal": {
"version": "1.1.1",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/deep-equal/-/deep-equal-1.1.1.tgz",
"integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
"requires": {
"is-arguments": "^1.0.4",
"is-date-object": "^1.0.1",
"is-regex": "^1.0.4",
"object-is": "^1.0.1",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.2.0"
}
},
"deep-freeze": { "deep-freeze": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz",
@@ -2347,7 +2394,6 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": { "requires": {
"object-keys": "^1.0.12" "object-keys": "^1.0.12"
} }
@@ -2523,6 +2569,11 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
}, },
"eventemitter3": {
"version": "2.0.3",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg=="
},
"express": { "express": {
"version": "4.17.1", "version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
@@ -2597,6 +2648,11 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}, },
"fast-diff": {
"version": "1.1.2",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
},
"fast-glob": { "fast-glob": {
"version": "3.2.5", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz",
@@ -2657,6 +2713,12 @@
"pinkie-promise": "^2.0.0" "pinkie-promise": "^2.0.0"
} }
}, },
"flatpickr": {
"version": "4.6.9",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/flatpickr/-/flatpickr-4.6.9.tgz",
"integrity": "sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw==",
"dev": true
},
"forever-agent": { "forever-agent": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -2719,6 +2781,11 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
}, },
"functions-have-names": {
"version": "1.2.3",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="
},
"gauge": { "gauge": {
"version": "2.7.4", "version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@@ -2757,7 +2824,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
@@ -2895,8 +2961,15 @@
"has-symbols": { "has-symbols": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
"dev": true },
"has-tostringtag": {
"version": "1.0.0",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"requires": {
"has-symbols": "^1.0.2"
}
}, },
"has-unicode": { "has-unicode": {
"version": "2.0.1", "version": "2.0.1",
@@ -3043,6 +3116,15 @@
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
}, },
"is-arguments": {
"version": "1.1.1",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"requires": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
}
},
"is-arrayish": { "is-arrayish": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -3065,6 +3147,14 @@
"has": "^1.0.3" "has": "^1.0.3"
} }
}, },
"is-date-object": {
"version": "1.0.5",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/is-date-object/-/is-date-object-1.0.5.tgz",
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
"requires": {
"has-tostringtag": "^1.0.0"
}
},
"is-directory": { "is-directory": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
@@ -3126,6 +3216,15 @@
"@types/estree": "*" "@types/estree": "*"
} }
}, },
"is-regex": {
"version": "1.1.4",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/is-regex/-/is-regex-1.1.4.tgz",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
"requires": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
}
},
"is-typedarray": { "is-typedarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@@ -3669,11 +3768,19 @@
"integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==", "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==",
"dev": true "dev": true
}, },
"object-is": {
"version": "1.1.5",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/object-is/-/object-is-1.1.5.tgz",
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
"object-keys": { "object-keys": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
"dev": true
}, },
"object.assign": { "object.assign": {
"version": "4.1.2", "version": "4.1.2",
@@ -3738,6 +3845,11 @@
"no-case": "^2.2.0" "no-case": "^2.2.0"
} }
}, },
"parchment": {
"version": "1.1.4",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
},
"parse-glob": { "parse-glob": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
@@ -3818,6 +3930,15 @@
"pinkie-promise": "^2.0.0" "pinkie-promise": "^2.0.0"
} }
}, },
"paystack": {
"version": "2.0.1",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/paystack/-/paystack-2.0.1.tgz",
"integrity": "sha512-reVONV7ZUMln/iWeM60n0BbogF3/zFWmUrqbKYVNzEAv+p9TcWDCHfNZ2mBGXzIXhyTsNXWwf4wNcXe28btAHw==",
"requires": {
"promise": "^7.1.1",
"request": "^2.79.0"
}
},
"performance-now": { "performance-now": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -3846,6 +3967,11 @@
"pinkie": "^2.0.0" "pinkie": "^2.0.0"
} }
}, },
"pluralize": {
"version": "8.0.0",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/pluralize/-/pluralize-8.0.0.tgz",
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="
},
"polka": { "polka": {
"version": "1.0.0-next.22", "version": "1.0.0-next.22",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/polka/-/polka-1.0.0-next.22.tgz", "resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/polka/-/polka-1.0.0-next.22.tgz",
@@ -3988,6 +4114,14 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
}, },
"promise": {
"version": "7.3.1",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"requires": {
"asap": "~2.0.3"
}
},
"proxy-addr": { "proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -4044,6 +4178,29 @@
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true "dev": true
}, },
"quill": {
"version": "1.3.7",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"requires": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"quill-delta": {
"version": "3.6.3",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"requires": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
}
},
"randombytes": { "randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -4175,6 +4332,16 @@
"@babel/runtime": "^7.8.4" "@babel/runtime": "^7.8.4"
} }
}, },
"regexp.prototype.flags": {
"version": "1.4.3",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
"integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"functions-have-names": "^1.2.2"
}
},
"regexparam": { "regexparam": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/regexparam/-/regexparam-1.3.0.tgz", "resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/regexparam/-/regexparam-1.3.0.tgz",
+7
View File
@@ -16,6 +16,8 @@
"@sentry/tracing": "^6.13.3", "@sentry/tracing": "^6.13.3",
"@supabase/supabase-js": "^1.20.0", "@supabase/supabase-js": "^1.20.0",
"@tailwindcss/forms": "^0.3.2", "@tailwindcss/forms": "^0.3.2",
"@types/paystack": "^2.0.1",
"@types/pluralize": "0.0.29",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"canvas-confetti": "^1.4.0", "canvas-confetti": "^1.4.0",
"compression": "^1.7.1", "compression": "^1.7.1",
@@ -27,7 +29,10 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^1.2.7", "marked": "^1.2.7",
"node-sass": "^6.0.0", "node-sass": "^6.0.0",
"paystack": "^2.0.1",
"pluralize": "^8.0.0",
"polka": "next", "polka": "next",
"quill": "^1.3.7",
"sapper-environment": "^1.0.1", "sapper-environment": "^1.0.1",
"sirv": "^1.0.0", "sirv": "^1.0.0",
"svelte-easy-crop": "^1.0.4", "svelte-easy-crop": "^1.0.4",
@@ -47,6 +52,8 @@
"@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-replace": "^2.4.2",
"@rollup/plugin-typescript": "^8.3.0", "@rollup/plugin-typescript": "^8.3.0",
"@tailwindcss/typography": "^0.3.1", "@tailwindcss/typography": "^0.3.1",
"@types/lodash": "^4.14.191",
"carbon-components-svelte": "^0.70.13",
"carbon-icons-svelte": "^10.21.0", "carbon-icons-svelte": "^10.21.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"postcss": "^8.0.9", "postcss": "^8.0.9",
@@ -1,6 +1,6 @@
<script> <script>
import Label from "./Label.svelte"; import Label from './Label.svelte';
import Avatar from "./Avatar.svelte"; import Avatar from './Avatar.svelte';
export let poll = {}; export let poll = {};
export let onSelect = () => {}; export let onSelect = () => {};
@@ -19,7 +19,7 @@
} }
function getSelectedUsers(users) { function getSelectedUsers(users) {
return users.map((o) => o.label).join(", "); return users.map((o) => o.label).join(', ');
} }
function getTotalVotes(options) { function getTotalVotes(options) {
@@ -103,7 +103,7 @@
<div class="mr-5"> <div class="mr-5">
<Label>Expiration</Label> <Label>Expiration</Label>
<p class="text-sm text-gray-600 mb-4"> <p class="text-sm text-gray-600 mb-4">
{poll.expiration || "Today"} {poll.expiration || 'Today'}
</p> </p>
</div> </div>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
@@ -170,7 +170,7 @@
{/each} {/each}
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600">
Expires: {poll.expiration || "Today"} Expires: {poll.expiration || 'Today'}
</p> </p>
<span class="mx-2">|</span> <span class="mx-2">|</span>
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600">
@@ -182,7 +182,7 @@
{#if isAuthor} {#if isAuthor}
<div class="w-full text-center mt-3"> <div class="w-full text-center mt-3">
<button <button
class="px-5 py-2 bg-blue-700 text-white" class="px-5 py-3 bg-blue-700 text-white"
on:click={() => (viewResult = !viewResult)} on:click={() => (viewResult = !viewResult)}
> >
{#if viewResult} {#if viewResult}
@@ -192,7 +192,7 @@
{/if} {/if}
</button> </button>
<button <button
class="px-5 py-2 bg-red-500 text-white" class="px-5 py-3 bg-red-500 text-white"
on:click={handlePollDelete} on:click={handlePollDelete}
> >
Delete Delete
+2 -2
View File
@@ -57,9 +57,9 @@
</button> </button>
{#if facultyFilterOpened} {#if facultyFilterOpened}
<div class="w-48 bg-white border border-grey rounded-lg mt-2 py-2"> <div class="w-48 bg-white border border-grey rounded-lg mt-2 py-3">
{#each faculties as faculty} {#each faculties as faculty}
<div class="block px-4 py-2 border-b"> <div class="block px-4 py-3 border-b">
<input <input
type="checkbox" type="checkbox"
class="border border-gray checked:bg-blue-600 checked:border-transparent mr-1 cursor-pointer" class="border border-gray checked:bg-blue-600 checked:border-transparent mr-1 cursor-pointer"
+1 -1
View File
@@ -28,7 +28,7 @@
<div class="w-full min-h-screen flex items-center justify-center mt-4"> <div class="w-full min-h-screen flex items-center justify-center mt-4">
<div class="container border border-gray bg-white"> <div class="container border border-gray bg-white">
<div class="flex items-center flex-col p-2 lg:px-8 lg:py-4"> <div class="flex items-center flex-col p-2 lg:px-8 lg:py-3">
{#if !showOnlyContent || showLogo} {#if !showOnlyContent || showLogo}
<div class="flex items-center w-full justify-center pt-4"> <div class="flex items-center w-full justify-center pt-4">
<img <img
+4 -1
View File
@@ -9,13 +9,16 @@
p { p {
display: inline-block; display: inline-block;
min-width: 20px; min-width: 20px;
padding: 0 6px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
line-height: 18px; line-height: 18px;
text-align: center; text-align: center;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 2em; border-radius: 2em;
width: 24px;
height: 24px;
padding-top: 2px;
} }
.bg-set { .bg-set {
@@ -25,7 +25,7 @@
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No, please don't" label="No, please don't"
onClick={() => { onClick={() => {
@@ -34,7 +34,7 @@
}} }}
/> />
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER} variant={VARIANTS.CONTAINED_DANGER}
label="Yes, delete" label="Yes, delete"
onClick={() => { onClick={() => {
@@ -132,13 +132,13 @@
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No, cancel" label="No, cancel"
type="submit" type="submit"
/> />
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER} variant={VARIANTS.CONTAINED_DANGER}
label={isDeleting ? 'Deleting...' : 'Yes, delete'} label={isDeleting ? 'Deleting...' : 'Yes, delete'}
isDisabled={isDeleting} isDisabled={isDeleting}
@@ -174,7 +174,7 @@
</QuestionContainer> </QuestionContainer>
<div class="float-right"> <div class="float-right">
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER} variant={VARIANTS.CONTAINED_DANGER}
label="Delete this exercise" label="Delete this exercise"
onClick={() => (shouldDelete = true)} onClick={() => (shouldDelete = true)}
@@ -111,7 +111,7 @@
}} }}
label="Save" label="Save"
variant={VARIANTS.CONTAINED_SUCCESS} variant={VARIANTS.CONTAINED_SUCCESS}
className="py-2 px-8" className="py-3 px-8"
/> />
</div> </div>
<div class="border border-gray-300 rounded-md mt-2"> <div class="border border-gray-300 rounded-md mt-2">
@@ -228,14 +228,14 @@
<h2 class="text-xl mb-2 mt-0">{$questionnaire.title}</h2> <h2 class="text-xl mb-2 mt-0">{$questionnaire.title}</h2>
{#if STATUS.GRADED === $questionnaireMetaData.status} {#if STATUS.GRADED === $questionnaireMetaData.status}
<span <span
class="status-text bg-green-700 text-white rounded-full py-2 px-6 text-center" class="status-text bg-green-700 text-white rounded-full py-3 px-6 text-center"
title="Status: Pending Review" title="Status: Pending Review"
> >
Graded Graded
</span> </span>
{:else} {:else}
<span <span
class="status-text bg-yellow-600 text-white rounded-full py-2 px-6 text-center" class="status-text bg-yellow-600 text-white rounded-full py-3 px-6 text-center"
title="Status: Pending Review" title="Status: Pending Review"
> >
Pending Pending
@@ -3,7 +3,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import AudioConsole32 from 'carbon-icons-svelte/lib/AudioConsole32'; import AudioConsole32 from 'carbon-icons-svelte/lib/AudioConsole32';
import Box from '../../../../Box/index.svelte'; import Box from '../../../../Box/index.svelte';
import { Firework } from 'svelte-loading-spinners'; import { Moon } from 'svelte-loading-spinners';
import Backdrop from '../../../../Backdrop/index.svelte'; import Backdrop from '../../../../Backdrop/index.svelte';
import PrimaryButton from '../../../../PrimaryButton/index.svelte'; import PrimaryButton from '../../../../PrimaryButton/index.svelte';
import RoleBasedSecurity from '../../../../RoleBasedSecurity/index.svelte'; import RoleBasedSecurity from '../../../../RoleBasedSecurity/index.svelte';
@@ -122,7 +122,7 @@
{#if isFetching} {#if isFetching}
<Backdrop> <Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" /> <Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop> </Backdrop>
{/if} {/if}
@@ -147,7 +147,7 @@
<div class="mt-5 flex items-center"> <div class="mt-5 flex items-center">
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
label="Submit" label="Submit"
onClick={handleAddExercise} onClick={handleAddExercise}
/> />
@@ -1,5 +1,6 @@
<script> <script>
import MODES from '../../../../../utils/constants/mode.js'; import MODES from '../../../../../utils/constants/mode.js';
import { formatYoutubeVideo } from '../../../../../utils/functions/formatYoutubeVideo';
import Tabs from '../../../../Tabs/index.svelte'; import Tabs from '../../../../Tabs/index.svelte';
import TabContent from '../../../../TabContent/index.svelte'; import TabContent from '../../../../TabContent/index.svelte';
@@ -24,34 +25,6 @@
const onChange = (tab) => () => (currentTab = tab); const onChange = (tab) => () => (currentTab = tab);
function formatYoutubeVideo(url) {
console.log(`url`, url);
const prefix = 'https://www.youtube.com/embed';
// https://www.youtube.com/embed/qajK1J1neAM
if (url.includes('embed')) {
return url;
}
// https://youtu.be/qajK1J1neAM
if (url.includes('.be/')) {
const splittedUrlWithBe = url.split('.be/');
return `${prefix}/${splittedUrlWithBe[1]}`;
}
// https://www.youtube.com/watch?v=qajK1J1neAM
const splitedUrl = url.split('watch');
if (splitedUrl.length !== 2) {
errors.video = 'Wrong url format';
return;
}
const query = new URLSearchParams(splitedUrl[1]);
return `${prefix}/${query.get('v')}`;
}
function handleSave(prevMode) { function handleSave(prevMode) {
if (prevMode === MODES.edit) { if (prevMode === MODES.edit) {
handleUpdateLessonMaterials($lesson, lessonId); handleUpdateLessonMaterials($lesson, lessonId);
@@ -146,7 +119,7 @@
width="100%" width="100%"
height="569" height="569"
class="iframe" class="iframe"
src={formatYoutubeVideo(url)} src={formatYoutubeVideo(url, errors)}
title="YouTube video player" title="YouTube video player"
frameborder="0" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
@@ -1,11 +1,13 @@
import { writable } from 'svelte/store'; import { writable, Writable, Updater } from 'svelte/store';
import { import {
createLesson, createLesson,
updateLesson, updateLesson,
deleteLesson, deleteLesson,
} from '../../../../../utils/services/courses'; } from '../../../../../utils/services/courses';
export const lessons = writable([]); import type { Lesson, Course } from '../../../../../utils/types';
export const lessons: Writable<Lesson[]> = writable([]);
export const lesson = writable({ export const lesson = writable({
id: null, id: null,
totalExercises: 0, totalExercises: 0,
@@ -19,7 +21,7 @@ export const lesson = writable({
export const isLessonDirty = writable(false); export const isLessonDirty = writable(false);
export function handleAddLesson() { export function handleAddLesson() {
lessons.update((_lessons) => { lessons.update(((_lessons) => {
return [ return [
..._lessons, ..._lessons,
{ {
@@ -31,10 +33,10 @@ export function handleAddLesson() {
is_complete: false, is_complete: false,
}, },
]; ];
}); }) as Updater<any>);
} }
export function handleDelete(lessonId) { export function handleDelete(lessonId: Lesson['id']) {
return async () => { return async () => {
// Need to implement soft delete // Need to implement soft delete
if (lessonId) { if (lessonId) {
@@ -52,18 +54,21 @@ export function handleDelete(lessonId) {
}; };
} }
export async function handleSaveLesson(lesson, course_id) { export async function handleSaveLesson(
lesson: Lesson,
course_id: Course['id']
) {
console.log(`handleSaveLesson lesson`, lesson); console.log(`handleSaveLesson lesson`, lesson);
const newLesson = { const newLesson = {
title: lesson.title, title: lesson.title,
lesson_at: lesson.lesson_at, lesson_at: lesson.lesson_at,
call_url: lesson.call_url, call_url: lesson.call_url,
teacher_id: lesson.profile ? lesson.profile.id : null, teacher_id: lesson.profile ? lesson.profile.id : undefined,
course_id, course_id,
is_complete: lesson.is_complete, is_complete: lesson.is_complete,
}; };
let updatedLesson; let updatedLesson: any[] | null = null;
if (typeof lesson.id === 'string') { if (typeof lesson.id === 'string') {
// No need to get the result of update cause we have all in local state // No need to get the result of update cause we have all in local state
@@ -85,7 +90,10 @@ export async function handleSaveLesson(lesson, course_id) {
); );
} }
export async function handleUpdateLessonMaterials(lesson, lessonId) { export async function handleUpdateLessonMaterials(
lesson: any,
lessonId: Lesson['id']
) {
const materials = { const materials = {
...lesson.materials, ...lesson.materials,
}; };
@@ -171,7 +171,7 @@
<a <a
class="item flex items-center {( class="item flex items-center {(
path || $page.path path || $page.path
).includes(item.id) && 'active'} pl-7 py-2 {isStudent && ).includes(item.id) && 'active'} pl-7 py-3 {isStudent &&
!item.is_complete !item.is_complete
? 'cursor-not-allowed' ? 'cursor-not-allowed'
: ''}" : ''}"
@@ -27,13 +27,13 @@
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No" label="No"
onClick={() => ($deleteMemberModal.open = false)} onClick={() => ($deleteMemberModal.open = false)}
/> />
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="Yes" label="Yes"
onClick={handleDelete} onClick={handleDelete}
@@ -9,7 +9,7 @@
} from '../../../../utils/services/courses'; } from '../../../../utils/services/courses';
import { invitationModal, resetForm } from './store'; import { invitationModal, resetForm } from './store';
import { validateForm } from '../../../Courses/functions'; import { validateForm } from '../../../Courses/functions';
import { course, setCourseData } from '../../../Course/store'; import { course, setCourse } from '../../store';
let errors = { let errors = {
emails: '', emails: '',
@@ -47,7 +47,7 @@
addGroupMember(membersStack).then(async (membersAdded) => { addGroupMember(membersStack).then(async (membersAdded) => {
const group = await fetchGroup($course.group.id); const group = await fetchGroup($course.group.id);
setCourseData( setCourse(
{ {
...$course, ...$course,
group: group.data, group: group.data,
@@ -90,7 +90,7 @@
/> />
<div class="mt-5 flex items-center"> <div class="mt-5 flex items-center">
<PrimaryButton className="px-6 py-2" label="Finish" type="submit" /> <PrimaryButton className="px-6 py-3" label="Finish" type="submit" />
</div> </div>
</form> </form>
</Modal> </Modal>
@@ -29,8 +29,7 @@
} }
$settingsDialog.open = false; $settingsDialog.open = false;
const logo = await updateCourse($course.id, { const logo = await updateCourse($course.id, avatar, {
avatar,
title: $course.title, title: $course.title,
description: $course.description, description: $course.description,
}); });
@@ -67,13 +66,13 @@
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No, cancel" label="No, cancel"
type="submit" type="submit"
/> />
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="Yes, delete" label="Yes, delete"
onClick={handleDelete} onClick={handleDelete}
@@ -103,12 +102,12 @@
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER} variant={VARIANTS.CONTAINED_DANGER}
label="Delete" label="Delete"
onClick={() => (shouldDelete = true)} onClick={() => (shouldDelete = true)}
/> />
<PrimaryButton className="px-6 py-2" label="Update" type="submit" /> <PrimaryButton className="px-6 py-3" label="Update" type="submit" />
</div> </div>
</form> </form>
{/if} {/if}
+3 -3
View File
@@ -26,12 +26,12 @@ export function getLessonsRoute(courseId, lessonId) {
return `${path}/${lessonId}`; return `${path}/${lessonId}`;
} }
export function getLectureNo(index) { export function getLectureNo(index, initNo = '00') {
if (index <= 9) { if (index <= 9) {
return `00${index}`; return `${initNo}${index}`;
} }
return `0${index}`; return `${initNo.replace('0', '')}${index}`;
} }
export function formatAnswers(data) { export function formatAnswers(data) {
+1 -1
View File
@@ -6,7 +6,7 @@
import PrimaryButton from '../PrimaryButton/index.svelte'; import PrimaryButton from '../PrimaryButton/index.svelte';
import PageBody from '../PageBody/index.svelte'; import PageBody from '../PageBody/index.svelte';
import EditContent from '../EditContent/index.svelte'; import EditContent from '../EditContent/index.svelte';
import { course } from '../Course/store'; import { course } from './store';
import { updateCourse } from '../../utils/services/courses'; import { updateCourse } from '../../utils/services/courses';
let mode = MODES.view; let mode = MODES.view;
@@ -1,8 +1,35 @@
import { writable } from 'svelte/store'; import { writable, Writable } from 'svelte/store';
import { lessons } from './components/Lesson/store/lessons'; import { lessons } from './components/Lesson/store/lessons';
//@ts-ignore
import { ROLE } from '../../utils/constants/roles'; import { ROLE } from '../../utils/constants/roles';
import type { Course } from '../../utils/types';
export const course = writable({ id: '' }); export const course: Writable<Course> = writable({
id: '',
title: '',
description: '',
cost: 0,
currency: '',
metadata: {
requirements: '',
description: '',
goals: '',
videoUrl: '',
showDiscount: false,
discount: 0,
reward: {
show: false,
description: '',
},
instructor: {
name: '',
role: '',
coursesNo: 0,
description: '',
imgUrl: '',
},
},
});
export const mockGroupMember = { export const mockGroupMember = {
id: '434534534535', id: '434534534535',
@@ -27,7 +54,7 @@ export const group = writable({
people: [], people: [],
}); });
export async function setCourseData(data, setLesson = true) { export async function setCourse(data: Course, setLesson = true) {
if (!data || !(Object.values(data) && Object.values(data).length)) return; if (!data || !(Object.values(data) && Object.values(data).length)) return;
// const tutorsById = {}; // const tutorsById = {};
@@ -38,27 +65,37 @@ export async function setCourseData(data, setLesson = true) {
people: [], people: [],
}); });
// @ts-ignore
if (Array.isArray(groupData.members)) { if (Array.isArray(groupData.members)) {
// @ts-ignore
for (const member of groupData.members) { for (const member of groupData.members) {
if (member.role_id === ROLE.STUDENT) { if (member.role_id === ROLE.STUDENT) {
// @ts-ignore
groupData.students.push(member); groupData.students.push(member);
} else if (member.profile) { } else if (member.profile) {
// @ts-ignore
groupData.tutors.push(member.profile); groupData.tutors.push(member.profile);
// tutorsById[member.profile.id] = member.profile; // tutorsById[member.profile.id] = member.profile;
} }
} }
// @ts-ignore
groupData.people = groupData.members; groupData.people = groupData.members;
} }
// @ts-ignore
delete groupData.members; delete groupData.members;
// @ts-ignore
group.set(groupData); group.set(groupData);
} }
if (setLesson) { if (setLesson) {
// @ts-ignore
const orderedLessons = (data.lessons || []) const orderedLessons = (data.lessons || [])
// @ts-ignore
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)) .sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
// @ts-ignore
.sort((a, b) => a.order - b.order); .sort((a, b) => a.order - b.order);
// .map((lesson) => ({ // .map((lesson) => ({
// ...lesson, // ...lesson,
@@ -67,6 +104,29 @@ export async function setCourseData(data, setLesson = true) {
lessons.set(orderedLessons); lessons.set(orderedLessons);
} }
//@ts-ignore
delete data.lessons; delete data.lessons;
if (data.metadata && !Object.values(data.metadata)) {
data.metadata = {
requirements: '',
description: '',
goals: '',
videoUrl: '',
showDiscount: false,
discount: 0,
reward: {
show: false,
description: '',
},
instructor: {
name: '',
role: '',
coursesNo: 0,
description: '',
imgUrl: '',
},
};
}
course.set(data); course.set(data);
} }
+2 -2
View File
@@ -1,5 +1,5 @@
<script> <script>
import { Firework } from 'svelte-loading-spinners'; import { Moon } from 'svelte-loading-spinners';
import Navigation from '../Course/components/Navigation/index.svelte'; import Navigation from '../Course/components/Navigation/index.svelte';
import Backdrop from '../../components/Backdrop/index.svelte'; import Backdrop from '../../components/Backdrop/index.svelte';
// import { lessons, lesson } from '../Course/components/Lesson/store/lessons'; // import { lessons, lesson } from '../Course/components/Lesson/store/lessons';
@@ -32,7 +32,7 @@
{#if isFetching} {#if isFetching}
<Backdrop> <Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" /> <Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop> </Backdrop>
{/if} {/if}
@@ -1,19 +0,0 @@
<script>
import TextField from '../../../Form/TextField.svelte';
import EditContent from '../../../EditContent/index.svelte';
import { landingPageStore } from '../../store';
</script>
<TextField
label="Company name"
bind:value={$landingPageStore.aboutUs.companyName}
className="mt-3 font-bold"
/>
<TextField
label="Company Logo URL"
bind:value={$landingPageStore.aboutUs.logoUrl}
className="mt-3 font-bold"
/>
<p class="mt-3 font-bold">Description</p>
<EditContent bind:value={$landingPageStore.aboutUs.description} />
@@ -4,14 +4,14 @@
import { isMobile } from '../../../../utils/store/useMobile'; import { isMobile } from '../../../../utils/store/useMobile';
import IconButton from '../../../IconButton/index.svelte'; import IconButton from '../../../IconButton/index.svelte';
let show = true; export let show = true;
</script> </script>
<section <section
class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}" class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}"
> >
{#if show} {#if show}
<div class="relative h-full bg-gray-100"> <div class="relative h-full bg-white">
<slot /> <slot />
</div> </div>
{/if} {/if}
@@ -46,6 +46,7 @@
min-width: 300px; min-width: 300px;
max-width: 300px; max-width: 300px;
top: 0; top: 0;
left: 0;
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
background-color: rgb(250, 251, 252); background-color: rgb(250, 251, 252);
@@ -1,6 +1,17 @@
<script> <script>
import EditContent from '../../../EditContent/index.svelte'; import { set, get, cloneDeep } from 'lodash';
import { landingPageStore } from '../../store'; import TextEditor from '../../../TextEditor/index.svelte';
export let course;
</script> </script>
<EditContent bind:value={$landingPageStore.description.content} /> <div class="h-2/5">
<TextEditor
content={get(course, 'metadata.description')}
onChange={(html) => {
const _course = cloneDeep(course);
set(_course, 'metadata.description', html);
course = _course;
}}
/>
</div>
@@ -0,0 +1,17 @@
<script>
import { set, get, cloneDeep } from 'lodash';
import TextEditor from '../../../TextEditor/index.svelte';
export let course;
</script>
<div class="h-2/5">
<TextEditor
content={get(course, 'metadata.goals')}
onChange={(html) => {
const _course = cloneDeep(course);
set(_course, 'metadata.goals', html);
course = _course;
}}
/>
</div>
@@ -1,38 +1,31 @@
<script> <script>
import TextArea from '../../../Form/TextArea.svelte'; import TextArea from '../../../Form/TextArea.svelte';
import TextField from '../../../Form/TextField.svelte'; import TextField from '../../../Form/TextField.svelte';
import { landingPageStore } from '../../store';
export let course = {};
</script> </script>
<TextArea <TextField
className="mt-5"
labelClassName="font-bold"
label="Title" label="Title"
bind:value={$landingPageStore.header.title} bind:value={course.title}
rows="3"
className="mt-3 font-bold"
bgColor="bg-white"
/> />
<TextArea <TextArea
label="Description" label="Description"
bind:value={$landingPageStore.header.description} bind:value={course.description}
rows="6" rows="6"
className="mt-3 font-bold" className="mt-5"
bgColor="bg-white" labelClassName="font-bold"
/> />
<TextField <TextField
className="mt-3 font-bold" className="mt-5"
label="Currency" labelClassName="font-bold"
bind:value={$landingPageStore.header.currency} label="Short Video"
/> placeholder="www.youtube.com/watch?v=uYRq60G5XTk"
helperMessage="Enter a link to a youtube video"
<TextField type="text"
className="mt-3 font-bold" bind:value={course.metadata.videoUrl}
label="Cost"
bind:value={$landingPageStore.header.cost}
/>
<TextField
label="Button Label"
className="mt-3 font-bold"
bind:value={$landingPageStore.header.buttonLabel}
/> />
@@ -0,0 +1,90 @@
<script>
import { set, cloneDeep, isEmpty, get } from 'lodash';
import TextArea from '../../../Form/TextArea.svelte';
import TextField from '../../../Form/TextField.svelte';
import UploadImage from '../../../UploadImage/index.svelte';
import { uploadAvatar } from '../../../../utils/services/courses';
export let course = {};
let name;
let role;
let imgUrl;
let description;
let courseNo;
let avatar;
let hasBeenSet = false;
function setter(value, setterKey) {
if (!value) return;
const _course = cloneDeep(course);
set(_course, setterKey, value);
course = _course;
}
async function onAvatarChange(_avatar) {
if (!_avatar) return;
const logo = await uploadAvatar(course.id, _avatar);
imgUrl = logo;
}
function setDefaults(course) {
if (isEmpty(course) || hasBeenSet) return;
hasBeenSet = true;
name = get(course, 'metadata.instructor.name');
role = get(course, 'metadata.instructor.role');
imgUrl = get(course, 'metadata.instructor.imgUrl');
description = get(course, 'metadata.instructor.description');
courseNo = get(course, 'metadata.instructor.courseNo');
}
$: setter(name, 'metadata.instructor.name');
$: setter(role, 'metadata.instructor.role');
$: setter(imgUrl, 'metadata.instructor.imgUrl');
$: setter(description, 'metadata.instructor.description');
$: setter(courseNo, 'metadata.instructor.courseNo');
$: onAvatarChange(avatar);
$: setDefaults(course);
</script>
<div class="mt-5">
<label for="upload" class="font-bold">Instructor Logo</label>
<UploadImage bind:avatar src={imgUrl} />
</div>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Instructor name"
placeholder="Your company name"
bind:value={name}
/>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Instructor Role"
placeholder="e.g Software developer"
bind:value={role}
/>
<TextArea
label="About instructor"
placeholder="A short background about the instructor"
rows="6"
className="mt-5"
labelClassName="font-bold"
bind:value={description}
/>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Total number of courses"
type="number"
bind:value={courseNo}
/>
@@ -0,0 +1,97 @@
<script>
import { set, isEmpty, get, cloneDeep } from 'lodash';
import { Toggle } from 'carbon-components-svelte';
import TextEditor from '../../../TextEditor/index.svelte';
import TextField from '../../../Form/TextField.svelte';
export let course = {};
let discount = 0;
let showDiscount;
let giftToggled;
let hasBeenSet = false;
function setDefaults(course) {
console.log('\n\nsetDefaults course', course);
if (isEmpty(course) || hasBeenSet) return;
hasBeenSet = true;
discount = get(course, 'metadata.discount', 0);
showDiscount = get(course, 'metadata.showDiscount', false);
giftToggled = get(course, 'metadata.reward.show', false);
}
function setter(value, setterKey) {
console.log('\n\nsetter course', value);
if (typeof value !== 'boolean' && !value) return;
const _course = cloneDeep(course);
set(_course, setterKey, value);
course = _course;
}
$: setter(showDiscount, 'metadata.showDiscount');
$: setter(discount, 'metadata.discount');
$: setter(giftToggled, 'metadata.reward.show');
$: setDefaults(course);
</script>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Currency"
value={course.currency}
isDisabled={true}
/>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Cost"
type="number"
bind:value={course.cost}
/>
<div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Discount" bind:toggled={showDiscount}>
<span slot="labelA">No</span>
<span slot="labelB">Yes</span>
</Toggle>
</div>
{#if showDiscount}
<TextField
className="mt-5"
labelClassName="font-bold"
label="Discout Percent"
type="number"
bind:value={discount}
helperMessage="In Percentage %"
/>
{/if}
<div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Gift on Completion" bind:toggled={giftToggled}>
<span slot="labelA">No</span>
<span slot="labelB">Yes</span>
</Toggle>
</div>
{#if giftToggled}
<p class="font-bold mt-5">About Gift</p>
<div class="h-2/5">
<TextEditor
content={get(course, 'metadata.reward.description')}
onChange={(html) => {
const _course = cloneDeep(course);
set(_course, 'metadata.reward.description', html);
course = _course;
}}
/>
</div>
{/if}
@@ -1,6 +1,17 @@
<script> <script>
import EditContent from '../../../EditContent/index.svelte'; import { set, get, cloneDeep } from 'lodash';
import { landingPageStore } from '../../store'; import TextEditor from '../../../TextEditor/index.svelte';
export let course;
</script> </script>
<EditContent bind:value={$landingPageStore.requirement.content} /> <div class="h-2/5">
<TextEditor
content={get(course, 'metadata.requirements')}
onChange={(html) => {
const _course = cloneDeep(course);
set(_course, 'metadata.requirements', html);
course = _course;
}}
/>
</div>
@@ -1,61 +0,0 @@
<script>
import TextField from '../../../Form/TextField.svelte';
import PrimaryButton from '../../../PrimaryButton/index.svelte';
import CloseButton from '../../../Buttons/Close/index.svelte';
import { VARIANTS } from '../../../PrimaryButton/constants';
import { landingPageStore } from '../../store';
function handleAdd() {
landingPageStore.update((store) => {
const { syllabus } = store;
const lessons = [
...syllabus.lessons,
{
title: '',
},
];
return {
...store,
syllabus: {
lessons,
},
};
});
}
function handleDelete(index) {
landingPageStore.update((store) => {
const { syllabus } = store;
return {
...store,
syllabus: {
lessons: syllabus.lessons.filter(
(lesson, lessonIndex) => index !== lessonIndex
),
},
};
});
}
</script>
<h4 class="mb-3">Lessons</h4>
{#each $landingPageStore.syllabus.lessons as lesson, index}
<div class="mb-4 relative">
<TextField bind:value={lesson.title} />
<div class="absolute -top-3 -right-3">
<CloseButton
onClick={() => handleDelete(index)}
contained={true}
size="small"
/>
</div>
</div>
{/each}
<div class="mt-3">
<PrimaryButton label="Add" onClick={handleAdd} variant={VARIANTS.OUTLINED} />
</div>
@@ -8,13 +8,18 @@
import HeaderForm from './HeaderForm.svelte'; import HeaderForm from './HeaderForm.svelte';
import RequirementForm from './RequirementForm.svelte'; import RequirementForm from './RequirementForm.svelte';
import DescriptionForm from './DescriptionForm.svelte'; import DescriptionForm from './DescriptionForm.svelte';
import SyllabusForm from './SyllabusForm.svelte'; import PricingForm from './PricingForm.svelte';
import AboutUsForm from './AboutUsForm.svelte'; import GoalsForm from './GoalsForm.svelte';
import InstructorForm from './InstructorForm.svelte';
import PrimaryButton from '../../../PrimaryButton/index.svelte'; import PrimaryButton from '../../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../../PrimaryButton/constants';
import { updateCourse } from '../../../../utils/services/courses';
export let course;
export let courseId; export let courseId;
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300'; let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
let loading = false;
const sections = [ const sections = [
{ {
@@ -31,11 +36,15 @@
}, },
{ {
key: 4, key: 4,
title: 'Syllabus', title: 'Goals',
}, },
{ {
key: 5, key: 5,
title: 'About Us', title: 'Instructor',
},
{
key: 6,
title: 'Pricing',
}, },
]; ];
let selectedSection = null; let selectedSection = null;
@@ -55,29 +64,66 @@
}; };
} }
function handlePublish() {} function generateSlug(title) {
return title
.replaceAll(' ', '-')
.toLowerCase()
.concat(`-${new Date().getTime()}`);
}
async function handleSave() {
loading = true;
course.slug = course.slug || generateSlug(course.title);
await updateCourse(courseId, undefined, {
...course,
attendance: undefined,
group: undefined,
lessons: undefined,
slug: course.slug,
});
loading = false;
return `${window.location.origin}/course/${course.slug}`;
}
/**
* 1. Use all you have for editing now
* 2. Make course one interface we might need to computation
*/
async function handlePreview() {
const link = `${window.location.origin}/course/${course.slug}`;
window.open(link, '_blank');
}
</script> </script>
<Container> <Container show={true}>
<!-- Publish Header Section -->
{#if !selectedSection} {#if !selectedSection}
<div <div class="flex justify-between items-center px-2 w-full">
class="flex justify-between items-center px-2 {borderBottomGrey} w-full"
>
<CloseButton onClick={handleClose} /> <CloseButton onClick={handleClose} />
<PrimaryButton <div class="flex items-center">
label="Publish" <PrimaryButton
onClick={handlePublish} label="Save"
disablePadding={true} type="button"
/> className="mr-1"
variant={VARIANTS.OUTLINED}
onClick={handleSave}
isLoading={loading}
/>
<PrimaryButton
label="Preview"
type="button"
onClick={handlePreview}
isDisabled={loading || !course.slug}
/>
</div>
</div> </div>
<div <div class="flex justify-between items-center px-2 w-full mb-2">
class="flex justify-between items-center px-2 bg-white {borderBottomGrey} w-full mb-2"
>
<h3 class="">Page Builder</h3> <h3 class="">Page Builder</h3>
</div> </div>
{#each sections as section} {#each sections as section}
<button <button
class="w-full flex items-center justify-between px-2 py-3 bg-white border border-b-0 border-gray-300" class="w-full flex items-center justify-between px-2 py-3 border border-b-0 border-gray-300"
on:click={handleSectionSelect(section.key)} on:click={handleSectionSelect(section.key)}
> >
<p class="mr-2">{section.title} section</p> <p class="mr-2">{section.title} section</p>
@@ -85,24 +131,27 @@
</button> </button>
{/each} {/each}
{:else} {:else}
<div class="flex items-center bg-white {borderBottomGrey} w-full"> <!-- Title -->
<div class="flex items-center {borderBottomGrey} w-full">
<IconButton onClick={handleClose}> <IconButton onClick={handleClose}>
<ArrowLeft24 class="carbon-icon" title="Go back" /> <ArrowLeft24 class="carbon-icon" title="Go back" />
</IconButton> </IconButton>
<h3 class="">{selectedSection.title}</h3> <h3 class="">{selectedSection.title}</h3>
</div> </div>
<div class="p-2"> <div class="p-2 h-4/5 overflow-y">
{#if selectedSection.key === 1} {#if selectedSection.key === 1}
<HeaderForm /> <HeaderForm bind:course />
{:else if selectedSection.key === 2} {:else if selectedSection.key === 2}
<RequirementForm /> <RequirementForm bind:course />
{:else if selectedSection.key === 3} {:else if selectedSection.key === 3}
<DescriptionForm /> <DescriptionForm bind:course />
{:else if selectedSection.key === 4} {:else if selectedSection.key === 4}
<SyllabusForm /> <GoalsForm bind:course />
{:else if selectedSection.key === 5} {:else if selectedSection.key === 5}
<AboutUsForm /> <InstructorForm bind:course />
{:else if selectedSection.key === 6}
<PricingForm bind:course />
{/if} {/if}
</div> </div>
{/if} {/if}
@@ -0,0 +1,147 @@
<script lang="ts">
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import type { Course } from '../../../utils/types';
import { get } from 'lodash';
export let className = '';
export let courseData: Course = {
id: '',
title: '',
description: '',
};
let calculatedCost = 0;
let discount = 0;
let formatter: Intl.NumberFormat | undefined;
function calcDisc(percent: number, cost: number) {
if (!percent) return cost;
return Math.round((percent / 100) * cost) || cost;
}
function addToCart() {}
function buyNow() {}
function setFormatter(currency: string | undefined) {
if (!currency) return;
formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
});
}
$: setFormatter(courseData.currency);
$: discount = get(courseData, 'metadata.discount', 0);
$: calculatedCost = calcDisc(discount, courseData.cost || 0);
</script>
<!-- Pricing Details -->
<aside
class="{className} price-container md:sticky md:top-0 md:shadow-2xl md:rounded-lg m-h-fit"
>
<div class="p-10">
<!-- Pricing -->
<div class="mb-6">
<p class="font-medium text-lg">
{formatter?.format(calculatedCost) || calculatedCost}
</p>
{#if courseData?.metadata?.showDiscount}
<p class="font-light text-sm text-gray-500">
{discount}% Discount.
<span class="line-through"
>{formatter?.format(courseData?.cost || 0) || courseData.cost}</span
>
</p>
{/if}
</div>
<!-- Call To Action Buttons -->
<div class="flex flex-col w-full items-center">
<PrimaryButton
label="Add to Cart"
className="w-full sm:w-full py-3 mb-3"
onClick={addToCart}
/>
<PrimaryButton
label="Buy Now"
className="w-full sm:w-full py-3 mb-3"
variant={VARIANTS.OUTLINED}
onClick={addToCart}
/>
<p class="font-light text-sm text-gray-500">Early bird offer. Buy ASAP</p>
</div>
</div>
<!-- Gift Container -->
{#if courseData?.metadata?.reward?.show}
<div
class="p-10 flex items-center flex-col border-t border-b border-gray-300"
>
{@html get(courseData, 'metadata.reward.description', '')}
</div>
{/if}
</aside>
<style lang="scss">
.banner {
background-color: #040f2d;
min-height: 472px;
}
.author {
color: #7888b7;
}
.banner-image {
max-width: 559px;
}
.backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
.active {
position: relative;
display: inline-block;
}
.active::after {
position: absolute;
content: '';
width: 100%;
height: 3px;
background-color: var(--main-primary-color);
display: block;
bottom: -14px;
left: 0px;
}
.price-container {
width: 405px;
min-width: 330px;
height: fit-content;
}
@media (max-width: 768px) {
.price-container {
width: 100%;
min-width: unset;
}
}
.course-content {
max-width: 608px;
}
:global(.list ul li) {
margin-left: 1rem;
list-style-type: disc;
}
:global(.plyr) {
width: 100% !important;
}
</style>
+13 -8
View File
@@ -1,9 +1,10 @@
export const SECTION_KEYS = { export const SECTION_KEYS = {
HEADER: 'header', HEADER: 'header',
REQUIREMENT: 'requirement', REQUIREMENT: '#requirement',
DESCRIPTION: 'description', DESCRIPTION: '#description',
SYLLABUS: 'syllabus', GOALS: '#goals',
ABOUT_US: 'about-us', LESSONS: '#lessons',
INSTRUCTOR: '#instructor',
}; };
export const NAV_ITEMS = [ export const NAV_ITEMS = [
@@ -16,11 +17,15 @@ export const NAV_ITEMS = [
label: 'Description', label: 'Description',
}, },
{ {
key: SECTION_KEYS.SYLLABUS, key: SECTION_KEYS.GOALS,
label: 'Syllabus', label: 'Goals',
}, },
{ {
key: SECTION_KEYS.ABOUT_US, key: SECTION_KEYS.LESSONS,
label: 'About Us', label: 'Lessons',
},
{
key: SECTION_KEYS.INSTRUCTOR,
label: 'Instructor',
}, },
]; ];
+309 -125
View File
@@ -1,163 +1,347 @@
<script> <script lang="ts">
// import paystack from 'paystack';
import get from 'lodash/get';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import RecentlyViewed16 from 'carbon-icons-svelte/lib/RecentlyViewed16'; import pluralize from 'pluralize';
import CurrencyDollar16 from 'carbon-icons-svelte/lib/CurrencyDollar16';
import Alarm16 from 'carbon-icons-svelte/lib/Alarm16';
import { stores } from '@sapper/app'; import { stores } from '@sapper/app';
import Expandable from '../Expandable/index.svelte'; import Notebook from 'carbon-icons-svelte/lib/Notebook16/Notebook16.svelte'; //note
import PresentationFile from 'carbon-icons-svelte/lib/PresentationFile16/PresentationFile16.svelte'; // exercise
import Video from 'carbon-icons-svelte/lib/Video16/Video16.svelte'; //video
import PageNumber from 'carbon-icons-svelte/lib/PageNumber16/PageNumber16.svelte';
import PlayFilled from 'carbon-icons-svelte/lib/PlayFilled16/PlayFilled16.svelte';
import PrimaryButton from '../PrimaryButton/index.svelte'; import PrimaryButton from '../PrimaryButton/index.svelte';
import MarkdownRender from '../MarkdownRender/index.svelte'; import PricingSection from './components/PricingSection.svelte';
import { NAV_ITEMS, SECTION_KEYS } from './constants'; import { getLectureNo } from '../Course/function';
import { landingPageStore } from './store'; import { NAV_ITEMS } from './constants';
import Chip from '../Chip/index.svelte';
import { getEmbedId } from '../../utils/functions/formatYoutubeVideo';
import type { Course, Lesson } from '../../utils/types';
let activeNavItem = ''; export let courseData: Course = {
id: '',
title: '',
description: '',
};
let lessons: Array<Lesson> = [];
let player: any;
const expandableRootClass =
'max-w-4xl pt-5 my-3 border-r-0 border-b border-l-0 border-gray-300';
const { page } = stores(); const { page } = stores();
// const paystackApi = paystack('');
function handleEnrol() {} let activeNav = NAV_ITEMS[0].key;
function handleNavItemClick(navItem) { let instructor = {};
return () => { let video: string | undefined;
activeNavItem = navItem;
}; function playVideo() {
console.log('open modal with video');
}
function locationHashChanged() {
activeNav = window.location.hash;
}
function addToCart() {}
function buyNow() {
// if (courseData.cost) {
// paystackApi.tra;
// } else {
// // Course is free
// }
}
function initPlyr(_player: any, _video: string | undefined) {
if (!player) return;
// @ts-ignore
const plyr = new Plyr('#player', {});
// @ts-ignore
window.player = plyr;
} }
onMount(() => { onMount(() => {
activeNavItem = (window.location.hash || NAV_ITEMS[0].key).replace('#', ''); window.onhashchange = locationHashChanged;
}); });
$: video = get(courseData, 'metadata.videoUrl');
$: lessons = get(courseData, 'lessons', []);
$: instructor = courseData?.metadata?.instructor || {};
$: initPlyr(player, video);
</script> </script>
<svelte:head> <svelte:head>
<title>{$landingPageStore.header.title}</title> <title>{get(courseData, 'title', '')}</title>
</svelte:head> </svelte:head>
<div class="w-full bg-white flex flex-col items-center"> <div class="w-full bg-white flex flex-col items-center">
<section <!-- Header Section -->
class="w-full flex items-center justify-center bg-gray-900 text-white p-6 pb-10" <header class="banner w-full flex items-center justify-center p-">
> <div
<div class="container flex justify-between items-center"> class="md:w-5/6 w-full flex items-center justify-between flex-col-reverse md:flex-row"
<div class="course-description mr-3"> >
<h1> <!-- Course Description -->
{$landingPageStore.header.title} <div class="md:w-2/5 w-11/12 py-10">
<h1 class="text-5xl text-white my-4 font-bold">
{get(courseData, 'title', '')}
</h1> </h1>
<h5> <p class="text-md text-white mb-6">
{$landingPageStore.header.description} {get(courseData, 'description', '')}
</h5>
<div>
<span class="flex items-center">
<RecentlyViewed16 class="carbon-icon mr-2" />
Last updated 10/2021
</span>
</div>
</div>
<div class="action-container bg-white p-3 flex items-start flex-col">
<p class="mb-2 text-black text-md flex items-center">
<Alarm16 class="carbon-icon active mr-2" />
{$landingPageStore.header.duration}
</p> </p>
<p class="mb-2 text-black text-md flex items-center">
<CurrencyDollar16 class="carbon-icon active mr-2" /> <p class="author my-3 text-sm">
{$landingPageStore.header.currency}{$landingPageStore.header.cost} {get(courseData, 'metadata.instructor.name', '')}
</p> </p>
<PrimaryButton <PrimaryButton
label={$landingPageStore.header.buttonLabel} label="Start Course"
onClick={handleEnrol} className="px-6 py-5 mt-6 sm:w-fit"
className="md:w-full"
/> />
</div> </div>
<!-- Banner Image getEmbedId(videoUrl) -->
{#if video}
<div class="banner-image md:w-2/3 w-full flex">
<div
bind:this={player}
id="player"
data-plyr-provider="youtube"
data-plyr-embed-id={getEmbedId(video)}
/>
</div>
<!-- <div class="container">
<div
bind:this={player}
id="player"
class="banner-image w-2/3 h-96 relative"
data-plyr-provider="youtube"
data-plyr-embed-id="bTqVqk7FSmY"
/>
</div> -->
{:else}
<!-- <div
class="banner-image w-2/3 h-96 relative cursor-pointer"
on:click={playVideo}
>
<div
aria-hidden="true"
class="absolute top-0 left-0 backdrop w-full h-full"
/>
<img
src="/images/course-banner-image.png"
alt="banner"
class="w-full h-full"
/>
<div
class="absolute top-0 left-0 w-full h-full z-50 flex items-center justify-center"
>
<img src="/images/play_video.png" alt="play video" />
</div>
</div> -->
{/if}
</div> </div>
</section> </header>
<PricingSection className="md:hidden" {courseData} />
<!-- Body -->
<div class="bg-white w-full">
<div class="py-8 lg:w-10/12 w-full m-2 lg:m-auto flex justify-between">
<!-- Course Details -->
<div class="course-content w-full p-3 md:w-9/12 md:mr-14">
<!-- Navigation -->
<nav class="flex items-center border-b border-gray-300 pb-3">
{#each NAV_ITEMS as navItem}
<a
href="{$page.path}{navItem.key}"
class="{navItem.key === activeNav &&
'active text-blue-700'} rounded-lg px-2 mr-6 text-slate-700 font-normal hover:bg-slate-100 hover:text-slate-900"
>
{navItem.label}
</a>
{/each}
</nav>
<nav <!-- Sections - Requirement -->
class="w-full z-10 max-w-4xl flex items-center justify-evenly py-4 px-4 bg-gray-100 mt-4 sticky top-0" <section id="requirement" class="border-b border-gray-300 mt-8 pb-10">
> <h3 class="text-2xl font-bold mt-0 mb-3">Requirement</h3>
{#each NAV_ITEMS as navItem} <ul class="list font-light">
<p {@html get(courseData, 'metadata.requirements', '')}
class="text-md text-gray-900 {activeNavItem === navItem.key && </ul>
'font-bold'}" </section>
on:click={handleNavItemClick(navItem.key)}
>
<a href="{$page.path}#{navItem.key}">{navItem.label}</a>
</p>
{/each}
</nav>
<Expandable <!-- Sections - Course Description -->
id={SECTION_KEYS.REQUIREMENT} <section id="description" class="border-b border-gray-300 mt-8 pb-10">
title="Requirement" <h3 class="text-2xl font-bold mt-0 mb-3">Course Description</h3>
titleClass="text-2xl" <p class="text-sm font-light">
rootClass={expandableRootClass} {@html get(courseData, 'metadata.description', '')}
supportsLink={true} </p>
> </section>
<MarkdownRender
content={$landingPageStore.requirement.content}
disableMaxWidth={true}
/>
</Expandable>
<Expandable <!-- Sections - Goal -->
id={SECTION_KEYS.DESCRIPTION} <section id="goals" class="mt-8 pb-10">
title="Description" <h3 class="text-2xl font-bold mt-0 mb-3">What you'll learn</h3>
titleClass="text-2xl" <ul class="list font-light">
rootClass={expandableRootClass} {@html get(courseData, 'metadata.goals', '')}
supportsLink={true} </ul>
> </section>
<MarkdownRender
content={$landingPageStore.description.content}
disableMaxWidth={true}
/>
</Expandable>
<Expandable <!-- Sections - Certificate -->
id={SECTION_KEYS.SYLLABUS} <section class="border-b border-gray-300 mt-8 pb-10">
title="Syllabus" <h3 class="text-2xl font-bold mt-0">Certificate</h3>
titleClass="text-2xl" <p class="text-sm font-light mb-3">
rootClass={expandableRootClass} When you complete all of the courses in the program, you'll earn a
supportsLink={true} Certificate to share with your professional network.
> </p>
{#each $landingPageStore.syllabus.lessons as lesson, index} <img
<div class="flex items-center my-2"> src="/images/certificate-template.svg"
<strong class="mr-2">Lesson {index + 1}. </strong> alt="certificate template"
{lesson.title} />
</section>
<!-- Sections - Lessons -->
<section id="lessons" class="border-b border-gray-300 mt-8 pb-10">
<div class="flex items-center justify-between w-full mb-3">
<h3 class="text-2xl font-bold mt-0 mb-3">Course Content</h3>
<p class="text-sm font-light">
<!-- {lessons.length} lessons -->
{pluralize('lesson', lessons.length, true)}
</p>
</div>
{#each lessons as lesson, index}
<div
class="w-full flex md:items-center justify-between mb-6 flex-col md:flex-row"
>
<div class="flex items-center mb-3">
<Chip
value={getLectureNo(index + 1, '0')}
className="bg-blue-100 text-blue-700"
/>
<p class="ml-2 text-sm font-light">{lesson.title}</p>
</div>
<div class="w-2/4">
<div class="flex items-center">
{#if lesson.slide_url}
<span class="text-sm font-light flex w-2/4"
><PresentationFile class="mr-1" />1 slide</span
>
{/if}
{#if lesson.note}
<span class="text-sm font-light flex w-2/4"
><Notebook class="mr-1" />1 note</span
>
{/if}
</div>
<div class="flex items-center">
{#if lesson.video_url}
<span class="text-sm font-light flex w-2/4"
><Video class="mr-1" />1 video</span
>
{/if}
{#if get(lesson, 'totalExercises[0].count')}
<span class="text-sm font-light flex w-2/4"
><PageNumber class="mr-1" />{pluralize(
'exercise',
get(lesson, 'totalExercises[0].count', 0),
true
)}</span
>
{/if}
</div>
</div>
</div>
{/each}
</section>
<!-- Sections - Instructor -->
<section id="instructor" class="mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0 mb-3">Instructor</h3>
<div class="flex items-center mb-4">
<img
alt="Author Avatar"
class="block rounded-full h-20 w-20 mr-3"
src={get(instructor, 'imgUrl', '')}
/>
<div>
<p class="text-md font-light">{get(instructor, 'name', '')}</p>
<p class="text-xs font-light">{get(instructor, 'role', '')}</p>
<p class="text-md font-light flex items-center">
<PlayFilled class="text-blue-700" />
<span class="ml-1"
>{get(instructor, 'courseNo', '')} courses</span
>
</p>
</div>
</div>
<p class="text-sm font-light">
{get(instructor, 'description', '')}
</p>
</section>
</div> </div>
{/each}
</Expandable>
<Expandable <!-- Pricing Details -->
id={SECTION_KEYS.ABOUT_US} <PricingSection className="hidden md:block" {courseData} />
title="About Us - {$landingPageStore.aboutUs.companyName}"
titleClass="text-2xl"
rootClass={expandableRootClass}
supportsLink={true}
>
<div class="w-full">
<img
src={$landingPageStore.aboutUs.logoUrl}
alt={$landingPageStore.aboutUs.companyName}
height="260"
width="260"
class="rounded-full m-auto"
/>
</div> </div>
<MarkdownRender </div>
content={$landingPageStore.aboutUs.description}
disableMaxWidth={true}
/>
</Expandable>
</div> </div>
<style lang="scss"> <style lang="scss">
.container { .banner {
max-width: 800px; background-color: #040f2d;
min-height: 472px;
}
.course-description { .author {
max-width: 500px; color: #7888b7;
} }
.action-container { .banner-image {
height: fit-content; max-width: 559px;
width: 200px; }
}
.backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
.active {
position: relative;
display: inline-block;
}
.active::after {
position: absolute;
content: '';
width: 100%;
height: 3px;
background-color: var(--main-primary-color);
display: block;
bottom: -14px;
left: 0px;
}
.price-container {
width: 405px;
min-width: 330px;
height: fit-content;
position: sticky;
top: 0;
}
.course-content {
max-width: 608px;
}
nav {
overflow: auto;
margin: 0;
overflow-y: hidden;
width: 100%;
}
:global(.list ul li) {
margin-left: 1rem;
list-style-type: disc;
}
:global(.plyr) {
width: 100% !important;
} }
</style> </style>
@@ -35,7 +35,7 @@
</script> </script>
<div <div
class="root w-72 border bg-white px-2 py-2 mr-4 mb-4 rounded-md hover:shadow-2xl transition ease-in-out relative" class="root w-72 border bg-white px-2 py-3 mr-4 mb-4 rounded-md hover:shadow-2xl transition ease-in-out relative"
> >
<Dropdown <Dropdown
options={getOptions()} options={getOptions()}
@@ -57,7 +57,7 @@
<div class="mt-5 flex items-center flex-row-reverse"> <div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
label="Create" label="Create"
type="submit" type="submit"
isLoading={$copyCourseModal.isSaving} isLoading={$copyCourseModal.isSaving}
@@ -128,7 +128,7 @@
/> />
<div class="mt-5 flex items-center"> <div class="mt-5 flex items-center">
<PrimaryButton className="px-6 py-2" label="Finish" type="submit" /> <PrimaryButton className="px-6 py-3" label="Finish" type="submit" />
</div> </div>
</form> </form>
</Modal> </Modal>
+2 -2
View File
@@ -1,6 +1,6 @@
<script> <script>
import Box from '../Box/index.svelte'; import Box from '../Box/index.svelte';
import { Firework } from 'svelte-loading-spinners'; import { Moon } from 'svelte-loading-spinners';
import Card from './components/Card/index.svelte'; import Card from './components/Card/index.svelte';
import CopyCourseModal from './components/CopyCourseModal/index.svelte'; import CopyCourseModal from './components/CopyCourseModal/index.svelte';
import { courses, courseMetaDeta } from './store'; import { courses, courseMetaDeta } from './store';
@@ -13,7 +13,7 @@
> >
{#if $courseMetaDeta.isLoading} {#if $courseMetaDeta.isLoading}
<Box> <Box>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" /> <Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Box> </Box>
{:else} {:else}
{#each $courses as courseData} {#each $courses as courseData}
+3 -3
View File
@@ -8,7 +8,7 @@
let open = false; let open = false;
const notIconClass = const notIconClass =
'border rounded-lg border-grey py-2 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200'; 'border rounded-lg border-grey py-3 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200';
function isOptionLast(index) { function isOptionLast(index) {
return index === options.length - 1; return index === options.length - 1;
@@ -41,11 +41,11 @@
{#if open} {#if open}
<div <div
class="dropdown w-48 bg-white border border-grey rounded-lg mt-2 py-2 absolute" class="dropdown w-48 bg-white border border-grey rounded-lg mt-2 py-3 absolute"
> >
{#each options as option, index} {#each options as option, index}
<button <button
class="block px-4 py-2 {!isOptionLast(index) && 'border-b'} w-full" class="block px-4 py-3 {!isOptionLast(index) && 'border-b'} w-full"
on:click={onClick(option)} on:click={onClick(option)}
> >
{option.label} {option.label}
+9 -6
View File
@@ -1,18 +1,21 @@
<script> <script>
</script> </script>
<style>
/* your styles go here */
</style>
<!-- markup (zero or more items) goes here --> <!-- markup (zero or more items) goes here -->
<div class="my-1"> <div class="my-1">
<hr /> <hr />
<div class="flex justify-center w-full"> <div class="flex justify-center w-full">
<span class="text-gray-700 text-sm py-2">Created with love by <span class="text-gray-700 text-sm py-3"
>Created with love by
<a <a
class="underline text-blue-700" class="underline text-blue-700"
href="https://twitter.com/rotimi_best" href="https://twitter.com/rotimi_best"
target="_blank">Rotimi Best</a></span> target="_blank">Rotimi Best</a
></span
>
</div> </div>
</div> </div>
<style>
/* your styles go here */
</style>
+3 -2
View File
@@ -5,7 +5,8 @@
export let rows = '3'; export let rows = '3';
export let maxRows = 3; export let maxRows = 3;
export let className = ''; export let className = '';
export let bgColor = 'bg-gray-100'; export let labelClassName = '';
export let bgColor = 'bg-gray-200';
export let helperMessage = ''; export let helperMessage = '';
export let errorMessage = ''; export let errorMessage = '';
export let isRequired = false; export let isRequired = false;
@@ -17,7 +18,7 @@
<label class="block w-full relative {className}"> <label class="block w-full relative {className}">
{#if label} {#if label}
<p for="text-field" class=""> <p for="text-field" class={labelClassName}>
{label} {label}
{#if isRequired} {#if isRequired}
+4 -2
View File
@@ -19,6 +19,7 @@
export let helperMessage = ''; export let helperMessage = '';
export let autoComplete = true; export let autoComplete = true;
export let onChange = () => {}; // This is to know if element is 'dirty' export let onChange = () => {}; // This is to know if element is 'dirty'
export let onInputChange = () => {};
let ref; let ref;
let fieldNode; let fieldNode;
@@ -61,10 +62,11 @@
{/if} {/if}
<input <input
use:typeAction use:typeAction
class="form-input p-3 mt-1 block w-full border-none bg-gray-100 {inputClassName} {errorMessage class="form-input p-3 mt-1 block w-full border-none bg-gray-200 {inputClassName} {errorMessage
? 'border-red-500' ? 'border-red-500'
: ''}" : ''}"
on:keydown={onKeyDown} on:keydown={onKeyDown}
on:change={onInputChange}
{placeholder} {placeholder}
bind:value bind:value
bind:this={ref} bind:this={ref}
@@ -92,7 +94,7 @@
<style> <style>
.password-eye { .password-eye {
position: absolute; position: absolute;
top: 33px; top: 23px;
right: 6px; right: 6px;
} }
</style> </style>
+2 -2
View File
@@ -93,13 +93,13 @@
<PrimaryButton <PrimaryButton
label="Login" label="Login"
variant={VARIANTS.NONE} variant={VARIANTS.NONE}
className="py-2 px-6 mx-2 app-background-color" className="py-3 px-6 mx-2 app-background-color"
onClick={() => goto('/login')} onClick={() => goto('/login')}
/> />
<PrimaryButton <PrimaryButton
label="Sign Up" label="Sign Up"
variant={VARIANTS.CONTAINED} variant={VARIANTS.CONTAINED}
className="py-2 px-6 mx-2" className="py-3 px-6 mx-2"
onClick={() => goto('/signup')} onClick={() => goto('/signup')}
/> />
</div> </div>
+4 -4
View File
@@ -13,7 +13,7 @@
export let isLoading = false; export let isLoading = false;
$: variant = isLoading ? VARIANTS.OUTLINED : variant; $: variant = isLoading ? VARIANTS.OUTLINED : variant;
$: isDisabled = isLoading ? true : isDisabled; $: isDisabled = isLoading ? true : false;
</script> </script>
<button <button
@@ -21,15 +21,15 @@
'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'} flex items-center h-auto {VARIANTS_CLASS[ 'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'} flex items-center h-auto {VARIANTS_CLASS[
variant variant
]} {!disablePadding && ]} {!disablePadding &&
'py-1 px-5'} rounded-none {className} py-2 w-full justify-center sm:w-auto {isLoading && 'py-1 px-5'} rounded-none {className} py-3 w-fit justify-center sm:w-auto {isLoading &&
'py-2 border-blue-700'} hover:shadow-xl transition delay-150 duration-300 ease-in-out" 'py-3 border-blue-700'} hover:shadow-xl transition delay-150 duration-300 ease-in-out"
on:click={onClick} on:click={onClick}
{name} {name}
{type} {type}
disabled={isDisabled} disabled={isDisabled}
> >
{#if isLoading} {#if isLoading}
<Moon size="30" color="#1d4ed8" unit="px" duration="1s" /> <Moon size="20" color="#1d4ed8" unit="px" duration="1s" />
{:else if !!label} {:else if !!label}
{label} {label}
{:else} {:else}
+1
View File
@@ -20,6 +20,7 @@
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 10px; width: 10px;
height: 10px;
} }
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {
+70 -82
View File
@@ -1,89 +1,77 @@
<script> <script>
import marked from 'marked'; import { onMount } from 'svelte';
import PrimaryButton from '../PrimaryButton/index.svelte';
let isWriteMode = true; export let onChange;
export let value = ``; export let content;
export let placeholder = ``; export let disable;
const activeClassName = 'border-b-2 border-blue-700'; export let docId;
export let placeholder = 'Type something...';
const handleTabClick = (tab) => (event) => { const settings = {
isWriteMode = tab === 1; modules: {
toolbar: {
modules: {
syntax: true,
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
['link', 'code-block'],
],
},
placeholder: 'Type something...',
theme: 'snow',
},
},
theme: 'snow',
placeholder,
}; };
let quill = null;
let container = null;
function resetContent(docId) {
if (quill && quill.setText) {
if (content) {
quill.setContents(JSON.parse(content));
} else {
quill.setText('');
}
}
}
onMount(async () => {
const { default: Quill } = await import('quill');
Quill.prototype.getHTML = function () {
return this.container.querySelector('.ql-editor').innerHTML;
};
Quill.prototype.setHTML = function (html) {
this.container.querySelector('.ql-editor').innerHTML = html;
};
quill = new Quill(container, settings);
quill.setHTML(content || '');
quill.on('text-change', function () {
onChange(quill.getHTML());
});
});
$: {
if (quill && quill.setText) {
if (disable) {
quill.disable(disable);
} else {
quill.enable(true);
}
}
}
$: resetContent(docId);
</script> </script>
<div class="root mt-5 border border-gray rounded-md"> <div class="p-2 w-full h-full">
<div class="p-2 pb-0"> <div bind:this={container} />
<button
on:click={handleTabClick(1)}
class="focus:outline-none p-2 {isWriteMode && `${activeClassName}`}"
>
Write
</button>
<button
on:click={handleTabClick(2)}
class="focus:outline-none p-2 {!isWriteMode && `${activeClassName}`}"
>
Preview
</button>
</div>
<div class="m-2 mb-0 p-0">
{#if isWriteMode}
<textarea
bind:value
{placeholder}
class="border border-gray rounded-md p-2"
/>
{:else}
<article class="m-auto preview prose prose-sm sm:prose p-2">
{@html marked(value)}
</article>
{/if}
</div>
{#if !isWriteMode}
<hr />
{/if}
<div class="flex justify-between items-center px-2 pb-1">
<div class="flex items-center text-sm">
<svg
class="octicon mr-2"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"
/>
</svg>
<span>Styling with Markdown is supported</span>
</div>
<PrimaryButton label="Answer" />
</div>
</div> </div>
<style>
.root {
width: 100%;
min-height: 200px;
}
textarea {
width: 100%;
height: 150px;
}
.preview {
min-height: 150px;
}
.octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}
</style>
+4
View File
@@ -16,6 +16,10 @@
<svelte:head> <svelte:head>
<title>{status}</title> <title>{status}</title>
<link
rel="stylesheet"
href="https://unpkg.com/carbon-components-svelte/css/g90.css"
/>
</svelte:head> </svelte:head>
<div class="w-3/5 h-3/5 m-auto flex flex-col items-center justify-center"> <div class="w-3/5 h-3/5 m-auto flex flex-col items-center justify-center">
+7 -3
View File
@@ -9,7 +9,7 @@
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import { stores, goto } from '@sapper/app'; import { stores, goto } from '@sapper/app';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { Firework } from 'svelte-loading-spinners'; import { Moon } from 'svelte-loading-spinners';
import Tailwindcss from '../components/Tailwindcss.svelte'; import Tailwindcss from '../components/Tailwindcss.svelte';
import Navigation from '../components/Navigation/index.svelte'; import Navigation from '../components/Navigation/index.svelte';
import Snackbar from '../components/Snackbar/index.svelte'; import Snackbar from '../components/Snackbar/index.svelte';
@@ -91,7 +91,7 @@
// If user coming to login page, then // If user coming to login page, then
if (!$profile.role) { if (!$profile.role) {
goto(ROUTE.ONBOARDING); goto(ROUTE.ONBOARDING);
} else { } else if (['login', 'signup', 'onboarding'].includes(path)) {
goto('/dashboard'); goto('/dashboard');
} }
} }
@@ -161,6 +161,10 @@
</script> </script>
<svelte:window on:resize={handleResize} /> <svelte:window on:resize={handleResize} />
<svelte:head>
<link href="/css/carbon.css" rel="stylesheet" />
</svelte:head>
<Tailwindcss /> <Tailwindcss />
<!-- <Nav {segment} /> --> <!-- <Nav {segment} /> -->
@@ -169,7 +173,7 @@
<main> <main>
{#if $preloading && $delayedPreloading} {#if $preloading && $delayedPreloading}
<Backdrop> <Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" /> <Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop> </Backdrop>
{/if} {/if}
{#if !ROUTES_TO_HIDE_NAV.includes($page.path) && !isCoursePage(path)} {#if !ROUTES_TO_HIDE_NAV.includes($page.path) && !isCoursePage(path)}
+3 -3
View File
@@ -88,7 +88,7 @@
<div class="flex items-center justify-evenly max-w-6xl m-auto"> <div class="flex items-center justify-evenly max-w-6xl m-auto">
{#each boxes as box} {#each boxes as box}
<div <div
class="bg-white rounded-md py-4 px-16 active shadow-md hover:shadow-2xl border-2 hover:border-blue-700 cursor-pointer {animate}" class="bg-white rounded-md py-3 px-16 active shadow-md hover:shadow-2xl border-2 hover:border-blue-700 cursor-pointer {animate}"
> >
{#if box.icon === 'students'} {#if box.icon === 'students'}
<UserSpeaker32 class="carbon-icon" /> <UserSpeaker32 class="carbon-icon" />
@@ -106,7 +106,7 @@
<div class="flex items-center justify-evenly mt-16"> <div class="flex items-center justify-evenly mt-16">
<div class="table rounded-md bg-white shadow-md"> <div class="table rounded-md bg-white shadow-md">
<div class="flex border-t-0 {borderBottomGrey} p-3"> <div class="flex border-t-0 {borderBottomGrey} p-3">
<h3 class="m-0 py-2 pl-2 text-lg">Outstanding students</h3> <h3 class="m-0 py-3 pl-2 text-lg">Outstanding students</h3>
</div> </div>
{#each topStudents as topStudent, index} {#each topStudents as topStudent, index}
@@ -123,7 +123,7 @@
<div class="table rounded-md bg-white shadow-md"> <div class="table rounded-md bg-white shadow-md">
<div class="flex border-t-0 {borderBottomGrey} p-3"> <div class="flex border-t-0 {borderBottomGrey} p-3">
<h3 class="m-0 py-2 pl-2 text-lg">Top rated courses</h3> <h3 class="m-0 py-3 pl-2 text-lg">Top rated courses</h3>
</div> </div>
{#each topCourses as topCourse, index} {#each topCourses as topCourse, index}
+17 -17
View File
@@ -1,22 +1,22 @@
<script> <script>
import ChevronDown24 from "carbon-icons-svelte/lib/ChevronDown24"; import ChevronDown24 from 'carbon-icons-svelte/lib/ChevronDown24';
import TextEditor from "../components/TextEditor/index.svelte"; import TextEditor from '../components/TextEditor/index.svelte';
let questionTitle = ""; let questionTitle = '';
let questionDescription = ""; let questionDescription = '';
let facultyFilterOpened = false; let facultyFilterOpened = false;
const faculties = [ const faculties = [
"ИЭКСУ", 'ИЭКСУ',
"ИМБТ", 'ИМБТ',
"ИМИ", 'ИМИ',
"ИПТДМ", 'ИПТДМ',
"ИИИР", 'ИИИР',
"ГФ", 'ГФ',
"ХТФ", 'ХТФ',
"ИДЗО", 'ИДЗО',
"УИИ", 'УИИ',
"УНИ", 'УНИ',
]; ];
let checkbox = faculties.reduce((acc, cur) => { let checkbox = faculties.reduce((acc, cur) => {
acc[cur] = false; acc[cur] = false;
@@ -38,7 +38,7 @@
<div class="main-title my-1 flex"> <div class="main-title my-1 flex">
<div class="faculty-filter mr-1"> <div class="faculty-filter mr-1">
<button <button
class="flex border rounded-lg border-grey py-2 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200 relative" class="flex border rounded-lg border-grey py-3 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200 relative"
on:click={handleFacultyFilters} on:click={handleFacultyFilters}
> >
<span class="flex flex-row" <span class="flex flex-row"
@@ -49,10 +49,10 @@
{#if facultyFilterOpened} {#if facultyFilterOpened}
<div <div
class="w-48 bg-white border border-grey rounded-lg mt-2 py-2 absolute" class="w-48 bg-white border border-grey rounded-lg mt-2 py-3 absolute"
> >
{#each faculties as faculty} {#each faculties as faculty}
<div class="block px-4 py-2 border-b"> <div class="block px-4 py-3 border-b">
<input <input
type="checkbox" type="checkbox"
class="border border-gray checked:bg-blue-600 checked:border-transparent mr-1 cursor-pointer" class="border border-gray checked:bg-blue-600 checked:border-transparent mr-1 cursor-pointer"
+31 -1
View File
@@ -5,9 +5,39 @@
</script> </script>
<script> <script>
import Backdrop from '../../components/Backdrop/index.svelte';
import { Moon } from 'svelte-loading-spinners';
import CourseLandingPage from '../../components/CourseLandingPage/index.svelte'; import CourseLandingPage from '../../components/CourseLandingPage/index.svelte';
import { profile } from '../../utils/store/user';
import { setCourse, course } from '../../components/Course/store';
import { lessons } from '../../components/Course/components/Lesson/store/lessons';
import { fetchCourse } from '../../utils/services/courses';
export let slug; export let slug;
let courseData = {};
let loading = false;
async function triggerSetCourse(_profile) {
loading = true;
const { data } = await fetchCourse(undefined, slug);
setCourse(data);
loading = false;
}
function setCourseData(course, lessons) {
courseData = { ...course, lessons };
console.log('courseData', courseData);
}
$: triggerSetCourse($profile);
$: setCourseData($course, $lessons);
</script> </script>
<CourseLandingPage {slug} /> {#if loading}
<Backdrop>
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{:else}
<CourseLandingPage {courseData} />
{/if}
+2 -6
View File
@@ -11,11 +11,7 @@
import PageNav from '../../../components/PageNav/index.svelte'; import PageNav from '../../../components/PageNav/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte'; import PageBody from '../../../components/PageBody/index.svelte';
import Box from '../../../components/Box/index.svelte'; import Box from '../../../components/Box/index.svelte';
import { import { course, group, setCourse } from '../../../components/Course/store';
course,
group,
setCourseData,
} from '../../../components/Course/store';
import { getLectureNo } from '../../../components/Course/function'; import { getLectureNo } from '../../../components/Course/function';
import { fetchCourse } from '../../../utils/services/courses'; import { fetchCourse } from '../../../utils/services/courses';
import { lessons } from '../../../components/Course/components/Lesson/store/lessons'; import { lessons } from '../../../components/Course/components/Lesson/store/lessons';
@@ -120,7 +116,7 @@
} }
const { data } = await fetchCourse(courseId); const { data } = await fetchCourse(courseId);
setCourseData(data); setCourse(data);
setAttendance(data); setAttendance(data);
}); });
+2 -2
View File
@@ -7,7 +7,7 @@
<script> <script>
import Overview from '../../../components/Course/index.svelte'; import Overview from '../../../components/Course/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte'; import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { setCourseData } from '../../../components/Course/store'; import { setCourse } from '../../../components/Course/store';
import { fetchCourse } from '../../../utils/services/courses'; import { fetchCourse } from '../../../utils/services/courses';
export let courseId; export let courseId;
@@ -19,7 +19,7 @@
const { data } = await fetchCourse(courseId); const { data } = await fetchCourse(courseId);
setCourseData(data); setCourse(data);
prevCourseId = courseId; prevCourseId = courseId;
} }
+22 -2
View File
@@ -9,8 +9,28 @@
import CourseLandingPage from '../../../components/CourseLandingPage/index.svelte'; import CourseLandingPage from '../../../components/CourseLandingPage/index.svelte';
import Editor from '../../../components/CourseLandingPage/components/Editor/index.svelte'; import Editor from '../../../components/CourseLandingPage/components/Editor/index.svelte';
import { isMobile } from '../../../utils/store/useMobile'; import { isMobile } from '../../../utils/store/useMobile';
import { profile } from '../../../utils/store/user';
import { setCourse, course } from '../../../components/Course/store';
import { lessons } from '../../../components/Course/components/Lesson/store/lessons';
import { fetchCourse } from '../../../utils/services/courses';
export let courseId; export let courseId;
let courseData = {};
async function triggerSetCourse(_profile) {
if ($course.id) return;
const { data } = await fetchCourse(courseId);
setCourse(data);
}
function setCourseData(course, lessons) {
courseData = { ...course, lessons };
}
$: triggerSetCourse($profile);
$: setCourseData($course, $lessons);
$: console.log('courseData changed', courseData);
</script> </script>
<div <div
@@ -18,9 +38,9 @@
in:fly={{ y: 500, duration: 500 }} in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }} out:fly={{ y: 500, duration: 500 }}
> >
<Editor {courseId} /> <Editor {courseId} bind:course={courseData} />
<div class="rightBar" class:isMobile={$isMobile}> <div class="rightBar" class:isMobile={$isMobile}>
<CourseLandingPage /> <CourseLandingPage bind:courseData />
</div> </div>
</div> </div>
@@ -29,10 +29,7 @@
import Materials from '../../../../../components/Course/components/Lesson/Materials/index.svelte'; import Materials from '../../../../../components/Course/components/Lesson/Materials/index.svelte';
import Exercises from '../../../../../components/Course/components/Lesson/Exercises/index.svelte'; import Exercises from '../../../../../components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '../../../../../utils/constants/mode.js'; import MODES from '../../../../../utils/constants/mode.js';
import { import { setCourse, course } from '../../../../../components/Course/store';
setCourseData,
course,
} from '../../../../../components/Course/store';
import { lesson } from '../../../../../components/Course/components/Lesson/store/lessons'; import { lesson } from '../../../../../components/Course/components/Lesson/store/lessons';
export let courseId; export let courseId;
@@ -53,14 +50,10 @@
let lessonData; let lessonData;
if (!$course.id) { if (!$course.id) {
const [_course, lesson] = await Promise.all([ const { data } = await fetchCourse(courseId);
fetchCourse(courseId),
fetchLesson(lessonId),
]);
lessonData = lesson.data; lessonData = data.lessons.find((lesson) => lesson.id === lessonId);
setCourse(data);
setCourseData(_course.data);
} else if (prevLessonId !== lessonId) { } else if (prevLessonId !== lessonId) {
const lesson = await fetchLesson(lessonId); const lesson = await fetchLesson(lessonId);
lessonData = lesson.data; lessonData = lesson.data;
+2 -2
View File
@@ -36,7 +36,7 @@
} from '../../../../components/Course/components/Lesson/store/lessons'; } from '../../../../components/Course/components/Lesson/store/lessons';
import { import {
course, course,
setCourseData, setCourse,
group, group,
} from '../../../../components/Course/store'; } from '../../../../components/Course/store';
@@ -107,7 +107,7 @@
onMount(async () => { onMount(async () => {
if ($course.id) return; if ($course.id) return;
const { data } = await fetchCourse(courseId); const { data } = await fetchCourse(courseId);
setCourseData(data); setCourse(data);
}); });
$: { $: {
+2 -6
View File
@@ -12,11 +12,7 @@
import PageBody from '../../../components/PageBody/index.svelte'; import PageBody from '../../../components/PageBody/index.svelte';
import Box from '../../../components/Box/index.svelte'; import Box from '../../../components/Box/index.svelte';
import { ROLE } from '../../../utils/constants/roles'; import { ROLE } from '../../../utils/constants/roles';
import { import { setCourse, course, group } from '../../../components/Course/store';
setCourseData,
course,
group,
} from '../../../components/Course/store';
import { lessons } from '../../../components/Course/components/Lesson/store/lessons'; import { lessons } from '../../../components/Course/components/Lesson/store/lessons';
import { import {
fetchCourse, fetchCourse,
@@ -50,7 +46,7 @@
onMount(async () => { onMount(async () => {
if (!$course.id) { if (!$course.id) {
const { data } = await fetchCourse(courseId); const { data } = await fetchCourse(courseId);
setCourseData(data); setCourse(data);
} }
const { data: marks } = await fetchMarks(courseId); const { data: marks } = await fetchMarks(courseId);
+2 -6
View File
@@ -25,11 +25,7 @@
import IconButton from '../../../components/IconButton/index.svelte'; import IconButton from '../../../components/IconButton/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte'; import PageBody from '../../../components/PageBody/index.svelte';
import RoleBasedSecurity from '../../../components/RoleBasedSecurity/index.svelte'; import RoleBasedSecurity from '../../../components/RoleBasedSecurity/index.svelte';
import { import { setCourse, course, group } from '../../../components/Course/store';
setCourseData,
course,
group,
} from '../../../components/Course/store';
import InviationModal from '../../../components/Course/components/People/InviationModal.svelte'; import InviationModal from '../../../components/Course/components/People/InviationModal.svelte';
import DeleteConfirmation from '../../../components/Course/components/People/DeleteConfirmation.svelte'; import DeleteConfirmation from '../../../components/Course/components/People/DeleteConfirmation.svelte';
import { import {
@@ -102,7 +98,7 @@
onMount(async () => { onMount(async () => {
if ($course.id) return; if ($course.id) return;
const { data } = await fetchCourse(courseId); const { data } = await fetchCourse(courseId);
setCourseData(data); setCourse(data);
}); });
$: sortAndFilterPeople($group.people, filterBy); $: sortAndFilterPeople($group.people, filterBy);
+2 -2
View File
@@ -20,12 +20,12 @@
import CourseContainer from '../../../components/CourseContainer/index.svelte'; import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte'; import PageNav from '../../../components/PageNav/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte'; import PageBody from '../../../components/PageBody/index.svelte';
import { setCourseData } from '../../../components/Course/store'; import { setCourse } from '../../../components/Course/store';
export let courseData = {}; export let courseData = {};
onMount(() => { onMount(() => {
setCourseData(courseData); setCourse(courseData);
}); });
</script> </script>
+3 -3
View File
@@ -16,7 +16,7 @@
import PageBody from '../../../components/PageBody/index.svelte'; import PageBody from '../../../components/PageBody/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte'; import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { fetchCourse } from '../../../utils/services/courses'; import { fetchCourse } from '../../../utils/services/courses';
import { setCourseData, course } from '../../../components/Course/store'; import { setCourse, course } from '../../../components/Course/store';
import { import {
fetchSubmissions, fetchSubmissions,
updateSubmission, updateSubmission,
@@ -182,7 +182,7 @@
onMount(async () => { onMount(async () => {
if (!$course.id) { if (!$course.id) {
const { data } = await fetchCourse(courseId); const { data } = await fetchCourse(courseId);
setCourseData(data); setCourse(data);
} }
const { data: submissions } = await fetchSubmissions( const { data: submissions } = await fetchSubmissions(
@@ -297,7 +297,7 @@
<div <div
class="border {item.isEarly class="border {item.isEarly
? 'border-grey-700' ? 'border-grey-700'
: 'border-red-700'} w-full my-2 mx-0 rounded-md bg-white py-2 px-3" : 'border-red-700'} w-full my-2 mx-0 rounded-md bg-white py-3 px-3"
animate:flip={{ duration: flipDurationMs }} animate:flip={{ duration: flipDurationMs }}
> >
<div <div
+2 -2
View File
@@ -20,12 +20,12 @@
import CourseContainer from '../../../components/CourseContainer/index.svelte'; import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte'; import PageNav from '../../../components/PageNav/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte'; import PageBody from '../../../components/PageBody/index.svelte';
import { setCourseData } from '../../../components/Course/store'; import { setCourse } from '../../../components/Course/store';
export let courseData = {}; export let courseData = {};
onMount(() => { onMount(() => {
setCourseData(courseData); setCourse(courseData);
}); });
</script> </script>
+2 -1
View File
@@ -65,7 +65,8 @@
<p class="text-md mb-6 text-center"> <p class="text-md mb-6 text-center">
We have sent a confirmation email to <span class="text-blue-700" We have sent a confirmation email to <span class="text-blue-700"
>{fields.email}</span >{fields.email}</span
>. Kindly check your inbox to reset password. >. Kindly check your inbox to reset password or spam to reset your
password.
</p> </p>
</div> </div>
+1 -1
View File
@@ -97,7 +97,7 @@
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
{#each areas as area, index} {#each areas as area, index}
<div <div
class="box m-3 bg-white rounded-md py-4 px-12 active shadow-xl border-2 hover:border-blue-700 {animate}" class="box m-3 bg-white rounded-md py-3 px-12 active shadow-xl border-2 hover:border-blue-700 {animate}"
> >
<h3 class="text-3xl"> <h3 class="text-3xl">
{#if index === 0} {#if index === 0}
+1 -1
View File
@@ -287,7 +287,7 @@
<PrimaryButton <PrimaryButton
label="Back" label="Back"
variant={VARIANTS.NONE} variant={VARIANTS.NONE}
className="py-2 px-6 mr-2 text-blue-700" className="py-3 px-6 mr-2 text-blue-700"
onClick={() => (step = step - 1)} onClick={() => (step = step - 1)}
/> />
{/if} {/if}
+3 -3
View File
@@ -183,7 +183,7 @@
This are the organizations you work at This are the organizations you work at
</p> </p>
<div class="hidden my-3 py-2 px-4 border shadow-sm rounded-md"> <div class="hidden my-3 py-3 px-4 border shadow-sm rounded-md">
<h4 class="m-0 font-normal"> <h4 class="m-0 font-normal">
<a class="text-blue-700 font-bold" href="/courses"> CitiBim </a> <a class="text-blue-700 font-bold" href="/courses"> CitiBim </a>
</h4> </h4>
@@ -204,7 +204,7 @@
</div> </div>
</div> </div>
<div class="hidden my-2 py-2 px-4 border shadow-sm rounded-md"> <div class="hidden my-2 py-3 px-4 border shadow-sm rounded-md">
<h4 class="m-0 font-normal"> <h4 class="m-0 font-normal">
<a class="text-blue-700 font-bold" href="/courses"> <a class="text-blue-700 font-bold" href="/courses">
Climate Change Group Climate Change Group
@@ -226,7 +226,7 @@
</div> </div>
</div> </div>
<PrimaryButton <PrimaryButton
className="px-6 py-2" className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER} variant={VARIANTS.CONTAINED_DANGER}
label="Log out" label="Log out"
onClick={logout} onClick={logout}
+6 -3
View File
@@ -12,7 +12,8 @@
<link rel="manifest" href="manifest.json" crossorigin="use-credentials"> <link rel="manifest" href="manifest.json" crossorigin="use-credentials">
<link rel="icon" type="image/png" href="favicon.ico"> <link rel="icon" type="image/png" href="favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png">
<link href="/css/quill.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
<!-- Global site tag (gtag.js) - Google Analytics --> <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C7WBN0S06R"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-C7WBN0S06R"></script>
<script> <script>
@@ -23,10 +24,12 @@
gtag('config', 'G-C7WBN0S06R'); gtag('config', 'G-C7WBN0S06R');
</script> </script>
<!--Crisp Chat --> <!--Crisp Chat
<script <script
type="text/javascript">window.$crisp = []; window.CRISP_WEBSITE_ID = "2158dbf9-90d4-4e50-b962-e1d8a1a6deb7"; (function () { d = document; s = d.createElement("script"); s.src = "https://client.crisp.chat/l.js"; s.async = 1; d.getElementsByTagName("head")[0].appendChild(s); })();</script> type="text/javascript">window.$crisp = []; window.CRISP_WEBSITE_ID = "2158dbf9-90d4-4e50-b962-e1d8a1a6deb7"; (function () { d = document; s = d.createElement("script"); s.src = "https://client.crisp.chat/l.js"; s.async = 1; d.getElementsByTagName("head")[0].appendChild(s); })();</script>
-->
<script src="https://cdn.plyr.io/3.7.3/plyr.js">
</script>
<!-- Sapper creates a <script> tag containing `src/client.js` <!-- Sapper creates a <script> tag containing `src/client.js`
and anything else it needs to hydrate the app and and anything else it needs to hydrate the app and
initialise the router --> initialise the router -->
+7 -1
View File
@@ -21,4 +21,10 @@ export const PUBLIC_ROUTES = [
ROUTE.RESET, ROUTE.RESET,
]; ];
export const ROUTES_TO_HIDE_NAV = [...PUBLIC_ROUTES, ROUTE.ONBOARDING]; export const ROUTES_TO_HIDE_NAV = [
ROUTE.LOGIN,
ROUTE.SIGN_UP,
ROUTE.FORGOT,
ROUTE.RESET,
ROUTE.ONBOARDING,
];
+51
View File
@@ -0,0 +1,51 @@
export function formatYoutubeVideo(url: string, errors: { video: string }) {
console.log(`url`, url);
const prefix = 'https://www.youtube.com/embed';
// https://www.youtube.com/embed/qajK1J1neAM
if (url.includes('embed')) {
return url;
}
// https://youtu.be/qajK1J1neAM
if (url.includes('.be/')) {
const splittedUrlWithBe = url.split('.be/');
return `${prefix}/${splittedUrlWithBe[1]}`;
}
// https://www.youtube.com/watch?v=qajK1J1neAM
const splitedUrl = url.split('watch');
if (splitedUrl.length !== 2) {
errors.video = 'Not a valid youtube link';
return;
}
const query = new URLSearchParams(splitedUrl[1]);
return `${prefix}/${query.get('v')}`;
}
export function getEmbedId(url: string) {
// https://www.youtube.com/embed/qajK1J1neAM
if (url.includes('embed')) {
return url.split('embed/')[1];
}
// https://youtu.be/qajK1J1neAM
if (url.includes('.be/')) {
const splittedUrlWithBe = url.split('.be/');
return splittedUrlWithBe[1];
}
// https://www.youtube.com/watch?v=qajK1J1neAM
const splitedUrl = url.split('watch');
if (splitedUrl.length !== 2) {
return '';
}
const query = new URLSearchParams(splitedUrl[1]);
return query.get('v');
}
@@ -1,11 +1,13 @@
export function isUUID(uuid) { export function isUUID(uuid: string) {
let s = '' + uuid; let s = '' + uuid;
s = s.match( const m: RegExpMatchArray | null = s.match(
'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
); );
if (s === null) {
if (m === null) {
return false; return false;
} }
return true; return true;
} }
View File
@@ -1,22 +1,24 @@
import { supabase } from '../../functions/supabase'; import { supabase } from '../../functions/supabase';
import { isUUID } from '../../functions/isUUID'; import { isUUID } from '../../functions/isUUID';
import { QUESTION_TYPE } from '../../../components/Question/constants'; import { QUESTION_TYPE } from '../../../components/Question/constants';
import type {
Lesson,
Course,
Group,
Groupmember,
Exercise,
} from '../../../utils/types';
export async function fetchCourse(courseId, session) { export async function fetchCourse(
console.log(`courseId`, courseId); courseId?: Course['id'],
let jsonUser; slug?: Course['slug']
) {
let match: { slug?: string; id?: string } = {};
if (!process.browser) { if (slug) {
// try { match.slug = slug;
// jsonUser = JSON.parse(session.user); } else {
// } catch (error) { match.id = courseId;
// console.log('Token is expired probably cause user not found');
// return {
// data: [],
// cantFetch: true,
// };
// // return this.redirect(301, '/login');
// }
} }
const { data, error } = await supabase const { data, error } = await supabase
@@ -33,14 +35,19 @@ export async function fetchCourse(courseId, session) {
profile(*) profile(*)
) )
), ),
slug,
cost,
currency,
metadata,
lessons:lesson( lessons:lesson(
id, title,public, lesson_at, call_url, is_complete, order, created_at, id, title,public, lesson_at, call_url, is_complete, order, created_at,
note, video_url, slide_url, call_url, totalExercises:exercise(count),
profile:teacher_id(id, avatar_url, fullname) profile:teacher_id(id, avatar_url, fullname)
), ),
attendance:group_attendance(*) attendance:group_attendance(*)
` `
) )
.match({ id: courseId }) .match(match)
.single(); .single();
console.log(`error`, error); console.log(`error`, error);
if (!data || error) { if (!data || error) {
@@ -56,7 +63,7 @@ export async function fetchCourse(courseId, session) {
}; };
} }
export async function fetchGroup(groupId) { export async function fetchGroup(groupId: Group['id']) {
const { data, error } = await supabase const { data, error } = await supabase
.from('group') .from('group')
.select(`*,members:groupmember(*,profile(*))`) .select(`*,members:groupmember(*,profile(*))`)
@@ -69,7 +76,10 @@ export async function fetchGroup(groupId) {
}; };
} }
export async function setProfileIdOfGroupMember(email, profileId) { export async function setProfileIdOfGroupMember(
email: string,
profileId: string
) {
const { data, error } = await supabase const { data, error } = await supabase
.from('groupmember') .from('groupmember')
.update({ .update({
@@ -84,13 +94,41 @@ export async function setProfileIdOfGroupMember(email, profileId) {
}; };
} }
export async function updateCourse(courseId, course) { export async function uploadAvatar(courseId: string, avatar: string) {
if (course.avatar) { const filename = `course/${courseId + Date.now()}.webp`;
let logo;
const { data } = await supabase.storage
.from('avatars')
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
if (data && data.Key) {
const { publicURL } = supabase.storage
.from('avatars')
.getPublicUrl(filename);
if (!publicURL) return;
logo = publicURL;
}
return logo;
}
export async function updateCourse(
courseId: Course['id'],
avatar: string | undefined,
course: Course
) {
if (avatar) {
const filename = `course/${courseId + Date.now()}.webp`; const filename = `course/${courseId + Date.now()}.webp`;
const { data } = await supabase.storage const { data } = await supabase.storage
.from('avatars') .from('avatars')
.upload(filename, course.avatar, { .upload(filename, avatar, {
cacheControl: '3600', cacheControl: '3600',
upsert: false, upsert: false,
}); });
@@ -100,12 +138,12 @@ export async function updateCourse(courseId, course) {
.from('avatars') .from('avatars')
.getPublicUrl(filename); .getPublicUrl(filename);
if (!publicURL) return;
course.logo = publicURL; course.logo = publicURL;
} }
} }
delete course.avatar;
await supabase await supabase
.from('course') .from('course')
.update(course, { returning: 'minimal' }) .update(course, { returning: 'minimal' })
@@ -114,28 +152,28 @@ export async function updateCourse(courseId, course) {
return course.logo; return course.logo;
} }
export async function deleteCourse(courseId) { export async function deleteCourse(courseId: Course['id']) {
return await supabase.from('course').delete().match({ id: courseId }); return await supabase.from('course').delete().match({ id: courseId });
} }
export function addGroupMember(member) { export function addGroupMember(member: any) {
return supabase.from('groupmember').insert(member, { return supabase.from('groupmember').insert(member, {
returning: 'minimal', returning: 'minimal',
}); });
} }
export function updatedGroupMember(update, match) { export function updatedGroupMember(update: any, match: any) {
return supabase return supabase
.from('groupmember') .from('groupmember')
.update(update, { returning: 'minimal' }) .update(update, { returning: 'minimal' })
.match(match); .match(match);
} }
export function deleteGroupMember(groupMemberId) { export function deleteGroupMember(groupMemberId: Groupmember['id']) {
return supabase.from('groupmember').delete().match({ id: groupMemberId }); return supabase.from('groupmember').delete().match({ id: groupMemberId });
} }
export function fetchLesson(lessonId) { export function fetchLesson(lessonId: Lesson['id']) {
return supabase return supabase
.from('lesson') .from('lesson')
.select( .select(
@@ -145,35 +183,39 @@ export function fetchLesson(lessonId) {
.single(); .single();
} }
export function createLesson(lesson) { export function createLesson(lesson: any) {
return supabase.from('lesson').insert(lesson); return supabase.from('lesson').insert(lesson);
} }
export function updateLesson(lesson, lessonId) { export function updateLesson(lesson: any, lessonId: Lesson['id']) {
return supabase return supabase
.from('lesson') .from('lesson')
.update({ ...lesson, id: undefined }, { returning: 'minimal' }) .update({ ...lesson, id: undefined }, { returning: 'minimal' })
.match({ id: lessonId }); .match({ id: lessonId });
} }
export function deleteLesson(lessonId) { export function deleteLesson(lessonId: Lesson['id']) {
// Need to implement soft delete // Need to implement soft delete
return supabase.from('lesson').delete().match({ id: lessonId }); return supabase.from('lesson').delete().match({ id: lessonId });
} }
export function createExercise(exercise) { export function createExercise(exercise: any) {
return supabase.from('exercise').insert(exercise); return supabase.from('exercise').insert(exercise);
} }
export function fetchExercisesByMarks(courseId) { export function fetchExercisesByMarks(courseId: Course['id']) {
return supabase.rpc('get_exercises').eq('course_id', courseId); return supabase.rpc('get_exercises').eq('course_id', courseId);
} }
function isNew(item) { function isNew(item: any) {
return isNaN(item); return isNaN(item);
} }
export async function upsertExercise(questionnaire, exerciseId) { // TODO: Add questionnaire type
export async function upsertExercise(
questionnaire: any,
exerciseId: Exercise['id']
) {
const { const {
questions, questions,
title, title,
@@ -263,10 +305,12 @@ export async function upsertExercise(questionnaire, exerciseId) {
// Delete cause this is not a field in the table // Delete cause this is not a field in the table
delete newQuestion.question_type_id; delete newQuestion.question_type_id;
// @ts-ignore
newQuestion.question_type = { id: question_type_id }; newQuestion.question_type = { id: question_type_id };
newQuestion.id = id; newQuestion.id = id;
newQuestion.name = name; newQuestion.name = name;
newQuestion.order = order; newQuestion.order = order;
// @ts-ignore
newQuestion.options = []; newQuestion.options = [];
// Don't map options for 'Paragraph' questions // Don't map options for 'Paragraph' questions
@@ -297,9 +341,11 @@ export async function upsertExercise(questionnaire, exerciseId) {
if (is_dirty || isNew(option.id)) { if (is_dirty || isNew(option.id)) {
const { data } = await supabase.from('option').upsert(newOption); const { data } = await supabase.from('option').upsert(newOption);
if (Array.isArray(data)) { if (Array.isArray(data)) {
// @ts-ignore
newQuestion.options.push(data[0]); newQuestion.options.push(data[0]);
} }
} else { } else {
// @ts-ignore
newQuestion.options.push(newOption); newQuestion.options.push(newOption);
} }
} }
@@ -312,12 +358,16 @@ export async function upsertExercise(questionnaire, exerciseId) {
return updatedQuestions; return updatedQuestions;
} }
interface LooseObject {
[key: string]: any;
}
export async function submitExercise( export async function submitExercise(
answers, answers: Array<string>,
questions, questions: Array<{ name: string; id: string }>,
exerciseId, exerciseId: Exercise['id'],
courseId, courseId: Course['id'],
groupMemberId groupMemberId: Groupmember['id'] | undefined
) { ) {
if (!groupMemberId) { if (!groupMemberId) {
return; return;
@@ -326,7 +376,7 @@ export async function submitExercise(
const questionsByName = questions.reduce( const questionsByName = questions.reduce(
(acc, q) => ({ ...acc, [q.name]: q.id }), (acc, q) => ({ ...acc, [q.name]: q.id }),
{} {}
); ) as LooseObject;
const questionAnswers = []; const questionAnswers = [];
const { data: submission } = await supabase.from('submission').insert({ const { data: submission } = await supabase.from('submission').insert({
@@ -343,7 +393,7 @@ export async function submitExercise(
question_id: questionsByName[questionName], question_id: questionsByName[questionName],
open_answer: '', open_answer: '',
answers: [], answers: [],
submission_id: submission[0].id, submission_id: Array.isArray(submission) ? submission[0].id : null,
}; };
if (typeof value === 'string') { if (typeof value === 'string') {
@@ -359,7 +409,10 @@ export async function submitExercise(
console.log(`res`, res); console.log(`res`, res);
} }
export async function deleteExercise(questions, exerciseId) { export async function deleteExercise(
questions: Array<{ id: string }>,
exerciseId: Exercise['id']
) {
for (const question of questions) { for (const question of questions) {
const { id } = question; const { id } = question;
+79 -68
View File
@@ -1,25 +1,30 @@
export * from './config'; export * from './config';
export * from './dashboard'; export * from './dashboard';
export interface CurrentSessionType { interface CourseMetadata {
access_token: string; requirements?: string;
expires_at: number; description?: string;
expires_in: number; goals?: string;
refresh_token: string; videoUrl?: '';
token_type: string; showDiscount: false;
} discount: 0;
reward?: {
export interface UserType { show: boolean;
openAuthModal: boolean; description: string;
fetchingUser: boolean | null; };
isLoggedIn: boolean; instructor?: {
currentSession: CurrentSessionType | null; name: string;
expiresAt: number; role: string;
coursesNo: number;
description: string;
imgUrl: string;
};
} }
// Generated from https://supabase-schema.vercel.app/
export interface Organization { export interface Organization {
id: string /* primary key */; id: string /* primary key */;
name: string; name: any; // type unknown;
} }
export interface Profile { export interface Profile {
@@ -29,27 +34,30 @@ export interface Profile {
avatar_url?: string; avatar_url?: string;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
email?: string; email?: any; // type unknown;
can_add_course?: boolean; can_add_course?: boolean;
role?: any; // type unknown;
goal?: any; // type unknown;
source?: any; // type unknown;
} }
export interface Question_type { export interface Question_type {
id: number /* primary key */; id: number /* primary key */;
label: string; label: any; // type unknown;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
typename?: string; typename?: any; // type unknown;
} }
export interface Submissionstatus { export interface Submissionstatus {
id: number /* primary key */; id: number /* primary key */;
label: string; label: any; // type unknown;
updated_at?: string; updated_at?: string;
} }
export interface Group { export interface Group {
id: string /* primary key */; id: string /* primary key */;
name: string; name: any; // type unknown;
description?: string; description?: string;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
@@ -58,8 +66,8 @@ export interface Group {
} }
export interface Role { export interface Role {
type: string; type: any; // type unknown;
description?: string; description?: any; // type unknown;
id: number /* primary key */; id: number /* primary key */;
updated_at?: string; updated_at?: string;
created_at?: string; created_at?: string;
@@ -67,24 +75,27 @@ export interface Role {
export interface Waitinglist { export interface Waitinglist {
id: number /* primary key */; id: number /* primary key */;
email: string; email: any; // type unknown;
created_at?: string; created_at?: string;
} }
export interface Course { export interface Course {
created_at?: string; title: any; // type unknown;
description: string; description: any; // type unknown;
group_id?: string /* foreign key to group.id */; overview?: any; // type unknown;
group?: Group;
id: string /* primary key */; id: string /* primary key */;
is_template?: boolean; created_at?: string;
lessons: Lesson[];
logo?: string;
organization_id?: string /* foreign key to organization.id */;
organization?: Organization;
overview?: string;
title: string;
updated_at?: string; updated_at?: string;
group_id?: string /* foreign key to group.id */;
is_template?: boolean;
organization_id?: string /* foreign key to organization.id */;
logo?: string;
slug?: any; // type unknown;
metadata?: CourseMetadata;
cost?: number;
currency?: string;
group?: Group;
organization?: Organization;
} }
export interface Groupmember { export interface Groupmember {
@@ -92,9 +103,9 @@ export interface Groupmember {
group_id: string /* foreign key to group.id */; group_id: string /* foreign key to group.id */;
role_id: number /* foreign key to role.id */; role_id: number /* foreign key to role.id */;
profile_id?: string /* foreign key to profile.id */; profile_id?: string /* foreign key to profile.id */;
email?: string; email?: any; // type unknown;
created_at?: string; created_at?: string;
assigned_student_id?: string; assigned_student_id?: any; // type unknown;
group?: Group; group?: Group;
role?: Role; role?: Role;
profile?: Profile; profile?: Profile;
@@ -123,46 +134,47 @@ export interface Group_attendance {
} }
export interface Lesson { export interface Lesson {
call_url?: string; note?: any; // type unknown;
video_url?: any; // type unknown;
slide_url?: any; // type unknown;
course_id: string /* foreign key to course.id */; course_id: string /* foreign key to course.id */;
created_at: string;
exercise?: Exercise[];
id: string /* primary key */; id: string /* primary key */;
is_complete?: boolean; created_at?: string;
lesson_at?: string;
note?: string;
order: number;
profile?: Profile;
public?: boolean;
slide_url?: string;
teacher_id?: string /* foreign key to profile.id */;
title: string;
updated_at?: string; updated_at?: string;
video_url?: string; title: any; // type unknown;
public?: boolean;
lesson_at?: string;
teacher_id?: string /* foreign key to profile.id */;
is_complete?: boolean;
call_url?: string;
order?: number;
course?: Course;
profile?: Profile;
} }
export interface Exercise { export interface Exercise {
title: any; // type unknown;
description?: any; // type unknown;
lesson_id?: string /* foreign key to lesson.id */;
created_at?: string; created_at?: string;
description?: string;
due_by?: string;
id: string /* primary key */;
lesson_id: string /* foreign key */;
questions?: Question[];
title: string;
updated_at?: string; updated_at?: string;
id: string /* primary key */;
due_by?: string;
lesson?: Lesson;
} }
export interface Question { export interface Question {
created_at?: string;
exercise_id: string /* foreign key to exercise.id */;
id: number /* primary key */; id: number /* primary key */;
name?: string;
options: Option[];
order?: number;
points?: string;
question_type_id: number /* foreign key to question_type.id */; question_type_id: number /* foreign key to question_type.id */;
title: string; title: any; // type unknown;
created_at?: string;
updated_at?: string; updated_at?: string;
exercise_id: string /* foreign key to exercise.id */;
name?: string;
points?: any; // type unknown;
order?: number;
question_type?: Question_type;
exercise?: Exercise;
} }
export interface Submission { export interface Submission {
@@ -182,15 +194,14 @@ export interface Submission {
} }
export interface Option { export interface Option {
created_at?: string; id: number /* primary key */;
id?: number /* primary key */; label: any; // type unknown;
is_correct: boolean; is_correct: boolean;
is_dirty?: boolean;
label: string;
question_id: number /* foreign key to question.id */; question_id: number /* foreign key to question.id */;
question?: Question;
updated_at?: string;
value?: string; value?: string;
created_at?: string;
updated_at?: string;
question?: Question;
} }
export interface Question_answer { export interface Question_answer {
File diff suppressed because one or more lines are too long
+957
View File
@@ -0,0 +1,957 @@
/*!
* Quill Editor v1.3.6
* https://quilljs.com/
* Copyright (c) 2014, Jason Chen
* Copyright (c) 2013, salesforce.com
*/
.ql-container {
box-sizing: border-box;
font-family: Helvetica, Arial, sans-serif;
font-size: 13px;
height: 100%;
margin: 0px;
position: relative;
}
.ql-container.ql-disabled .ql-tooltip {
visibility: hidden;
}
.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
pointer-events: none;
}
.ql-clipboard {
left: -100000px;
height: 1px;
overflow-y: hidden;
position: absolute;
top: 50%;
}
.ql-clipboard p {
margin: 0;
padding: 0;
}
.ql-editor {
box-sizing: border-box;
line-height: 1.42;
height: 100%;
outline: none;
overflow-y: auto;
padding: 12px 15px;
tab-size: 4;
-moz-tab-size: 4;
text-align: left;
white-space: pre-wrap;
word-wrap: break-word;
}
.ql-editor > * {
cursor: text;
}
.ql-editor p,
.ql-editor ol,
.ql-editor ul,
.ql-editor pre,
.ql-editor blockquote,
.ql-editor h1,
.ql-editor h2,
.ql-editor h3,
.ql-editor h4,
.ql-editor h5,
.ql-editor h6 {
margin: 0;
padding: 0;
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol,
.ql-editor ul {
padding-left: 1.5em;
}
.ql-editor ol > li,
.ql-editor ul > li {
list-style-type: none;
}
.ql-editor ul > li::before {
content: '\2022';
}
.ql-editor ul[data-checked='true'],
.ql-editor ul[data-checked='false'] {
pointer-events: none;
}
.ql-editor ul[data-checked='true'] > li *,
.ql-editor ul[data-checked='false'] > li * {
pointer-events: all;
}
.ql-editor ul[data-checked='true'] > li::before,
.ql-editor ul[data-checked='false'] > li::before {
color: #777;
cursor: pointer;
pointer-events: all;
}
.ql-editor ul[data-checked='true'] > li::before {
content: '\2611';
}
.ql-editor ul[data-checked='false'] > li::before {
content: '\2610';
}
.ql-editor li::before {
display: inline-block;
white-space: nowrap;
width: 1.2em;
}
.ql-editor li:not(.ql-direction-rtl)::before {
margin-left: -1.5em;
margin-right: 0.3em;
text-align: right;
}
.ql-editor li.ql-direction-rtl::before {
margin-left: 0.3em;
margin-right: -1.5em;
}
.ql-editor ol li:not(.ql-direction-rtl),
.ql-editor ul li:not(.ql-direction-rtl) {
padding-left: 1.5em;
}
.ql-editor ol li.ql-direction-rtl,
.ql-editor ul li.ql-direction-rtl {
padding-right: 1.5em;
}
.ql-editor ol li {
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
counter-increment: list-0;
}
.ql-editor ol li:before {
content: counter(list-0, decimal) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-increment: list-1;
}
.ql-editor ol li.ql-indent-1:before {
content: counter(list-1, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-2 {
counter-increment: list-2;
}
.ql-editor ol li.ql-indent-2:before {
content: counter(list-2, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-2 {
counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-3 {
counter-increment: list-3;
}
.ql-editor ol li.ql-indent-3:before {
content: counter(list-3, decimal) '. ';
}
.ql-editor ol li.ql-indent-3 {
counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-4 {
counter-increment: list-4;
}
.ql-editor ol li.ql-indent-4:before {
content: counter(list-4, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-4 {
counter-reset: list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-5 {
counter-increment: list-5;
}
.ql-editor ol li.ql-indent-5:before {
content: counter(list-5, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-5 {
counter-reset: list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-6 {
counter-increment: list-6;
}
.ql-editor ol li.ql-indent-6:before {
content: counter(list-6, decimal) '. ';
}
.ql-editor ol li.ql-indent-6 {
counter-reset: list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-7 {
counter-increment: list-7;
}
.ql-editor ol li.ql-indent-7:before {
content: counter(list-7, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-7 {
counter-reset: list-8 list-9;
}
.ql-editor ol li.ql-indent-8 {
counter-increment: list-8;
}
.ql-editor ol li.ql-indent-8:before {
content: counter(list-8, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-8 {
counter-reset: list-9;
}
.ql-editor ol li.ql-indent-9 {
counter-increment: list-9;
}
.ql-editor ol li.ql-indent-9:before {
content: counter(list-9, decimal) '. ';
}
.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
padding-left: 3em;
}
.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
padding-left: 4.5em;
}
.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 3em;
}
.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 4.5em;
}
.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
padding-left: 6em;
}
.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
padding-left: 7.5em;
}
.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 6em;
}
.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 7.5em;
}
.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
padding-left: 9em;
}
.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
padding-left: 10.5em;
}
.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 9em;
}
.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 10.5em;
}
.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
padding-left: 12em;
}
.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
padding-left: 13.5em;
}
.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 12em;
}
.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 13.5em;
}
.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
padding-left: 15em;
}
.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
padding-left: 16.5em;
}
.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 15em;
}
.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 16.5em;
}
.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
padding-left: 18em;
}
.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
padding-left: 19.5em;
}
.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 18em;
}
.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 19.5em;
}
.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
padding-left: 21em;
}
.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
padding-left: 22.5em;
}
.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 21em;
}
.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 22.5em;
}
.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
padding-left: 24em;
}
.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
padding-left: 25.5em;
}
.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 24em;
}
.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 25.5em;
}
.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
padding-left: 27em;
}
.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
padding-left: 28.5em;
}
.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 27em;
}
.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 28.5em;
}
.ql-editor .ql-video {
display: block;
max-width: 100%;
}
.ql-editor .ql-video.ql-align-center {
margin: 0 auto;
}
.ql-editor .ql-video.ql-align-right {
margin: 0 0 0 auto;
}
.ql-editor .ql-bg-black {
background-color: #000;
}
.ql-editor .ql-bg-red {
background-color: #e60000;
}
.ql-editor .ql-bg-orange {
background-color: #f90;
}
.ql-editor .ql-bg-yellow {
background-color: #ff0;
}
.ql-editor .ql-bg-green {
background-color: #008a00;
}
.ql-editor .ql-bg-blue {
background-color: #06c;
}
.ql-editor .ql-bg-purple {
background-color: #93f;
}
.ql-editor .ql-color-white {
color: #fff;
}
.ql-editor .ql-color-red {
color: #e60000;
}
.ql-editor .ql-color-orange {
color: #f90;
}
.ql-editor .ql-color-yellow {
color: #ff0;
}
.ql-editor .ql-color-green {
color: #008a00;
}
.ql-editor .ql-color-blue {
color: #06c;
}
.ql-editor .ql-color-purple {
color: #93f;
}
.ql-editor .ql-font-serif {
font-family: Georgia, Times New Roman, serif;
}
.ql-editor .ql-font-monospace {
font-family: Monaco, Courier New, monospace;
}
.ql-editor .ql-size-small {
font-size: 0.75em;
}
.ql-editor .ql-size-large {
font-size: 1.5em;
}
.ql-editor .ql-size-huge {
font-size: 2.5em;
}
.ql-editor .ql-direction-rtl {
direction: rtl;
text-align: inherit;
}
.ql-editor .ql-align-center {
text-align: center;
}
.ql-editor .ql-align-justify {
text-align: justify;
}
.ql-editor .ql-align-right {
text-align: right;
}
.ql-editor.ql-blank::before {
color: rgba(0, 0, 0, 0.6);
content: attr(data-placeholder);
font-style: italic;
left: 15px;
pointer-events: none;
position: absolute;
right: 15px;
}
.ql-snow.ql-toolbar:after,
.ql-snow .ql-toolbar:after {
clear: both;
content: '';
display: table;
}
.ql-snow.ql-toolbar button,
.ql-snow .ql-toolbar button {
background: none;
border: none;
cursor: pointer;
display: inline-block;
float: left;
height: 24px;
padding: 3px 5px;
width: 28px;
}
.ql-snow.ql-toolbar button svg,
.ql-snow .ql-toolbar button svg {
float: left;
height: 100%;
}
.ql-snow.ql-toolbar button:active:hover,
.ql-snow .ql-toolbar button:active:hover {
outline: none;
}
.ql-snow.ql-toolbar input.ql-image[type='file'],
.ql-snow .ql-toolbar input.ql-image[type='file'] {
display: none;
}
.ql-snow.ql-toolbar button:hover,
.ql-snow .ql-toolbar button:hover,
.ql-snow.ql-toolbar button:focus,
.ql-snow .ql-toolbar button:focus,
.ql-snow.ql-toolbar button.ql-active,
.ql-snow .ql-toolbar button.ql-active,
.ql-snow.ql-toolbar .ql-picker-label:hover,
.ql-snow .ql-toolbar .ql-picker-label:hover,
.ql-snow.ql-toolbar .ql-picker-label.ql-active,
.ql-snow .ql-toolbar .ql-picker-label.ql-active,
.ql-snow.ql-toolbar .ql-picker-item:hover,
.ql-snow .ql-toolbar .ql-picker-item:hover,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected {
color: #06c;
}
.ql-snow.ql-toolbar button:hover .ql-fill,
.ql-snow .ql-toolbar button:hover .ql-fill,
.ql-snow.ql-toolbar button:focus .ql-fill,
.ql-snow .ql-toolbar button:focus .ql-fill,
.ql-snow.ql-toolbar button.ql-active .ql-fill,
.ql-snow .ql-toolbar button.ql-active .ql-fill,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,
.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,
.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,
.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,
.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,
.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,
.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,
.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,
.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,
.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill {
fill: #06c;
}
.ql-snow.ql-toolbar button:hover .ql-stroke,
.ql-snow .ql-toolbar button:hover .ql-stroke,
.ql-snow.ql-toolbar button:focus .ql-stroke,
.ql-snow .ql-toolbar button:focus .ql-stroke,
.ql-snow.ql-toolbar button.ql-active .ql-stroke,
.ql-snow .ql-toolbar button.ql-active .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,
.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,
.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
.ql-snow.ql-toolbar button:hover .ql-stroke-miter,
.ql-snow .ql-toolbar button:hover .ql-stroke-miter,
.ql-snow.ql-toolbar button:focus .ql-stroke-miter,
.ql-snow .ql-toolbar button:focus .ql-stroke-miter,
.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,
.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {
stroke: #06c;
}
@media (pointer: coarse) {
.ql-snow.ql-toolbar button:hover:not(.ql-active),
.ql-snow .ql-toolbar button:hover:not(.ql-active) {
color: #444;
}
.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,
.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,
.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,
.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill {
fill: #444;
}
.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,
.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,
.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,
.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter {
stroke: #444;
}
}
.ql-snow {
box-sizing: border-box;
}
.ql-snow * {
box-sizing: border-box;
}
.ql-snow .ql-hidden {
display: none;
}
.ql-snow .ql-out-bottom,
.ql-snow .ql-out-top {
visibility: hidden;
}
.ql-snow .ql-tooltip {
position: absolute;
transform: translateY(10px);
}
.ql-snow .ql-tooltip a {
cursor: pointer;
text-decoration: none;
}
.ql-snow .ql-tooltip.ql-flip {
transform: translateY(-10px);
}
.ql-snow .ql-formats {
display: inline-block;
vertical-align: middle;
}
.ql-snow .ql-formats:after {
clear: both;
content: '';
display: table;
}
.ql-snow .ql-stroke {
fill: none;
stroke: #444;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
}
.ql-snow .ql-stroke-miter {
fill: none;
stroke: #444;
stroke-miterlimit: 10;
stroke-width: 2;
}
.ql-snow .ql-fill,
.ql-snow .ql-stroke.ql-fill {
fill: #444;
}
.ql-snow .ql-empty {
fill: none;
}
.ql-snow .ql-even {
fill-rule: evenodd;
}
.ql-snow .ql-thin,
.ql-snow .ql-stroke.ql-thin {
stroke-width: 1;
}
.ql-snow .ql-transparent {
opacity: 0.4;
}
.ql-snow .ql-direction svg:last-child {
display: none;
}
.ql-snow .ql-direction.ql-active svg:last-child {
display: inline;
}
.ql-snow .ql-direction.ql-active svg:first-child {
display: none;
}
.ql-snow .ql-editor h1 {
font-size: 2em;
}
.ql-snow .ql-editor h2 {
font-size: 1.5em;
}
.ql-snow .ql-editor h3 {
font-size: 1.17em;
}
.ql-snow .ql-editor h4 {
font-size: 1em;
}
.ql-snow .ql-editor h5 {
font-size: 0.83em;
}
.ql-snow .ql-editor h6 {
font-size: 0.67em;
}
.ql-snow .ql-editor a {
text-decoration: underline;
}
.ql-snow .ql-editor blockquote {
border-left: 4px solid #ccc;
margin-bottom: 5px;
margin-top: 5px;
padding-left: 16px;
}
.ql-snow .ql-editor code,
.ql-snow .ql-editor pre {
background-color: #f0f0f0;
border-radius: 3px;
}
.ql-snow .ql-editor pre {
white-space: pre-wrap;
margin-bottom: 5px;
margin-top: 5px;
padding: 5px 10px;
}
.ql-snow .ql-editor code {
font-size: 85%;
padding: 2px 4px;
}
.ql-snow .ql-editor pre.ql-syntax {
background-color: #23241f;
color: #f8f8f2;
overflow: visible;
}
.ql-snow .ql-editor img {
max-width: 100%;
}
.ql-snow .ql-picker {
color: #444;
display: inline-block;
float: left;
font-size: 14px;
font-weight: 500;
height: 24px;
position: relative;
vertical-align: middle;
}
.ql-snow .ql-picker-label {
cursor: pointer;
display: inline-block;
height: 100%;
padding-left: 8px;
padding-right: 2px;
position: relative;
width: 100%;
}
.ql-snow .ql-picker-label::before {
display: inline-block;
line-height: 22px;
}
.ql-snow .ql-picker-options {
background-color: #fff;
display: none;
min-width: 100%;
padding: 4px 8px;
position: absolute;
white-space: nowrap;
}
.ql-snow .ql-picker-options .ql-picker-item {
cursor: pointer;
display: block;
padding-bottom: 5px;
padding-top: 5px;
}
.ql-snow .ql-picker.ql-expanded .ql-picker-label {
color: #ccc;
z-index: 2;
}
.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill {
fill: #ccc;
}
.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
stroke: #ccc;
}
.ql-snow .ql-picker.ql-expanded .ql-picker-options {
display: block;
margin-top: -1px;
top: 100%;
z-index: 1;
}
.ql-snow .ql-color-picker,
.ql-snow .ql-icon-picker {
width: 28px;
}
.ql-snow .ql-color-picker .ql-picker-label,
.ql-snow .ql-icon-picker .ql-picker-label {
padding: 2px 4px;
}
.ql-snow .ql-color-picker .ql-picker-label svg,
.ql-snow .ql-icon-picker .ql-picker-label svg {
right: 4px;
}
.ql-snow .ql-icon-picker .ql-picker-options {
padding: 4px 0px;
}
.ql-snow .ql-icon-picker .ql-picker-item {
height: 24px;
width: 24px;
padding: 2px 4px;
}
.ql-snow .ql-color-picker .ql-picker-options {
padding: 3px 5px;
width: 152px;
}
.ql-snow .ql-color-picker .ql-picker-item {
border: 1px solid transparent;
float: left;
height: 16px;
margin: 2px;
padding: 0px;
width: 16px;
}
.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {
position: absolute;
margin-top: -9px;
right: 0;
top: 50%;
width: 18px;
}
.ql-snow
.ql-picker.ql-header
.ql-picker-label[data-label]:not([data-label=''])::before,
.ql-snow
.ql-picker.ql-font
.ql-picker-label[data-label]:not([data-label=''])::before,
.ql-snow
.ql-picker.ql-size
.ql-picker-label[data-label]:not([data-label=''])::before,
.ql-snow
.ql-picker.ql-header
.ql-picker-item[data-label]:not([data-label=''])::before,
.ql-snow
.ql-picker.ql-font
.ql-picker-item[data-label]:not([data-label=''])::before,
.ql-snow
.ql-picker.ql-size
.ql-picker-item[data-label]:not([data-label=''])::before {
content: attr(data-label);
}
.ql-snow .ql-picker.ql-header {
width: 98px;
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: 'Normal';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
content: 'Heading 1';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
content: 'Heading 2';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
content: 'Heading 3';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
content: 'Heading 4';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
content: 'Heading 5';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
content: 'Heading 6';
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
font-size: 2em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
font-size: 1.5em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
font-size: 1.17em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
font-size: 1em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
font-size: 0.83em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
font-size: 0.67em;
}
.ql-snow .ql-picker.ql-font {
width: 108px;
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: 'Sans Serif';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
content: 'Serif';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
content: 'Monospace';
}
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
font-family: Georgia, Times New Roman, serif;
}
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
font-family: Monaco, Courier New, monospace;
}
.ql-snow .ql-picker.ql-size {
width: 98px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: 'Normal';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
content: 'Small';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
content: 'Large';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
content: 'Huge';
}
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
font-size: 10px;
}
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
font-size: 18px;
}
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
font-size: 32px;
}
.ql-snow .ql-color-picker.ql-background .ql-picker-item {
background-color: #fff;
}
.ql-snow .ql-color-picker.ql-color .ql-picker-item {
background-color: #000;
}
.ql-toolbar.ql-snow {
border: 1px solid #ccc;
box-sizing: border-box;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
padding: 8px;
}
.ql-toolbar.ql-snow .ql-formats {
margin-right: 15px;
}
.ql-toolbar.ql-snow .ql-picker-label {
border: 1px solid transparent;
}
.ql-toolbar.ql-snow .ql-picker-options {
border: 1px solid transparent;
box-shadow: rgba(0, 0, 0, 0.2) 0 2px 8px;
}
.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label {
border-color: #ccc;
}
.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
border-color: #ccc;
}
.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,
.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover {
border-color: #000;
}
.ql-toolbar.ql-snow + .ql-container.ql-snow {
border-top: 0px;
}
.ql-snow .ql-tooltip {
background-color: #fff;
border: 1px solid #ccc;
box-shadow: 0px 0px 5px #ddd;
color: #444;
padding: 5px 12px;
white-space: nowrap;
}
.ql-snow .ql-tooltip::before {
content: 'Visit URL:';
line-height: 26px;
margin-right: 8px;
}
.ql-snow .ql-tooltip input[type='text'] {
display: none;
border: 1px solid #ccc;
font-size: 13px;
height: 26px;
margin: 0px;
padding: 3px 5px;
width: 170px;
}
.ql-snow .ql-tooltip a.ql-preview {
display: inline-block;
max-width: 200px;
overflow-x: hidden;
text-overflow: ellipsis;
vertical-align: top;
}
.ql-snow .ql-tooltip a.ql-action::after {
border-right: 1px solid #ccc;
content: 'Edit';
margin-left: 16px;
padding-right: 8px;
}
.ql-snow .ql-tooltip a.ql-remove::before {
content: 'Remove';
margin-left: 8px;
}
.ql-snow .ql-tooltip a {
line-height: 26px;
}
.ql-snow .ql-tooltip.ql-editing a.ql-preview,
.ql-snow .ql-tooltip.ql-editing a.ql-remove {
display: none;
}
.ql-snow .ql-tooltip.ql-editing input[type='text'] {
display: inline-block;
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: 'Save';
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode='link']::before {
content: 'Enter link:';
}
.ql-snow .ql-tooltip[data-mode='formula']::before {
content: 'Enter formula:';
}
.ql-snow .ql-tooltip[data-mode='video']::before {
content: 'Enter video:';
}
.ql-snow a {
color: #06c;
}
.ql-container.ql-snow {
border: 1px solid #ccc;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB