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",
"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": {
"version": "15.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.6.0.tgz",
"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": {
"version": "2.0.4",
"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",
"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": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@@ -1874,7 +1895,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
@@ -1945,6 +1965,15 @@
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.4.0.tgz",
"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": {
"version": "10.45.1",
"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": {
"version": "1.1.0",
"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",
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz",
@@ -2347,7 +2394,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": {
"object-keys": "^1.0.12"
}
@@ -2523,6 +2569,11 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"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": {
"version": "4.17.1",
"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",
"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": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz",
@@ -2657,6 +2713,12 @@
"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": {
"version": "0.6.1",
"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",
"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": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@@ -2757,7 +2824,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
@@ -2895,8 +2961,15 @@
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
"dev": true
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"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": {
"version": "2.0.1",
@@ -3043,6 +3116,15 @@
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"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": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -3065,6 +3147,14 @@
"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": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
@@ -3126,6 +3216,15 @@
"@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": {
"version": "1.0.0",
"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==",
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"object.assign": {
"version": "4.1.2",
@@ -3738,6 +3845,11 @@
"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": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
@@ -3818,6 +3930,15 @@
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -3846,6 +3967,11 @@
"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": {
"version": "1.0.0-next.22",
"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",
"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": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -4044,6 +4178,29 @@
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -4175,6 +4332,16 @@
"@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": {
"version": "1.3.0",
"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",
"@supabase/supabase-js": "^1.20.0",
"@tailwindcss/forms": "^0.3.2",
"@types/paystack": "^2.0.1",
"@types/pluralize": "0.0.29",
"body-parser": "^1.19.0",
"canvas-confetti": "^1.4.0",
"compression": "^1.7.1",
@@ -27,7 +29,10 @@
"lodash": "^4.17.21",
"marked": "^1.2.7",
"node-sass": "^6.0.0",
"paystack": "^2.0.1",
"pluralize": "^8.0.0",
"polka": "next",
"quill": "^1.3.7",
"sapper-environment": "^1.0.1",
"sirv": "^1.0.0",
"svelte-easy-crop": "^1.0.4",
@@ -47,6 +52,8 @@
"@rollup/plugin-replace": "^2.4.2",
"@rollup/plugin-typescript": "^8.3.0",
"@tailwindcss/typography": "^0.3.1",
"@types/lodash": "^4.14.191",
"carbon-components-svelte": "^0.70.13",
"carbon-icons-svelte": "^10.21.0",
"dotenv": "^10.0.0",
"postcss": "^8.0.9",
@@ -1,6 +1,6 @@
<script>
import Label from "./Label.svelte";
import Avatar from "./Avatar.svelte";
import Label from './Label.svelte';
import Avatar from './Avatar.svelte';
export let poll = {};
export let onSelect = () => {};
@@ -19,7 +19,7 @@
}
function getSelectedUsers(users) {
return users.map((o) => o.label).join(", ");
return users.map((o) => o.label).join(', ');
}
function getTotalVotes(options) {
@@ -103,7 +103,7 @@
<div class="mr-5">
<Label>Expiration</Label>
<p class="text-sm text-gray-600 mb-4">
{poll.expiration || "Today"}
{poll.expiration || 'Today'}
</p>
</div>
<div class="flex flex-col items-center">
@@ -170,7 +170,7 @@
{/each}
<div class="flex items-center">
<p class="text-sm text-gray-600">
Expires: {poll.expiration || "Today"}
Expires: {poll.expiration || 'Today'}
</p>
<span class="mx-2">|</span>
<p class="text-sm text-gray-600">
@@ -182,7 +182,7 @@
{#if isAuthor}
<div class="w-full text-center mt-3">
<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)}
>
{#if viewResult}
@@ -192,7 +192,7 @@
{/if}
</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}
>
Delete
+2 -2
View File
@@ -57,9 +57,9 @@
</button>
{#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}
<div class="block px-4 py-2 border-b">
<div class="block px-4 py-3 border-b">
<input
type="checkbox"
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="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}
<div class="flex items-center w-full justify-center pt-4">
<img
+4 -1
View File
@@ -9,13 +9,16 @@
p {
display: inline-block;
min-width: 20px;
padding: 0 6px;
font-size: 12px;
font-weight: 500;
line-height: 18px;
text-align: center;
border: 1px solid transparent;
border-radius: 2em;
width: 24px;
height: 24px;
padding-top: 2px;
}
.bg-set {
@@ -25,7 +25,7 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No, please don't"
onClick={() => {
@@ -34,7 +34,7 @@
}}
/>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Yes, delete"
onClick={() => {
@@ -132,13 +132,13 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No, cancel"
type="submit"
/>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label={isDeleting ? 'Deleting...' : 'Yes, delete'}
isDisabled={isDeleting}
@@ -174,7 +174,7 @@
</QuestionContainer>
<div class="float-right">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Delete this exercise"
onClick={() => (shouldDelete = true)}
@@ -111,7 +111,7 @@
}}
label="Save"
variant={VARIANTS.CONTAINED_SUCCESS}
className="py-2 px-8"
className="py-3 px-8"
/>
</div>
<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>
{#if STATUS.GRADED === $questionnaireMetaData.status}
<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"
>
Graded
</span>
{:else}
<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"
>
Pending
@@ -3,7 +3,7 @@
import { onMount } from 'svelte';
import AudioConsole32 from 'carbon-icons-svelte/lib/AudioConsole32';
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 PrimaryButton from '../../../../PrimaryButton/index.svelte';
import RoleBasedSecurity from '../../../../RoleBasedSecurity/index.svelte';
@@ -122,7 +122,7 @@
{#if isFetching}
<Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/if}
@@ -147,7 +147,7 @@
<div class="mt-5 flex items-center">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
label="Submit"
onClick={handleAddExercise}
/>
@@ -1,5 +1,6 @@
<script>
import MODES from '../../../../../utils/constants/mode.js';
import { formatYoutubeVideo } from '../../../../../utils/functions/formatYoutubeVideo';
import Tabs from '../../../../Tabs/index.svelte';
import TabContent from '../../../../TabContent/index.svelte';
@@ -24,34 +25,6 @@
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) {
if (prevMode === MODES.edit) {
handleUpdateLessonMaterials($lesson, lessonId);
@@ -146,7 +119,7 @@
width="100%"
height="569"
class="iframe"
src={formatYoutubeVideo(url)}
src={formatYoutubeVideo(url, errors)}
title="YouTube video player"
frameborder="0"
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 {
createLesson,
updateLesson,
deleteLesson,
} 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({
id: null,
totalExercises: 0,
@@ -19,7 +21,7 @@ export const lesson = writable({
export const isLessonDirty = writable(false);
export function handleAddLesson() {
lessons.update((_lessons) => {
lessons.update(((_lessons) => {
return [
..._lessons,
{
@@ -31,10 +33,10 @@ export function handleAddLesson() {
is_complete: false,
},
];
});
}) as Updater<any>);
}
export function handleDelete(lessonId) {
export function handleDelete(lessonId: Lesson['id']) {
return async () => {
// Need to implement soft delete
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);
const newLesson = {
title: lesson.title,
lesson_at: lesson.lesson_at,
call_url: lesson.call_url,
teacher_id: lesson.profile ? lesson.profile.id : null,
teacher_id: lesson.profile ? lesson.profile.id : undefined,
course_id,
is_complete: lesson.is_complete,
};
let updatedLesson;
let updatedLesson: any[] | null = null;
if (typeof lesson.id === 'string') {
// 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 = {
...lesson.materials,
};
@@ -171,7 +171,7 @@
<a
class="item flex items-center {(
path || $page.path
).includes(item.id) && 'active'} pl-7 py-2 {isStudent &&
).includes(item.id) && 'active'} pl-7 py-3 {isStudent &&
!item.is_complete
? 'cursor-not-allowed'
: ''}"
@@ -27,13 +27,13 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No"
onClick={() => ($deleteMemberModal.open = false)}
/>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="Yes"
onClick={handleDelete}
@@ -9,7 +9,7 @@
} from '../../../../utils/services/courses';
import { invitationModal, resetForm } from './store';
import { validateForm } from '../../../Courses/functions';
import { course, setCourseData } from '../../../Course/store';
import { course, setCourse } from '../../store';
let errors = {
emails: '',
@@ -47,7 +47,7 @@
addGroupMember(membersStack).then(async (membersAdded) => {
const group = await fetchGroup($course.group.id);
setCourseData(
setCourse(
{
...$course,
group: group.data,
@@ -90,7 +90,7 @@
/>
<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>
</form>
</Modal>
@@ -29,8 +29,7 @@
}
$settingsDialog.open = false;
const logo = await updateCourse($course.id, {
avatar,
const logo = await updateCourse($course.id, avatar, {
title: $course.title,
description: $course.description,
});
@@ -67,13 +66,13 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No, cancel"
type="submit"
/>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="Yes, delete"
onClick={handleDelete}
@@ -103,12 +102,12 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Delete"
onClick={() => (shouldDelete = true)}
/>
<PrimaryButton className="px-6 py-2" label="Update" type="submit" />
<PrimaryButton className="px-6 py-3" label="Update" type="submit" />
</div>
</form>
{/if}
+3 -3
View File
@@ -26,12 +26,12 @@ export function getLessonsRoute(courseId, lessonId) {
return `${path}/${lessonId}`;
}
export function getLectureNo(index) {
export function getLectureNo(index, initNo = '00') {
if (index <= 9) {
return `00${index}`;
return `${initNo}${index}`;
}
return `0${index}`;
return `${initNo.replace('0', '')}${index}`;
}
export function formatAnswers(data) {
+1 -1
View File
@@ -6,7 +6,7 @@
import PrimaryButton from '../PrimaryButton/index.svelte';
import PageBody from '../PageBody/index.svelte';
import EditContent from '../EditContent/index.svelte';
import { course } from '../Course/store';
import { course } from './store';
import { updateCourse } from '../../utils/services/courses';
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';
//@ts-ignore
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 = {
id: '434534534535',
@@ -27,7 +54,7 @@ export const group = writable({
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;
// const tutorsById = {};
@@ -38,27 +65,37 @@ export async function setCourseData(data, setLesson = true) {
people: [],
});
// @ts-ignore
if (Array.isArray(groupData.members)) {
// @ts-ignore
for (const member of groupData.members) {
if (member.role_id === ROLE.STUDENT) {
// @ts-ignore
groupData.students.push(member);
} else if (member.profile) {
// @ts-ignore
groupData.tutors.push(member.profile);
// tutorsById[member.profile.id] = member.profile;
}
}
// @ts-ignore
groupData.people = groupData.members;
}
// @ts-ignore
delete groupData.members;
// @ts-ignore
group.set(groupData);
}
if (setLesson) {
// @ts-ignore
const orderedLessons = (data.lessons || [])
// @ts-ignore
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
// @ts-ignore
.sort((a, b) => a.order - b.order);
// .map((lesson) => ({
// ...lesson,
@@ -67,6 +104,29 @@ export async function setCourseData(data, setLesson = true) {
lessons.set(orderedLessons);
}
//@ts-ignore
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);
}
+2 -2
View File
@@ -1,5 +1,5 @@
<script>
import { Firework } from 'svelte-loading-spinners';
import { Moon } from 'svelte-loading-spinners';
import Navigation from '../Course/components/Navigation/index.svelte';
import Backdrop from '../../components/Backdrop/index.svelte';
// import { lessons, lesson } from '../Course/components/Lesson/store/lessons';
@@ -32,7 +32,7 @@
{#if isFetching}
<Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/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 IconButton from '../../../IconButton/index.svelte';
let show = true;
export let show = true;
</script>
<section
class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}"
>
{#if show}
<div class="relative h-full bg-gray-100">
<div class="relative h-full bg-white">
<slot />
</div>
{/if}
@@ -46,6 +46,7 @@
min-width: 300px;
max-width: 300px;
top: 0;
left: 0;
border-right: 1px solid var(--border-color);
background-color: rgb(250, 251, 252);
@@ -1,6 +1,17 @@
<script>
import EditContent from '../../../EditContent/index.svelte';
import { landingPageStore } from '../../store';
import { set, get, cloneDeep } from 'lodash';
import TextEditor from '../../../TextEditor/index.svelte';
export let course;
</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>
import TextArea from '../../../Form/TextArea.svelte';
import TextField from '../../../Form/TextField.svelte';
import { landingPageStore } from '../../store';
export let course = {};
</script>
<TextArea
<TextField
className="mt-5"
labelClassName="font-bold"
label="Title"
bind:value={$landingPageStore.header.title}
rows="3"
className="mt-3 font-bold"
bgColor="bg-white"
bind:value={course.title}
/>
<TextArea
label="Description"
bind:value={$landingPageStore.header.description}
bind:value={course.description}
rows="6"
className="mt-3 font-bold"
bgColor="bg-white"
className="mt-5"
labelClassName="font-bold"
/>
<TextField
className="mt-3 font-bold"
label="Currency"
bind:value={$landingPageStore.header.currency}
/>
<TextField
className="mt-3 font-bold"
label="Cost"
bind:value={$landingPageStore.header.cost}
/>
<TextField
label="Button Label"
className="mt-3 font-bold"
bind:value={$landingPageStore.header.buttonLabel}
className="mt-5"
labelClassName="font-bold"
label="Short Video"
placeholder="www.youtube.com/watch?v=uYRq60G5XTk"
helperMessage="Enter a link to a youtube video"
type="text"
bind:value={course.metadata.videoUrl}
/>
@@ -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>
import EditContent from '../../../EditContent/index.svelte';
import { landingPageStore } from '../../store';
import { set, get, cloneDeep } from 'lodash';
import TextEditor from '../../../TextEditor/index.svelte';
export let course;
</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 RequirementForm from './RequirementForm.svelte';
import DescriptionForm from './DescriptionForm.svelte';
import SyllabusForm from './SyllabusForm.svelte';
import AboutUsForm from './AboutUsForm.svelte';
import PricingForm from './PricingForm.svelte';
import GoalsForm from './GoalsForm.svelte';
import InstructorForm from './InstructorForm.svelte';
import PrimaryButton from '../../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../../PrimaryButton/constants';
import { updateCourse } from '../../../../utils/services/courses';
export let course;
export let courseId;
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
let loading = false;
const sections = [
{
@@ -31,11 +36,15 @@
},
{
key: 4,
title: 'Syllabus',
title: 'Goals',
},
{
key: 5,
title: 'About Us',
title: 'Instructor',
},
{
key: 6,
title: 'Pricing',
},
];
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>
<Container>
<Container show={true}>
<!-- Publish Header Section -->
{#if !selectedSection}
<div
class="flex justify-between items-center px-2 {borderBottomGrey} w-full"
>
<div class="flex justify-between items-center px-2 w-full">
<CloseButton onClick={handleClose} />
<PrimaryButton
label="Publish"
onClick={handlePublish}
disablePadding={true}
/>
<div class="flex items-center">
<PrimaryButton
label="Save"
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
class="flex justify-between items-center px-2 bg-white {borderBottomGrey} w-full mb-2"
>
<div class="flex justify-between items-center px-2 w-full mb-2">
<h3 class="">Page Builder</h3>
</div>
{#each sections as section}
<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)}
>
<p class="mr-2">{section.title} section</p>
@@ -85,24 +131,27 @@
</button>
{/each}
{:else}
<div class="flex items-center bg-white {borderBottomGrey} w-full">
<!-- Title -->
<div class="flex items-center {borderBottomGrey} w-full">
<IconButton onClick={handleClose}>
<ArrowLeft24 class="carbon-icon" title="Go back" />
</IconButton>
<h3 class="">{selectedSection.title}</h3>
</div>
<div class="p-2">
<div class="p-2 h-4/5 overflow-y">
{#if selectedSection.key === 1}
<HeaderForm />
<HeaderForm bind:course />
{:else if selectedSection.key === 2}
<RequirementForm />
<RequirementForm bind:course />
{:else if selectedSection.key === 3}
<DescriptionForm />
<DescriptionForm bind:course />
{:else if selectedSection.key === 4}
<SyllabusForm />
<GoalsForm bind:course />
{:else if selectedSection.key === 5}
<AboutUsForm />
<InstructorForm bind:course />
{:else if selectedSection.key === 6}
<PricingForm bind:course />
{/if}
</div>
{/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 = {
HEADER: 'header',
REQUIREMENT: 'requirement',
DESCRIPTION: 'description',
SYLLABUS: 'syllabus',
ABOUT_US: 'about-us',
REQUIREMENT: '#requirement',
DESCRIPTION: '#description',
GOALS: '#goals',
LESSONS: '#lessons',
INSTRUCTOR: '#instructor',
};
export const NAV_ITEMS = [
@@ -16,11 +17,15 @@ export const NAV_ITEMS = [
label: 'Description',
},
{
key: SECTION_KEYS.SYLLABUS,
label: 'Syllabus',
key: SECTION_KEYS.GOALS,
label: 'Goals',
},
{
key: SECTION_KEYS.ABOUT_US,
label: 'About Us',
key: SECTION_KEYS.LESSONS,
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 RecentlyViewed16 from 'carbon-icons-svelte/lib/RecentlyViewed16';
import CurrencyDollar16 from 'carbon-icons-svelte/lib/CurrencyDollar16';
import Alarm16 from 'carbon-icons-svelte/lib/Alarm16';
import pluralize from 'pluralize';
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 MarkdownRender from '../MarkdownRender/index.svelte';
import { NAV_ITEMS, SECTION_KEYS } from './constants';
import { landingPageStore } from './store';
import PricingSection from './components/PricingSection.svelte';
import { getLectureNo } from '../Course/function';
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 paystackApi = paystack('');
function handleEnrol() {}
function handleNavItemClick(navItem) {
return () => {
activeNavItem = navItem;
};
let activeNav = NAV_ITEMS[0].key;
let instructor = {};
let video: string | undefined;
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(() => {
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>
<svelte:head>
<title>{$landingPageStore.header.title}</title>
<title>{get(courseData, 'title', '')}</title>
</svelte:head>
<div class="w-full bg-white flex flex-col items-center">
<section
class="w-full flex items-center justify-center bg-gray-900 text-white p-6 pb-10"
>
<div class="container flex justify-between items-center">
<div class="course-description mr-3">
<h1>
{$landingPageStore.header.title}
<!-- Header Section -->
<header class="banner w-full flex items-center justify-center p-">
<div
class="md:w-5/6 w-full flex items-center justify-between flex-col-reverse md:flex-row"
>
<!-- Course Description -->
<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>
<h5>
{$landingPageStore.header.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 class="text-md text-white mb-6">
{get(courseData, 'description', '')}
</p>
<p class="mb-2 text-black text-md flex items-center">
<CurrencyDollar16 class="carbon-icon active mr-2" />
{$landingPageStore.header.currency}{$landingPageStore.header.cost}
<p class="author my-3 text-sm">
{get(courseData, 'metadata.instructor.name', '')}
</p>
<PrimaryButton
label={$landingPageStore.header.buttonLabel}
onClick={handleEnrol}
className="md:w-full"
label="Start Course"
className="px-6 py-5 mt-6 sm:w-fit"
/>
</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>
</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
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"
>
{#each NAV_ITEMS as navItem}
<p
class="text-md text-gray-900 {activeNavItem === navItem.key &&
'font-bold'}"
on:click={handleNavItemClick(navItem.key)}
>
<a href="{$page.path}#{navItem.key}">{navItem.label}</a>
</p>
{/each}
</nav>
<!-- Sections - Requirement -->
<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>
<ul class="list font-light">
{@html get(courseData, 'metadata.requirements', '')}
</ul>
</section>
<Expandable
id={SECTION_KEYS.REQUIREMENT}
title="Requirement"
titleClass="text-2xl"
rootClass={expandableRootClass}
supportsLink={true}
>
<MarkdownRender
content={$landingPageStore.requirement.content}
disableMaxWidth={true}
/>
</Expandable>
<!-- Sections - Course Description -->
<section id="description" class="border-b border-gray-300 mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0 mb-3">Course Description</h3>
<p class="text-sm font-light">
{@html get(courseData, 'metadata.description', '')}
</p>
</section>
<Expandable
id={SECTION_KEYS.DESCRIPTION}
title="Description"
titleClass="text-2xl"
rootClass={expandableRootClass}
supportsLink={true}
>
<MarkdownRender
content={$landingPageStore.description.content}
disableMaxWidth={true}
/>
</Expandable>
<!-- Sections - Goal -->
<section id="goals" class="mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0 mb-3">What you'll learn</h3>
<ul class="list font-light">
{@html get(courseData, 'metadata.goals', '')}
</ul>
</section>
<Expandable
id={SECTION_KEYS.SYLLABUS}
title="Syllabus"
titleClass="text-2xl"
rootClass={expandableRootClass}
supportsLink={true}
>
{#each $landingPageStore.syllabus.lessons as lesson, index}
<div class="flex items-center my-2">
<strong class="mr-2">Lesson {index + 1}. </strong>
{lesson.title}
<!-- Sections - Certificate -->
<section class="border-b border-gray-300 mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0">Certificate</h3>
<p class="text-sm font-light mb-3">
When you complete all of the courses in the program, you'll earn a
Certificate to share with your professional network.
</p>
<img
src="/images/certificate-template.svg"
alt="certificate template"
/>
</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>
{/each}
</Expandable>
<Expandable
id={SECTION_KEYS.ABOUT_US}
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"
/>
<!-- Pricing Details -->
<PricingSection className="hidden md:block" {courseData} />
</div>
<MarkdownRender
content={$landingPageStore.aboutUs.description}
disableMaxWidth={true}
/>
</Expandable>
</div>
</div>
<style lang="scss">
.container {
max-width: 800px;
.banner {
background-color: #040f2d;
min-height: 472px;
}
.course-description {
max-width: 500px;
}
.author {
color: #7888b7;
}
.action-container {
height: fit-content;
width: 200px;
}
.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;
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>
@@ -35,7 +35,7 @@
</script>
<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
options={getOptions()}
@@ -57,7 +57,7 @@
<div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
label="Create"
type="submit"
isLoading={$copyCourseModal.isSaving}
@@ -128,7 +128,7 @@
/>
<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>
</form>
</Modal>
+2 -2
View File
@@ -1,6 +1,6 @@
<script>
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 CopyCourseModal from './components/CopyCourseModal/index.svelte';
import { courses, courseMetaDeta } from './store';
@@ -13,7 +13,7 @@
>
{#if $courseMetaDeta.isLoading}
<Box>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Box>
{:else}
{#each $courses as courseData}
+3 -3
View File
@@ -8,7 +8,7 @@
let open = false;
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) {
return index === options.length - 1;
@@ -41,11 +41,11 @@
{#if open}
<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}
<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)}
>
{option.label}
+9 -6
View File
@@ -1,18 +1,21 @@
<script>
</script>
<style>
/* your styles go here */
</style>
<!-- markup (zero or more items) goes here -->
<div class="my-1">
<hr />
<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
class="underline text-blue-700"
href="https://twitter.com/rotimi_best"
target="_blank">Rotimi Best</a></span>
target="_blank">Rotimi Best</a
></span
>
</div>
</div>
<style>
/* your styles go here */
</style>
+3 -2
View File
@@ -5,7 +5,8 @@
export let rows = '3';
export let maxRows = 3;
export let className = '';
export let bgColor = 'bg-gray-100';
export let labelClassName = '';
export let bgColor = 'bg-gray-200';
export let helperMessage = '';
export let errorMessage = '';
export let isRequired = false;
@@ -17,7 +18,7 @@
<label class="block w-full relative {className}">
{#if label}
<p for="text-field" class="">
<p for="text-field" class={labelClassName}>
{label}
{#if isRequired}
+4 -2
View File
@@ -19,6 +19,7 @@
export let helperMessage = '';
export let autoComplete = true;
export let onChange = () => {}; // This is to know if element is 'dirty'
export let onInputChange = () => {};
let ref;
let fieldNode;
@@ -61,10 +62,11 @@
{/if}
<input
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'
: ''}"
on:keydown={onKeyDown}
on:change={onInputChange}
{placeholder}
bind:value
bind:this={ref}
@@ -92,7 +94,7 @@
<style>
.password-eye {
position: absolute;
top: 33px;
top: 23px;
right: 6px;
}
</style>
+2 -2
View File
@@ -93,13 +93,13 @@
<PrimaryButton
label="Login"
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')}
/>
<PrimaryButton
label="Sign Up"
variant={VARIANTS.CONTAINED}
className="py-2 px-6 mx-2"
className="py-3 px-6 mx-2"
onClick={() => goto('/signup')}
/>
</div>
+4 -4
View File
@@ -13,7 +13,7 @@
export let isLoading = false;
$: variant = isLoading ? VARIANTS.OUTLINED : variant;
$: isDisabled = isLoading ? true : isDisabled;
$: isDisabled = isLoading ? true : false;
</script>
<button
@@ -21,15 +21,15 @@
'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'} flex items-center h-auto {VARIANTS_CLASS[
variant
]} {!disablePadding &&
'py-1 px-5'} rounded-none {className} py-2 w-full justify-center sm:w-auto {isLoading &&
'py-2 border-blue-700'} hover:shadow-xl transition delay-150 duration-300 ease-in-out"
'py-1 px-5'} rounded-none {className} py-3 w-fit justify-center sm:w-auto {isLoading &&
'py-3 border-blue-700'} hover:shadow-xl transition delay-150 duration-300 ease-in-out"
on:click={onClick}
{name}
{type}
disabled={isDisabled}
>
{#if isLoading}
<Moon size="30" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="20" color="#1d4ed8" unit="px" duration="1s" />
{:else if !!label}
{label}
{:else}
+1
View File
@@ -20,6 +20,7 @@
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
+70 -82
View File
@@ -1,89 +1,77 @@
<script>
import marked from 'marked';
import PrimaryButton from '../PrimaryButton/index.svelte';
import { onMount } from 'svelte';
let isWriteMode = true;
export let value = ``;
export let placeholder = ``;
const activeClassName = 'border-b-2 border-blue-700';
export let onChange;
export let content;
export let disable;
export let docId;
export let placeholder = 'Type something...';
const handleTabClick = (tab) => (event) => {
isWriteMode = tab === 1;
const settings = {
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>
<div class="root mt-5 border border-gray rounded-md">
<div class="p-2 pb-0">
<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 class="p-2 w-full h-full">
<div bind:this={container} />
</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>
<title>{status}</title>
<link
rel="stylesheet"
href="https://unpkg.com/carbon-components-svelte/css/g90.css"
/>
</svelte:head>
<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 { stores, goto } from '@sapper/app';
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 Navigation from '../components/Navigation/index.svelte';
import Snackbar from '../components/Snackbar/index.svelte';
@@ -91,7 +91,7 @@
// If user coming to login page, then
if (!$profile.role) {
goto(ROUTE.ONBOARDING);
} else {
} else if (['login', 'signup', 'onboarding'].includes(path)) {
goto('/dashboard');
}
}
@@ -161,6 +161,10 @@
</script>
<svelte:window on:resize={handleResize} />
<svelte:head>
<link href="/css/carbon.css" rel="stylesheet" />
</svelte:head>
<Tailwindcss />
<!-- <Nav {segment} /> -->
@@ -169,7 +173,7 @@
<main>
{#if $preloading && $delayedPreloading}
<Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/if}
{#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">
{#each boxes as box}
<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'}
<UserSpeaker32 class="carbon-icon" />
@@ -106,7 +106,7 @@
<div class="flex items-center justify-evenly mt-16">
<div class="table rounded-md bg-white shadow-md">
<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>
{#each topStudents as topStudent, index}
@@ -123,7 +123,7 @@
<div class="table rounded-md bg-white shadow-md">
<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>
{#each topCourses as topCourse, index}
+17 -17
View File
@@ -1,22 +1,22 @@
<script>
import ChevronDown24 from "carbon-icons-svelte/lib/ChevronDown24";
import TextEditor from "../components/TextEditor/index.svelte";
import ChevronDown24 from 'carbon-icons-svelte/lib/ChevronDown24';
import TextEditor from '../components/TextEditor/index.svelte';
let questionTitle = "";
let questionDescription = "";
let questionTitle = '';
let questionDescription = '';
let facultyFilterOpened = false;
const faculties = [
"ИЭКСУ",
"ИМБТ",
"ИМИ",
"ИПТДМ",
"ИИИР",
"ГФ",
"ХТФ",
"ИДЗО",
"УИИ",
"УНИ",
'ИЭКСУ',
'ИМБТ',
'ИМИ',
'ИПТДМ',
'ИИИР',
'ГФ',
'ХТФ',
'ИДЗО',
'УИИ',
'УНИ',
];
let checkbox = faculties.reduce((acc, cur) => {
acc[cur] = false;
@@ -38,7 +38,7 @@
<div class="main-title my-1 flex">
<div class="faculty-filter mr-1">
<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}
>
<span class="flex flex-row"
@@ -49,10 +49,10 @@
{#if facultyFilterOpened}
<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}
<div class="block px-4 py-2 border-b">
<div class="block px-4 py-3 border-b">
<input
type="checkbox"
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>
import Backdrop from '../../components/Backdrop/index.svelte';
import { Moon } from 'svelte-loading-spinners';
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;
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>
<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 PageBody from '../../../components/PageBody/index.svelte';
import Box from '../../../components/Box/index.svelte';
import {
course,
group,
setCourseData,
} from '../../../components/Course/store';
import { course, group, setCourse } from '../../../components/Course/store';
import { getLectureNo } from '../../../components/Course/function';
import { fetchCourse } from '../../../utils/services/courses';
import { lessons } from '../../../components/Course/components/Lesson/store/lessons';
@@ -120,7 +116,7 @@
}
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
setAttendance(data);
});
+2 -2
View File
@@ -7,7 +7,7 @@
<script>
import Overview from '../../../components/Course/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';
export let courseId;
@@ -19,7 +19,7 @@
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
prevCourseId = courseId;
}
+22 -2
View File
@@ -9,8 +9,28 @@
import CourseLandingPage from '../../../components/CourseLandingPage/index.svelte';
import Editor from '../../../components/CourseLandingPage/components/Editor/index.svelte';
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;
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>
<div
@@ -18,9 +38,9 @@
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<Editor {courseId} />
<Editor {courseId} bind:course={courseData} />
<div class="rightBar" class:isMobile={$isMobile}>
<CourseLandingPage />
<CourseLandingPage bind:courseData />
</div>
</div>
@@ -29,10 +29,7 @@
import Materials from '../../../../../components/Course/components/Lesson/Materials/index.svelte';
import Exercises from '../../../../../components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '../../../../../utils/constants/mode.js';
import {
setCourseData,
course,
} from '../../../../../components/Course/store';
import { setCourse, course } from '../../../../../components/Course/store';
import { lesson } from '../../../../../components/Course/components/Lesson/store/lessons';
export let courseId;
@@ -53,14 +50,10 @@
let lessonData;
if (!$course.id) {
const [_course, lesson] = await Promise.all([
fetchCourse(courseId),
fetchLesson(lessonId),
]);
const { data } = await fetchCourse(courseId);
lessonData = lesson.data;
setCourseData(_course.data);
lessonData = data.lessons.find((lesson) => lesson.id === lessonId);
setCourse(data);
} else if (prevLessonId !== lessonId) {
const lesson = await fetchLesson(lessonId);
lessonData = lesson.data;
+2 -2
View File
@@ -36,7 +36,7 @@
} from '../../../../components/Course/components/Lesson/store/lessons';
import {
course,
setCourseData,
setCourse,
group,
} from '../../../../components/Course/store';
@@ -107,7 +107,7 @@
onMount(async () => {
if ($course.id) return;
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 Box from '../../../components/Box/index.svelte';
import { ROLE } from '../../../utils/constants/roles';
import {
setCourseData,
course,
group,
} from '../../../components/Course/store';
import { setCourse, course, group } from '../../../components/Course/store';
import { lessons } from '../../../components/Course/components/Lesson/store/lessons';
import {
fetchCourse,
@@ -50,7 +46,7 @@
onMount(async () => {
if (!$course.id) {
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
}
const { data: marks } = await fetchMarks(courseId);
+2 -6
View File
@@ -25,11 +25,7 @@
import IconButton from '../../../components/IconButton/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import RoleBasedSecurity from '../../../components/RoleBasedSecurity/index.svelte';
import {
setCourseData,
course,
group,
} from '../../../components/Course/store';
import { setCourse, course, group } from '../../../components/Course/store';
import InviationModal from '../../../components/Course/components/People/InviationModal.svelte';
import DeleteConfirmation from '../../../components/Course/components/People/DeleteConfirmation.svelte';
import {
@@ -102,7 +98,7 @@
onMount(async () => {
if ($course.id) return;
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
});
$: sortAndFilterPeople($group.people, filterBy);
+2 -2
View File
@@ -20,12 +20,12 @@
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import { setCourseData } from '../../../components/Course/store';
import { setCourse } from '../../../components/Course/store';
export let courseData = {};
onMount(() => {
setCourseData(courseData);
setCourse(courseData);
});
</script>
+3 -3
View File
@@ -16,7 +16,7 @@
import PageBody from '../../../components/PageBody/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { fetchCourse } from '../../../utils/services/courses';
import { setCourseData, course } from '../../../components/Course/store';
import { setCourse, course } from '../../../components/Course/store';
import {
fetchSubmissions,
updateSubmission,
@@ -182,7 +182,7 @@
onMount(async () => {
if (!$course.id) {
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
}
const { data: submissions } = await fetchSubmissions(
@@ -297,7 +297,7 @@
<div
class="border {item.isEarly
? '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 }}
>
<div
+2 -2
View File
@@ -20,12 +20,12 @@
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import { setCourseData } from '../../../components/Course/store';
import { setCourse } from '../../../components/Course/store';
export let courseData = {};
onMount(() => {
setCourseData(courseData);
setCourse(courseData);
});
</script>
+2 -1
View File
@@ -65,7 +65,8 @@
<p class="text-md mb-6 text-center">
We have sent a confirmation email to <span class="text-blue-700"
>{fields.email}</span
>. Kindly check your inbox to reset password.
>. Kindly check your inbox to reset password or spam to reset your
password.
</p>
</div>
+1 -1
View File
@@ -97,7 +97,7 @@
<div class="flex flex-col md:flex-row">
{#each areas as area, index}
<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">
{#if index === 0}
+1 -1
View File
@@ -287,7 +287,7 @@
<PrimaryButton
label="Back"
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)}
/>
{/if}
+3 -3
View File
@@ -183,7 +183,7 @@
This are the organizations you work at
</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">
<a class="text-blue-700 font-bold" href="/courses"> CitiBim </a>
</h4>
@@ -204,7 +204,7 @@
</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">
<a class="text-blue-700 font-bold" href="/courses">
Climate Change Group
@@ -226,7 +226,7 @@
</div>
</div>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Log out"
onClick={logout}
+6 -3
View File
@@ -12,7 +12,8 @@
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
<link rel="icon" type="image/png" href="favicon.ico">
<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 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C7WBN0S06R"></script>
<script>
@@ -23,10 +24,12 @@
gtag('config', 'G-C7WBN0S06R');
</script>
<!--Crisp Chat -->
<!--Crisp Chat
<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`
and anything else it needs to hydrate the app and
initialise the router -->
+7 -1
View File
@@ -21,4 +21,10 @@ export const PUBLIC_ROUTES = [
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;
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}$'
);
if (s === null) {
if (m === null) {
return false;
}
return true;
}
View File
@@ -1,22 +1,24 @@
import { supabase } from '../../functions/supabase';
import { isUUID } from '../../functions/isUUID';
import { QUESTION_TYPE } from '../../../components/Question/constants';
import type {
Lesson,
Course,
Group,
Groupmember,
Exercise,
} from '../../../utils/types';
export async function fetchCourse(courseId, session) {
console.log(`courseId`, courseId);
let jsonUser;
export async function fetchCourse(
courseId?: Course['id'],
slug?: Course['slug']
) {
let match: { slug?: string; id?: string } = {};
if (!process.browser) {
// try {
// jsonUser = JSON.parse(session.user);
// } catch (error) {
// console.log('Token is expired probably cause user not found');
// return {
// data: [],
// cantFetch: true,
// };
// // return this.redirect(301, '/login');
// }
if (slug) {
match.slug = slug;
} else {
match.id = courseId;
}
const { data, error } = await supabase
@@ -33,14 +35,19 @@ export async function fetchCourse(courseId, session) {
profile(*)
)
),
slug,
cost,
currency,
metadata,
lessons:lesson(
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)
),
attendance:group_attendance(*)
`
)
.match({ id: courseId })
.match(match)
.single();
console.log(`error`, 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
.from('group')
.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
.from('groupmember')
.update({
@@ -84,13 +94,41 @@ export async function setProfileIdOfGroupMember(email, profileId) {
};
}
export async function updateCourse(courseId, course) {
if (course.avatar) {
export async function uploadAvatar(courseId: string, avatar: string) {
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 { data } = await supabase.storage
.from('avatars')
.upload(filename, course.avatar, {
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
@@ -100,12 +138,12 @@ export async function updateCourse(courseId, course) {
.from('avatars')
.getPublicUrl(filename);
if (!publicURL) return;
course.logo = publicURL;
}
}
delete course.avatar;
await supabase
.from('course')
.update(course, { returning: 'minimal' })
@@ -114,28 +152,28 @@ export async function updateCourse(courseId, course) {
return course.logo;
}
export async function deleteCourse(courseId) {
export async function deleteCourse(courseId: Course['id']) {
return await supabase.from('course').delete().match({ id: courseId });
}
export function addGroupMember(member) {
export function addGroupMember(member: any) {
return supabase.from('groupmember').insert(member, {
returning: 'minimal',
});
}
export function updatedGroupMember(update, match) {
export function updatedGroupMember(update: any, match: any) {
return supabase
.from('groupmember')
.update(update, { returning: 'minimal' })
.match(match);
}
export function deleteGroupMember(groupMemberId) {
export function deleteGroupMember(groupMemberId: Groupmember['id']) {
return supabase.from('groupmember').delete().match({ id: groupMemberId });
}
export function fetchLesson(lessonId) {
export function fetchLesson(lessonId: Lesson['id']) {
return supabase
.from('lesson')
.select(
@@ -145,35 +183,39 @@ export function fetchLesson(lessonId) {
.single();
}
export function createLesson(lesson) {
export function createLesson(lesson: any) {
return supabase.from('lesson').insert(lesson);
}
export function updateLesson(lesson, lessonId) {
export function updateLesson(lesson: any, lessonId: Lesson['id']) {
return supabase
.from('lesson')
.update({ ...lesson, id: undefined }, { returning: 'minimal' })
.match({ id: lessonId });
}
export function deleteLesson(lessonId) {
export function deleteLesson(lessonId: Lesson['id']) {
// Need to implement soft delete
return supabase.from('lesson').delete().match({ id: lessonId });
}
export function createExercise(exercise) {
export function createExercise(exercise: any) {
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);
}
function isNew(item) {
function isNew(item: any) {
return isNaN(item);
}
export async function upsertExercise(questionnaire, exerciseId) {
// TODO: Add questionnaire type
export async function upsertExercise(
questionnaire: any,
exerciseId: Exercise['id']
) {
const {
questions,
title,
@@ -263,10 +305,12 @@ export async function upsertExercise(questionnaire, exerciseId) {
// Delete cause this is not a field in the table
delete newQuestion.question_type_id;
// @ts-ignore
newQuestion.question_type = { id: question_type_id };
newQuestion.id = id;
newQuestion.name = name;
newQuestion.order = order;
// @ts-ignore
newQuestion.options = [];
// Don't map options for 'Paragraph' questions
@@ -297,9 +341,11 @@ export async function upsertExercise(questionnaire, exerciseId) {
if (is_dirty || isNew(option.id)) {
const { data } = await supabase.from('option').upsert(newOption);
if (Array.isArray(data)) {
// @ts-ignore
newQuestion.options.push(data[0]);
}
} else {
// @ts-ignore
newQuestion.options.push(newOption);
}
}
@@ -312,12 +358,16 @@ export async function upsertExercise(questionnaire, exerciseId) {
return updatedQuestions;
}
interface LooseObject {
[key: string]: any;
}
export async function submitExercise(
answers,
questions,
exerciseId,
courseId,
groupMemberId
answers: Array<string>,
questions: Array<{ name: string; id: string }>,
exerciseId: Exercise['id'],
courseId: Course['id'],
groupMemberId: Groupmember['id'] | undefined
) {
if (!groupMemberId) {
return;
@@ -326,7 +376,7 @@ export async function submitExercise(
const questionsByName = questions.reduce(
(acc, q) => ({ ...acc, [q.name]: q.id }),
{}
);
) as LooseObject;
const questionAnswers = [];
const { data: submission } = await supabase.from('submission').insert({
@@ -343,7 +393,7 @@ export async function submitExercise(
question_id: questionsByName[questionName],
open_answer: '',
answers: [],
submission_id: submission[0].id,
submission_id: Array.isArray(submission) ? submission[0].id : null,
};
if (typeof value === 'string') {
@@ -359,7 +409,10 @@ export async function submitExercise(
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) {
const { id } = question;
+79 -68
View File
@@ -1,25 +1,30 @@
export * from './config';
export * from './dashboard';
export interface CurrentSessionType {
access_token: string;
expires_at: number;
expires_in: number;
refresh_token: string;
token_type: string;
}
export interface UserType {
openAuthModal: boolean;
fetchingUser: boolean | null;
isLoggedIn: boolean;
currentSession: CurrentSessionType | null;
expiresAt: number;
interface CourseMetadata {
requirements?: string;
description?: string;
goals?: string;
videoUrl?: '';
showDiscount: false;
discount: 0;
reward?: {
show: boolean;
description: string;
};
instructor?: {
name: string;
role: string;
coursesNo: number;
description: string;
imgUrl: string;
};
}
// Generated from https://supabase-schema.vercel.app/
export interface Organization {
id: string /* primary key */;
name: string;
name: any; // type unknown;
}
export interface Profile {
@@ -29,27 +34,30 @@ export interface Profile {
avatar_url?: string;
created_at?: string;
updated_at?: string;
email?: string;
email?: any; // type unknown;
can_add_course?: boolean;
role?: any; // type unknown;
goal?: any; // type unknown;
source?: any; // type unknown;
}
export interface Question_type {
id: number /* primary key */;
label: string;
label: any; // type unknown;
created_at?: string;
updated_at?: string;
typename?: string;
typename?: any; // type unknown;
}
export interface Submissionstatus {
id: number /* primary key */;
label: string;
label: any; // type unknown;
updated_at?: string;
}
export interface Group {
id: string /* primary key */;
name: string;
name: any; // type unknown;
description?: string;
created_at?: string;
updated_at?: string;
@@ -58,8 +66,8 @@ export interface Group {
}
export interface Role {
type: string;
description?: string;
type: any; // type unknown;
description?: any; // type unknown;
id: number /* primary key */;
updated_at?: string;
created_at?: string;
@@ -67,24 +75,27 @@ export interface Role {
export interface Waitinglist {
id: number /* primary key */;
email: string;
email: any; // type unknown;
created_at?: string;
}
export interface Course {
created_at?: string;
description: string;
group_id?: string /* foreign key to group.id */;
group?: Group;
title: any; // type unknown;
description: any; // type unknown;
overview?: any; // type unknown;
id: string /* primary key */;
is_template?: boolean;
lessons: Lesson[];
logo?: string;
organization_id?: string /* foreign key to organization.id */;
organization?: Organization;
overview?: string;
title: string;
created_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 {
@@ -92,9 +103,9 @@ export interface Groupmember {
group_id: string /* foreign key to group.id */;
role_id: number /* foreign key to role.id */;
profile_id?: string /* foreign key to profile.id */;
email?: string;
email?: any; // type unknown;
created_at?: string;
assigned_student_id?: string;
assigned_student_id?: any; // type unknown;
group?: Group;
role?: Role;
profile?: Profile;
@@ -123,46 +134,47 @@ export interface Group_attendance {
}
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 */;
created_at: string;
exercise?: Exercise[];
id: string /* primary key */;
is_complete?: boolean;
lesson_at?: string;
note?: string;
order: number;
profile?: Profile;
public?: boolean;
slide_url?: string;
teacher_id?: string /* foreign key to profile.id */;
title: string;
created_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 {
title: any; // type unknown;
description?: any; // type unknown;
lesson_id?: string /* foreign key to lesson.id */;
created_at?: string;
description?: string;
due_by?: string;
id: string /* primary key */;
lesson_id: string /* foreign key */;
questions?: Question[];
title: string;
updated_at?: string;
id: string /* primary key */;
due_by?: string;
lesson?: Lesson;
}
export interface Question {
created_at?: string;
exercise_id: string /* foreign key to exercise.id */;
id: number /* primary key */;
name?: string;
options: Option[];
order?: number;
points?: string;
question_type_id: number /* foreign key to question_type.id */;
title: string;
title: any; // type unknown;
created_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 {
@@ -182,15 +194,14 @@ export interface Submission {
}
export interface Option {
created_at?: string;
id?: number /* primary key */;
id: number /* primary key */;
label: any; // type unknown;
is_correct: boolean;
is_dirty?: boolean;
label: string;
question_id: number /* foreign key to question.id */;
question?: Question;
updated_at?: string;
value?: string;
created_at?: string;
updated_at?: string;
question?: Question;
}
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