feat: move away from carbon components and TinyMCE (#597)
* feat: remove tinymce & replace org sidebar * chore: PR Fixes * chore: move ui font into src dir * remove files in vite config * use google font import for loading font * remove custom font * more fonts update * remove font bold * more removals of font bold * fix import * checkpoint * chore: checkpoint (add course & lms shadcn sidebar) * minor css improvements and try out font sans * chore: replace carbon components with shadcn's * chore: replace carbon components with shadcn _ v3 * chore: ui fixes & storybook update * feat: UI cleanup * fix: quick ui fixes * chore: update pnpm-lock.yaml with new dependencies and versions; fix output.css formatting * chore: add isloading to button * feat: add loading state to button stories * chore: feedback fixes * fix: feedback fixes * chore: remove console * fix: PR comments * fix: PR comments --------- Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
co-authored by
Rotimi Best
parent
2145782385
commit
cbf6ae7e63
@@ -92,7 +92,6 @@
|
||||
"@types/node": "^20",
|
||||
"all-object-keys": "^2.2.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"carbon-components-svelte": "^0.79.0",
|
||||
"diff": "^5.2.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^9.36.0",
|
||||
|
||||
+177
-21
@@ -39,9 +39,11 @@ html .dark {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
|
||||
}
|
||||
|
||||
html .dark {
|
||||
--scrollbarBG: #000;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@@ -106,6 +108,7 @@ iframe.iframe {
|
||||
border: 1px solid gray;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
iframe.iframe {
|
||||
height: 209px;
|
||||
@@ -116,11 +119,6 @@ code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* [aria-current] :global(svg.carbon-icon),
|
||||
.active :global(svg.carbon-icon) {
|
||||
fill: var(--main-primary-color);
|
||||
} */
|
||||
|
||||
.prose a {
|
||||
color: var(--main-primary-color);
|
||||
}
|
||||
@@ -128,20 +126,15 @@ code {
|
||||
svg.lucide-icon:not(.custom) {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.dark svg.lucide-icon:not(.custom) {
|
||||
color: #fff;
|
||||
}
|
||||
/* .success :global(svg.carbon-icon) {
|
||||
fill: green;
|
||||
}
|
||||
|
||||
:global(svg.carbon-icon.error) {
|
||||
fill: red;
|
||||
} */
|
||||
|
||||
.org-root {
|
||||
height: calc(100vh - 48px);
|
||||
}
|
||||
|
||||
.org-slot {
|
||||
min-width: calc(100vw - 250px);
|
||||
height: calc(100vh - 48px);
|
||||
@@ -151,20 +144,25 @@ svg.lucide-icon:not(.custom) {
|
||||
.shake {
|
||||
animation: shake 0.85s;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
|
||||
10%,
|
||||
90% {
|
||||
transform: translate3d(-15px, 0, 0);
|
||||
}
|
||||
|
||||
20%,
|
||||
80% {
|
||||
transform: translate3d(15px, 0, 0);
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70% {
|
||||
transform: translate3d(-15px, 0, 0);
|
||||
}
|
||||
|
||||
40%,
|
||||
60% {
|
||||
transform: translate3d(15px, 0, 0);
|
||||
@@ -174,11 +172,13 @@ svg.lucide-icon:not(.custom) {
|
||||
.bx--list-box__menu-item__option {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.bx--list-box--xl .bx--list-box__menu-item,
|
||||
.bx--list-box--lg .bx--list-box__menu-item,
|
||||
.bx--list-box__menu-item__option {
|
||||
height: fit-content !important;
|
||||
}
|
||||
|
||||
.cio-bg-blue {
|
||||
background: rgb(2, 51, 189);
|
||||
background-size: cover;
|
||||
@@ -199,9 +199,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-50 {
|
||||
background-color: #fff1f2 !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-50 {
|
||||
color: #fff1f2 !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-50 {
|
||||
border-color: #fff1f2 !important;
|
||||
}
|
||||
@@ -209,9 +211,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-100 {
|
||||
background-color: #ffe4e7 !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-100 {
|
||||
color: #ffe4e7 !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-100 {
|
||||
border-color: #ffe4e7 !important;
|
||||
}
|
||||
@@ -219,9 +223,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-200 {
|
||||
background-color: #fecdd4 !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-200 {
|
||||
color: #fecdd4 !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-200 {
|
||||
border-color: #fecdd4 !important;
|
||||
}
|
||||
@@ -229,9 +235,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-300 {
|
||||
background-color: #fda4b2 !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-300 {
|
||||
color: #fda4b2 !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-300 {
|
||||
border-color: #fda4b2 !important;
|
||||
}
|
||||
@@ -239,9 +247,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-400 {
|
||||
background-color: #fb7189 !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-400 {
|
||||
color: #fb7189 !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-400 {
|
||||
border-color: #fb7189 !important;
|
||||
}
|
||||
@@ -249,9 +259,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-500 {
|
||||
background-color: #f43f63 !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-500 {
|
||||
color: #f43f63 !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-500 {
|
||||
border-color: #f43f63 !important;
|
||||
}
|
||||
@@ -259,9 +271,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-600 {
|
||||
background-color: #e21d4e !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-600 {
|
||||
color: #e21d4e !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-600 {
|
||||
border-color: #e21d4e !important;
|
||||
}
|
||||
@@ -269,9 +283,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-700 {
|
||||
background-color: #be1241 !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-700 {
|
||||
color: #be1241 !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-700 {
|
||||
border-color: #be1241 !important;
|
||||
}
|
||||
@@ -279,9 +295,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-800 {
|
||||
background-color: #9f123d !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-800 {
|
||||
color: #9f123d !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-800 {
|
||||
border-color: #9f123d !important;
|
||||
}
|
||||
@@ -289,9 +307,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-rose .bg-primary-900 {
|
||||
background-color: #88133a !important;
|
||||
}
|
||||
|
||||
.theme-rose .text-primary-900 {
|
||||
color: #88133a !important;
|
||||
}
|
||||
|
||||
.theme-rose .border-primary-900 {
|
||||
border-color: #88133a !important;
|
||||
}
|
||||
@@ -302,9 +322,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-50 {
|
||||
background-color: #eff6ff !important;
|
||||
}
|
||||
|
||||
.text-primary-50 {
|
||||
color: #eff6ff !important;
|
||||
}
|
||||
|
||||
.border-primary-50 {
|
||||
border-color: #eff6ff !important;
|
||||
}
|
||||
@@ -312,9 +334,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-100 {
|
||||
background-color: #dbeafe !important;
|
||||
}
|
||||
|
||||
.text-primary-100 {
|
||||
color: #dbeafe !important;
|
||||
}
|
||||
|
||||
.border-primary-100 {
|
||||
border-color: #dbeafe !important;
|
||||
}
|
||||
@@ -322,9 +346,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-200 {
|
||||
background-color: #bfdafe !important;
|
||||
}
|
||||
|
||||
.text-primary-200 {
|
||||
color: #bfdafe !important;
|
||||
}
|
||||
|
||||
.border-primary-200 {
|
||||
border-color: #bfdafe !important;
|
||||
}
|
||||
@@ -332,9 +358,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-300 {
|
||||
background-color: #92c4fe !important;
|
||||
}
|
||||
|
||||
.text-primary-300 {
|
||||
color: #92c4fe !important;
|
||||
}
|
||||
|
||||
.border-primary-300 {
|
||||
border-color: #92c4fe !important;
|
||||
}
|
||||
@@ -342,9 +370,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-400 {
|
||||
background-color: #5fa3fb !important;
|
||||
}
|
||||
|
||||
.text-primary-400 {
|
||||
color: #5fa3fb !important;
|
||||
}
|
||||
|
||||
.border-primary-400 {
|
||||
border-color: #5fa3fb !important;
|
||||
}
|
||||
@@ -352,9 +382,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-500 {
|
||||
background-color: #3a7ff7 !important;
|
||||
}
|
||||
|
||||
.text-primary-500 {
|
||||
color: #3a7ff7 !important;
|
||||
}
|
||||
|
||||
.border-primary-500 {
|
||||
border-color: #3a7ff7 !important;
|
||||
}
|
||||
@@ -362,9 +394,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-600 {
|
||||
background-color: #2460ec !important;
|
||||
}
|
||||
|
||||
.text-primary-600 {
|
||||
color: #2460ec !important;
|
||||
}
|
||||
|
||||
.border-primary-600 {
|
||||
border-color: #2460ec !important;
|
||||
}
|
||||
@@ -372,9 +406,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-700 {
|
||||
background-color: #1d4ee2 !important;
|
||||
}
|
||||
|
||||
.text-primary-700 {
|
||||
color: #1d4ee2 !important;
|
||||
}
|
||||
|
||||
.border-primary-700 {
|
||||
border-color: #1d4ee2 !important;
|
||||
}
|
||||
@@ -382,9 +418,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-800 {
|
||||
background-color: #1d3eb0 !important;
|
||||
}
|
||||
|
||||
.text-primary-800 {
|
||||
color: #1d3eb0 !important;
|
||||
}
|
||||
|
||||
.border-primary-800 {
|
||||
border-color: #1d3eb0 !important;
|
||||
}
|
||||
@@ -392,9 +430,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.bg-primary-900 {
|
||||
background-color: #1d398b !important;
|
||||
}
|
||||
|
||||
.text-primary-900 {
|
||||
color: #1d398b !important;
|
||||
}
|
||||
|
||||
.border-primary-900 {
|
||||
border-color: #1d398b !important;
|
||||
}
|
||||
@@ -405,9 +445,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-50 {
|
||||
background-color: #effef0 !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-50 {
|
||||
color: #effef0 !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-50 {
|
||||
border-color: #effef0 !important;
|
||||
}
|
||||
@@ -415,9 +457,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-100 {
|
||||
background-color: #d9ffdc !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-100 {
|
||||
color: #d9ffdc !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-100 {
|
||||
border-color: #d9ffdc !important;
|
||||
}
|
||||
@@ -425,9 +469,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-200 {
|
||||
background-color: #b6fcbc !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-200 {
|
||||
color: #b6fcbc !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-200 {
|
||||
border-color: #b6fcbc !important;
|
||||
}
|
||||
@@ -435,9 +481,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-300 {
|
||||
background-color: #7df88a !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-300 {
|
||||
color: #7df88a !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-300 {
|
||||
border-color: #7df88a !important;
|
||||
}
|
||||
@@ -445,9 +493,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-400 {
|
||||
background-color: #3deb51 !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-400 {
|
||||
color: #3deb51 !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-400 {
|
||||
border-color: #3deb51 !important;
|
||||
}
|
||||
@@ -455,9 +505,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-500 {
|
||||
background-color: #12bf26 !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-500 {
|
||||
color: #12bf26 !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-500 {
|
||||
border-color: #12bf26 !important;
|
||||
}
|
||||
@@ -465,9 +517,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-600 {
|
||||
background-color: #0aaf1d !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-600 {
|
||||
color: #0aaf1d !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-600 {
|
||||
border-color: #0aaf1d !important;
|
||||
}
|
||||
@@ -475,9 +529,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-700 {
|
||||
background-color: #0c891b !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-700 {
|
||||
color: #0c891b !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-700 {
|
||||
border-color: #0c891b !important;
|
||||
}
|
||||
@@ -485,9 +541,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-800 {
|
||||
background-color: #0f6c1b !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-800 {
|
||||
color: #0f6c1b !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-800 {
|
||||
border-color: #0f6c1b !important;
|
||||
}
|
||||
@@ -495,9 +553,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-green .bg-primary-900 {
|
||||
background-color: #0f581a !important;
|
||||
}
|
||||
|
||||
.theme-green .text-primary-900 {
|
||||
color: #0f581a !important;
|
||||
}
|
||||
|
||||
.theme-green .border-primary-900 {
|
||||
border-color: #0f581a !important;
|
||||
}
|
||||
@@ -508,9 +568,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-50 {
|
||||
background-color: #fff8ec !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-50 {
|
||||
color: #fff8ec !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-50 {
|
||||
border-color: #fff8ec !important;
|
||||
}
|
||||
@@ -518,9 +580,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-100 {
|
||||
background-color: #fff0d3 !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-100 {
|
||||
color: #fff0d3 !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-100 {
|
||||
border-color: #fff0d3 !important;
|
||||
}
|
||||
@@ -528,9 +592,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-200 {
|
||||
background-color: #ffdca5 !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-200 {
|
||||
color: #ffdca5 !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-200 {
|
||||
border-color: #ffdca5 !important;
|
||||
}
|
||||
@@ -538,9 +604,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-300 {
|
||||
background-color: #ffc26d !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-300 {
|
||||
color: #ffc26d !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-300 {
|
||||
border-color: #ffc26d !important;
|
||||
}
|
||||
@@ -548,9 +616,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-400 {
|
||||
background-color: #ff9d32 !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-400 {
|
||||
color: #ff9d32 !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-400 {
|
||||
border-color: #ff9d32 !important;
|
||||
}
|
||||
@@ -558,9 +628,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-500 {
|
||||
background-color: #ff7f0a !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-500 {
|
||||
color: #ff7f0a !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-500 {
|
||||
border-color: #ff7f0a !important;
|
||||
}
|
||||
@@ -568,9 +640,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-600 {
|
||||
background-color: #ff6600 !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-600 {
|
||||
color: #ff6600 !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-600 {
|
||||
border-color: #ff6600 !important;
|
||||
}
|
||||
@@ -578,9 +652,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-700 {
|
||||
background-color: #cc4902 !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-700 {
|
||||
color: #cc4902 !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-700 {
|
||||
border-color: #cc4902 !important;
|
||||
}
|
||||
@@ -588,9 +664,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-800 {
|
||||
background-color: #a1390b !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-800 {
|
||||
color: #a1390b !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-800 {
|
||||
border-color: #a1390b !important;
|
||||
}
|
||||
@@ -598,9 +676,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-orange .bg-primary-900 {
|
||||
background-color: #82310c !important;
|
||||
}
|
||||
|
||||
.theme-orange .text-primary-900 {
|
||||
color: #82310c !important;
|
||||
}
|
||||
|
||||
.theme-orange .border-primary-900 {
|
||||
border-color: #82310c !important;
|
||||
}
|
||||
@@ -611,9 +691,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-50 {
|
||||
background-color: #fff2fe !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-50 {
|
||||
color: #fff2fe !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-50 {
|
||||
border-color: #fff2fe !important;
|
||||
}
|
||||
@@ -621,9 +703,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-100 {
|
||||
background-color: #ffe3fe !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-100 {
|
||||
color: #ffe3fe !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-100 {
|
||||
border-color: #ffe3fe !important;
|
||||
}
|
||||
@@ -631,9 +715,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-200 {
|
||||
background-color: #ffc6fe !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-200 {
|
||||
color: #ffc6fe !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-200 {
|
||||
border-color: #ffc6fe !important;
|
||||
}
|
||||
@@ -641,9 +727,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-300 {
|
||||
background-color: #ff99f7 !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-300 {
|
||||
color: #ff99f7 !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-300 {
|
||||
border-color: #ff99f7 !important;
|
||||
}
|
||||
@@ -651,9 +739,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-400 {
|
||||
background-color: #ff5df2 !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-400 {
|
||||
color: #ff5df2 !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-400 {
|
||||
border-color: #ff5df2 !important;
|
||||
}
|
||||
@@ -661,9 +751,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-500 {
|
||||
background-color: #ff21f5 !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-500 {
|
||||
color: #ff21f5 !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-500 {
|
||||
border-color: #ff21f5 !important;
|
||||
}
|
||||
@@ -671,9 +763,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-600 {
|
||||
background-color: #fb00ff !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-600 {
|
||||
color: #fb00ff !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-600 {
|
||||
border-color: #fb00ff !important;
|
||||
}
|
||||
@@ -681,9 +775,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-700 {
|
||||
background-color: #cf00ce !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-700 {
|
||||
color: #cf00ce !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-700 {
|
||||
border-color: #cf00ce !important;
|
||||
}
|
||||
@@ -691,9 +787,11 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-800 {
|
||||
background-color: #a900a6 !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-800 {
|
||||
color: #a900a6 !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-800 {
|
||||
border-color: #a900a6 !important;
|
||||
}
|
||||
@@ -701,24 +799,26 @@ svg.lucide-icon:not(.custom) {
|
||||
.theme-violet .bg-primary-900 {
|
||||
background-color: #890684 !important;
|
||||
}
|
||||
|
||||
.theme-violet .text-primary-900 {
|
||||
color: #890684 !important;
|
||||
}
|
||||
|
||||
.theme-violet .border-primary-900 {
|
||||
border-color: #890684 !important;
|
||||
}
|
||||
|
||||
/* Tailwind v4 Custom Animations and Keyframes */
|
||||
@theme {
|
||||
--animate-shake-announcement: shakeAnnouncement 0.5s ease-in-out;
|
||||
--animate-flip-book: flipBook 0.6s ease-in-out;
|
||||
--animate-scroll-unroll: scrollUnroll 0.8s ease-out;
|
||||
--animate-check-items: attendanceCount 0.8s ease-in-out;
|
||||
--animate-task-complete: taskComplete 0.6s ease-in-out;
|
||||
--animate-grade-glow: gradeGlow 1s ease-in-out;
|
||||
--animate-people-wave: peopleWave 0.8s ease-in-out;
|
||||
--animate-chart-grow: dataScan 1s ease-in-out;
|
||||
--animate-screen-glow: screenGlow 1s ease-in-out;
|
||||
--animate-NEWS_FEED: shakeAnnouncement 0.5s ease-in-out;
|
||||
--animate-SECTION: flipBook 0.6s ease-in-out;
|
||||
--animate-LESSONS: scrollUnroll 0.8s ease-out;
|
||||
--animate-ATTENDANCE: attendanceCount 0.8s ease-in-out;
|
||||
--animate-SUBMISSIONS: taskComplete 0.6s ease-in-out;
|
||||
--animate-MARKS: gradeGlow 1s ease-in-out;
|
||||
--animate-PEOPLE: peopleWave 0.8s ease-in-out;
|
||||
--animate-ANALYTICS: dataScan 1s ease-in-out;
|
||||
--animate-LANDING_PAGE: screenGlow 1s ease-in-out;
|
||||
--animate-library-expand: libraryExpand 0.7s ease-out;
|
||||
--animate-template-morph: templateMorph 0.6s ease-in-out;
|
||||
--animate-ripple: communityRipple 0.9s ease-out;
|
||||
@@ -728,10 +828,12 @@ svg.lucide-icon:not(.custom) {
|
||||
}
|
||||
|
||||
@keyframes shakeAnnouncement {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
10%,
|
||||
30%,
|
||||
50%,
|
||||
@@ -739,6 +841,7 @@ svg.lucide-icon:not(.custom) {
|
||||
90% {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
20%,
|
||||
40%,
|
||||
60%,
|
||||
@@ -751,9 +854,11 @@ svg.lucide-icon:not(.custom) {
|
||||
0% {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotateY(90deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
@@ -764,10 +869,12 @@ svg.lucide-icon:not(.custom) {
|
||||
transform: scaleY(0) translateY(-50%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scaleY(0.8) translateY(-25%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleY(1) translateY(0);
|
||||
opacity: 1;
|
||||
@@ -779,22 +886,27 @@ svg.lucide-icon:not(.custom) {
|
||||
transform: scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: scale(1.15);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: scale(1.02);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
@@ -805,26 +917,32 @@ svg.lucide-icon:not(.custom) {
|
||||
0% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(1.1) rotate(-5deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2) rotate(5deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1.1) rotate(-2deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradeGlow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
filter: brightness(1) drop-shadow(0 0 0 rgba(34, 197, 94, 0));
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
filter: brightness(1.3) drop-shadow(0 0 8px rgba(34, 197, 94, 0.6));
|
||||
@@ -832,16 +950,20 @@ svg.lucide-icon:not(.custom) {
|
||||
}
|
||||
|
||||
@keyframes peopleWave {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(-5deg) scale(1.05);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(5deg) scale(1.1);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(-3deg) scale(1.05);
|
||||
}
|
||||
@@ -852,18 +974,22 @@ svg.lucide-icon:not(.custom) {
|
||||
transform: scale(1);
|
||||
filter: brightness(1) drop-shadow(0 0 0 rgba(99, 102, 241, 0));
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(1.1) drop-shadow(0 0 4px rgba(99, 102, 241, 0.4));
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
filter: brightness(1.2) drop-shadow(0 0 8px rgba(99, 102, 241, 0.6));
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1.15) rotate(2deg);
|
||||
filter: brightness(1.3) drop-shadow(0 0 12px rgba(99, 102, 241, 0.8));
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
filter: brightness(1) drop-shadow(0 0 0 rgba(99, 102, 241, 0));
|
||||
@@ -871,11 +997,13 @@ svg.lucide-icon:not(.custom) {
|
||||
}
|
||||
|
||||
@keyframes screenGlow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
filter: brightness(1) drop-shadow(0 0 0 rgba(59, 130, 246, 0));
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
filter: brightness(1.2) drop-shadow(0 0 6px rgba(59, 130, 246, 0.5));
|
||||
@@ -887,18 +1015,22 @@ svg.lucide-icon:not(.custom) {
|
||||
transform: translateY(-100%) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translateY(-20%) scale(0.9);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translateY(5%) scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translateY(-2%) scale(1.02);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0%) scale(1);
|
||||
opacity: 1;
|
||||
@@ -909,15 +1041,19 @@ svg.lucide-icon:not(.custom) {
|
||||
0% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(1.05) rotate(-2deg) skewX(2deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1) rotate(2deg) skewX(-2deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1.05) rotate(-1deg) skewX(1deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg) skewX(0deg);
|
||||
}
|
||||
@@ -928,18 +1064,22 @@ svg.lucide-icon:not(.custom) {
|
||||
transform: translateX(-100%) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translateX(-20%) scale(0.9);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translateX(5%) scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translateX(-2%) scale(1.02);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0%) scale(1);
|
||||
opacity: 1;
|
||||
@@ -950,18 +1090,23 @@ svg.lucide-icon:not(.custom) {
|
||||
0% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: scale(1.05) rotate(72deg);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1.1) rotate(144deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: scale(1.15) rotate(216deg);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: scale(1.1) rotate(288deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1) rotate(360deg);
|
||||
}
|
||||
@@ -972,18 +1117,22 @@ svg.lucide-icon:not(.custom) {
|
||||
transform: scale(1);
|
||||
filter: brightness(1) drop-shadow(0 0 0 rgba(147, 51, 234, 0));
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(0.95) scaleX(1.1);
|
||||
filter: brightness(1.05) drop-shadow(0 0 2px rgba(147, 51, 234, 0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1) scaleX(0.9);
|
||||
filter: brightness(1.1) drop-shadow(0 0 4px rgba(147, 51, 234, 0.5));
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1.05) scaleX(1.05);
|
||||
filter: brightness(1.05) drop-shadow(0 0 2px rgba(147, 51, 234, 0.3));
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1) scaleX(1);
|
||||
filter: brightness(1) drop-shadow(0 0 0 rgba(147, 51, 234, 0));
|
||||
@@ -994,24 +1143,31 @@ svg.lucide-icon:not(.custom) {
|
||||
0% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
15% {
|
||||
transform: scale(1.02) rotate(15deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: scale(1.04) rotate(-10deg);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: scale(1.06) rotate(20deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: scale(1.08) rotate(-15deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1.06) rotate(10deg);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: scale(1.02) rotate(-5deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
// import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
// import { writable } from 'svelte/store';
|
||||
// // import { useCompletion } from 'ai/svelte';
|
||||
// import { Popover } from 'carbon-components-svelte';
|
||||
// import type { PopoverProps } from 'carbon-components-svelte/types/Popover/Popover.svelte';
|
||||
// import WandSparklesIcon from '@lucide/svelte/icons/wand-sparkles';
|
||||
// import { IconButton } from '$lib/components/IconButton';
|
||||
// import { Loading } from 'carbon-components-svelte';
|
||||
// import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
// import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<p class="text-center text-sm font-medium text-gray-600 md:text-left dark:text-gray-200">
|
||||
{activity.title}
|
||||
</p>
|
||||
<p class="text-center text-2xl font-bold md:text-left">
|
||||
<p class="text-center text-2xl md:text-left">
|
||||
{activity.percentage}
|
||||
{#if !activity.hidePercentage}
|
||||
%
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="flex w-full flex-col items-center justify-start gap-4 text-start md:flex-row">
|
||||
<Avatar src={user.avatarUrl} name={user.fullName} width="w-16" height="h-16" />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<p class="text-center text-2xl font-bold md:text-left dark:text-white">
|
||||
<p class="text-center text-2xl md:text-left dark:text-white">
|
||||
{user.fullName}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script>
|
||||
import { SkeletonPlaceholder } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl pt-14">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 10rem;" />
|
||||
<Skeleton class="h-40 w-full" />
|
||||
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<SkeletonPlaceholder style="width: 270px; height: 10rem;" />
|
||||
<SkeletonPlaceholder style="width: 270px; height: 10rem;" />
|
||||
<SkeletonPlaceholder style="width: 270px; height: 10rem;" />
|
||||
<Skeleton class="h-40 w-[270px]" />
|
||||
<Skeleton class="h-40 w-[270px]" />
|
||||
<Skeleton class="h-40 w-[270px]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
</script>
|
||||
|
||||
<IconButton {onClick} {color} {contained} {size}>
|
||||
<XIcon size={size === 'large' ? 24 : 16} class="carbon-icon dark:text-black" />
|
||||
<XIcon size={size === 'large' ? 24 : 16} class="dark:text-black" />
|
||||
</IconButton>
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
</script>
|
||||
|
||||
<IconButton {onClick}>
|
||||
<XIcon size={16} class="carbon-icon dark:text-white" />
|
||||
<XIcon size={16} class="dark:text-white" />
|
||||
</IconButton>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<p
|
||||
class="{className} {size === 'sm' ? 'px-1 py-1' : 'px-2 py-2'} org-avatar {className.includes('bg-')
|
||||
? ''
|
||||
: 'bg-gray-200 dark:text-black'} {shape} text-center"
|
||||
: 'bg-gray-200 dark:text-black'} {shape} flex items-center justify-center text-center"
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
|
||||
+18
-17
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { SkeletonPlaceholder } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen">
|
||||
@@ -10,11 +10,12 @@
|
||||
{#each Array(4) as _}
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800">
|
||||
<div class="flex items-center space-x-4">
|
||||
<SkeletonPlaceholder style="width: 48px; height: 48px;" />
|
||||
<Skeleton class="h-[48px] w-[48px]" />
|
||||
<div class="flex-1">
|
||||
<SkeletonPlaceholder style="width: 80px; height: 16px; margin-bottom: 8px;" />
|
||||
<SkeletonPlaceholder style="width: 40px; height: 24px; margin-bottom: 4px;" />
|
||||
<SkeletonPlaceholder style="width: 60px; height: 12px;" />
|
||||
<Skeleton class="h-[48px] w-[48px]" />
|
||||
<Skeleton class="h-[16px] w-[80px] mb-2" />
|
||||
<Skeleton class="h-[24px] w-[40px] mb-1" />
|
||||
<Skeleton class="h-[12px] w-[60px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,10 +26,10 @@
|
||||
<!-- Charts Section Skeleton -->
|
||||
<div class="mb-12">
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800">
|
||||
<SkeletonPlaceholder style="width: 200px; height: 24px; margin-bottom: 24px;" />
|
||||
<Skeleton class="h-[24px] w-[200px] mb-6" />
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 300px;" />
|
||||
<SkeletonPlaceholder style="width: 100%; height: 300px;" />
|
||||
<Skeleton class="w-full h-[300px]" />
|
||||
<Skeleton class="w-full h-[300px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,22 +37,22 @@
|
||||
<!-- Students Table Skeleton -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-neutral-700 dark:bg-neutral-800">
|
||||
<div class="border-b border-gray-200 px-6 py-4 dark:border-neutral-700">
|
||||
<SkeletonPlaceholder style="width: 200px; height: 24px;" />
|
||||
<Skeleton class="h-[24px] w-[200px] mb-6" />
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
{#each Array(5) as _}
|
||||
<div class="flex items-center space-x-4">
|
||||
<SkeletonPlaceholder style="width: 32px; height: 32px;" />
|
||||
<Skeleton class="h-[32px] w-[32px]" />
|
||||
<div class="flex-1">
|
||||
<SkeletonPlaceholder style="width: 150px; height: 16px; margin-bottom: 4px;" />
|
||||
<SkeletonPlaceholder style="width: 200px; height: 12px;" />
|
||||
<Skeleton class="h-[16px] w-[150px] mb-1" />
|
||||
<Skeleton class="h-[12px] w-[200px]" />
|
||||
</div>
|
||||
<SkeletonPlaceholder style="width: 80px; height: 16px;" />
|
||||
<SkeletonPlaceholder style="width: 60px; height: 16px;" />
|
||||
<SkeletonPlaceholder style="width: 40px; height: 20px;" />
|
||||
<SkeletonPlaceholder style="width: 80px; height: 12px;" />
|
||||
<SkeletonPlaceholder style="width: 100px; height: 32px;" />
|
||||
<Skeleton class="h-[16px] w-[80px]" />
|
||||
<Skeleton class="h-[16px] w-[60px]" />
|
||||
<Skeleton class="h-[20px] w-[40px]" />
|
||||
<Skeleton class="h-[12px] w-[80px]" />
|
||||
<Skeleton class="h-[32px] w-[100px]" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { Tag, Pagination } from 'carbon-components-svelte';
|
||||
import type { StudentOverview } from '$lib/utils/types/analytics';
|
||||
import EmptyState from './EmptyState.svelte';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
import { course } from '../../store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
import * as Pagination from '@cio/ui/base/pagination';
|
||||
|
||||
import { course } from '../../store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import EmptyState from './EmptyState.svelte';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import type { StudentOverview } from '$lib/utils/types/analytics';
|
||||
|
||||
interface Props {
|
||||
students?: StudentOverview[];
|
||||
}
|
||||
@@ -32,10 +35,6 @@
|
||||
goto(`/courses/${$course.id}/people/${student.id}?back=/courses/${$course.id}/analytics`);
|
||||
};
|
||||
|
||||
function handlePageChange(event: CustomEvent) {
|
||||
currentPage = event.detail.page;
|
||||
}
|
||||
|
||||
// Reset to first page when students array changes
|
||||
$effect(() => {
|
||||
if (students.length > 0 && currentPage > totalPages) {
|
||||
@@ -128,12 +127,15 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="min-w-[120px] whitespace-nowrap px-4 py-3">
|
||||
<Tag
|
||||
type={student.averageGrade >= 80 ? 'green' : student.averageGrade >= 60 ? 'blue' : 'red'}
|
||||
size="sm"
|
||||
<Badge
|
||||
class={student.averageGrade >= 80
|
||||
? 'bg-green-600'
|
||||
: student.averageGrade >= 60
|
||||
? 'bg-blue-600'
|
||||
: 'bg-red-600'}
|
||||
>
|
||||
{student.averageGrade}%
|
||||
</Tag>
|
||||
</Badge>
|
||||
</td>
|
||||
<td class="min-w-[120px] whitespace-nowrap px-4 py-3">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
@@ -165,7 +167,37 @@
|
||||
total: students.length
|
||||
})}
|
||||
</div>
|
||||
<Pagination page={currentPage} totalItems={students.length} {pageSize} on:change={handlePageChange} />
|
||||
|
||||
<Pagination.Root
|
||||
count={students.length}
|
||||
perPage={pageSize}
|
||||
page={currentPage}
|
||||
onPageChange={(page) => (currentPage = page)}
|
||||
>
|
||||
{#snippet children({ pages, currentPage: activePage })}
|
||||
<Pagination.Content>
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton />
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === 'ellipsis'}
|
||||
<Pagination.Item>
|
||||
<Pagination.Ellipsis />
|
||||
</Pagination.Item>
|
||||
{:else}
|
||||
<Pagination.Item>
|
||||
<Pagination.Link {page} isActive={activePage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton />
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
{/snippet}
|
||||
</Pagination.Root>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
<script>
|
||||
import { RadioButtonGroup, RadioButton, Toggle } from 'carbon-components-svelte';
|
||||
import ZapIcon from '@lucide/svelte/icons/zap';
|
||||
import { goto } from '$app/navigation';
|
||||
import { updateCourse } from '$lib/utils/services/courses';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import Professional from './templates/Professional.svelte';
|
||||
import Plain from './templates/Plain.svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import ZapIcon from '@lucide/svelte/icons/zap';
|
||||
import * as RadioGroup from '@cio/ui/base/radio-group';
|
||||
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import PurpleProfessionalBadge from './templates/PurpleProfessionalBadge.svelte';
|
||||
import BlueProfessionalBadge from './templates/BlueProfessionalBadge.svelte';
|
||||
import PurpleBadgePattern from './templates/PurpleBadgePattern.svelte';
|
||||
import BlueBadgePattern from './templates/BlueBadgePattern.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { updateCourse } from '$lib/utils/services/courses';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { saveCertificateValidation } from '$lib/utils/functions/validator';
|
||||
|
||||
import Plain from './templates/Plain.svelte';
|
||||
import Professional from './templates/Professional.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import BlueBadgePattern from './templates/BlueBadgePattern.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import PurpleBadgePattern from './templates/PurpleBadgePattern.svelte';
|
||||
import BlueProfessionalBadge from './templates/BlueProfessionalBadge.svelte';
|
||||
import PurpleProfessionalBadge from './templates/PurpleProfessionalBadge.svelte';
|
||||
|
||||
const studentNamePlaceholder = 'Name of student';
|
||||
const themes = [
|
||||
'professional',
|
||||
@@ -94,16 +97,20 @@
|
||||
<p class="my-4 text-xs font-normal dark:text-gray-100">
|
||||
{$t('course.navItem.certificates.theme')}
|
||||
</p>
|
||||
<RadioButtonGroup bind:selected={$course.certificate_theme} class="mb-10" disabled={$isFreePlan}>
|
||||
|
||||
<RadioGroup.Root bind:value={$course.certificate_theme} disabled={$isFreePlan} class="mb-10">
|
||||
<div class="flex flex-wrap justify-between gap-y-5">
|
||||
{#each themes as theme}
|
||||
<div class="mr-3 flex">
|
||||
<RadioButton value={theme} />
|
||||
<div class="mr-3 flex items-start space-x-2">
|
||||
<RadioGroup.Item value={theme} id={theme} />
|
||||
<Label for={theme} class="cursor-pointer">
|
||||
<img src={`/images/certificate_theme_${theme}.png`} alt="themes" class="h-[82px] w-[110px]" />
|
||||
</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</RadioButtonGroup>
|
||||
</RadioGroup.Root>
|
||||
|
||||
<div>
|
||||
<p class="my-2 text-xs font-normal text-black dark:text-gray-100">
|
||||
{$t('course.navItem.certificates.logo')}
|
||||
@@ -135,18 +142,16 @@
|
||||
helperMessage={helperText}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
labelText={$t('course.navItem.certificates.allow')}
|
||||
bind:toggled={$course.is_certificate_downloadable}
|
||||
class="my-4"
|
||||
size="sm"
|
||||
<div class="my-4 flex items-center space-x-2">
|
||||
<Switch
|
||||
id="certificate-downloadable"
|
||||
bind:checked={$course.is_certificate_downloadable}
|
||||
disabled={$isFreePlan}
|
||||
>
|
||||
<span slot="labelA" style={$globalStore.isDark ? 'color: white' : 'color: #161616'}
|
||||
>{$t('generic.locked')}</span
|
||||
>
|
||||
<span slot="labelB" style="color: green">{$t('generic.unlocked')}</span>
|
||||
</Toggle>
|
||||
/>
|
||||
<Label for="certificate-downloadable" class="text-sm font-medium dark:text-gray-100">
|
||||
{$t('course.navItem.certificates.allow')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex w-full items-center justify-center rounded-md bg-gray-100 lg:w-3/5 dark:bg-neutral-800">
|
||||
|
||||
+10
-15
@@ -1,16 +1,16 @@
|
||||
<script>
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
import { issueCertificateModal, resetForm } from './store';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import { Toggle } from 'carbon-components-svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { issueCertificateModal, resetForm } from './store';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
const onToggle = (e) => {
|
||||
console.log(e);
|
||||
};
|
||||
let isAutomatic = $state(false);
|
||||
|
||||
const issueCertificate = () => {
|
||||
resetForm();
|
||||
@@ -26,15 +26,10 @@
|
||||
>
|
||||
<main>
|
||||
<div>
|
||||
<Toggle
|
||||
labelText="Automatically send certificates on completion"
|
||||
on:toggle={(e) => onToggle(e)}
|
||||
size="sm"
|
||||
class="mb-4"
|
||||
>
|
||||
<span slot="labelA" style="color: #161616">Automatic</span>
|
||||
<span slot="labelB" style="color: green">Automatic</span>
|
||||
</Toggle>
|
||||
<div class="mb-4 flex items-center space-x-2">
|
||||
<Switch id="auto-certificate" bind:checked={isAutomatic} />
|
||||
<Label for="auto-certificate" class="text-sm font-medium">Automatic</Label>
|
||||
</div>
|
||||
<p class="my-4 text-sm font-medium">
|
||||
If you set this as automatic, certificates will be issued after the learner completes the course.
|
||||
</p>
|
||||
|
||||
@@ -1,23 +1,51 @@
|
||||
<script>
|
||||
import SearchIcon from '@lucide/svelte/icons/search';
|
||||
import * as Pagination from '@cio/ui/base/pagination';
|
||||
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import * as Table from '@cio/ui/base/table';
|
||||
import { Checkbox } from '@cio/ui/base/checkbox';
|
||||
|
||||
import { CertificateReportData, students } from './mockData';
|
||||
import {
|
||||
Search,
|
||||
Checkbox,
|
||||
StructuredList,
|
||||
StructuredListHead,
|
||||
StructuredListRow,
|
||||
StructuredListCell,
|
||||
StructuredListBody,
|
||||
Pagination
|
||||
} from 'carbon-components-svelte';
|
||||
|
||||
let searchValue = $state('');
|
||||
let checked = $state(false);
|
||||
let selectAll = $state(false);
|
||||
let selectedStudents = $state(new Set());
|
||||
|
||||
let currentPage = $state(1);
|
||||
const pageSize = 10;
|
||||
|
||||
// let totalPages = $derived(Math.ceil(students.length / pageSize));
|
||||
let startIndex = $derived((currentPage - 1) * pageSize);
|
||||
let endIndex = $derived(startIndex + pageSize);
|
||||
let paginatedStudents = $derived(students.slice(startIndex, endIndex));
|
||||
|
||||
function toggleSelectAll() {
|
||||
selectAll = !selectAll;
|
||||
if (selectAll) {
|
||||
selectedStudents = new Set(paginatedStudents.map((s) => s.id));
|
||||
} else {
|
||||
selectedStudents = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStudent(id) {
|
||||
if (selectedStudents.has(id)) {
|
||||
selectedStudents.delete(id);
|
||||
} else {
|
||||
selectedStudents.add(id);
|
||||
}
|
||||
selectedStudents = selectedStudents;
|
||||
selectAll = selectedStudents.size === paginatedStudents.length;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="-ml-4 -mr-4 mb-10 flex flex-wrap items-start gap-2">
|
||||
{#each CertificateReportData as data (data.id)}
|
||||
<div class="box mb-5 flex flex-col justify-center rounded border border-gray-200 px-2 dark:border-neutral-600">
|
||||
<div
|
||||
class="mb-5 flex h-[165px] w-[246px] flex-col justify-center rounded border border-gray-200 px-2 dark:border-neutral-600"
|
||||
>
|
||||
<p class="text-xs font-normal">{data.title}</p>
|
||||
<bold class="text-2xl font-medium">{data.value}</bold>
|
||||
</div>
|
||||
@@ -25,61 +53,80 @@
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="flex flex-row items-center justify-between gap-4">
|
||||
<p class="w-full text-base font-semibold">Certificate issued</p>
|
||||
<Search placeholder="search students" bind:value={searchValue} searchClass="border-b border-transparent" />
|
||||
<div class="relative w-full max-w-sm">
|
||||
<SearchIcon class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input type="search" placeholder="Search students" bind:value={searchValue} class="pl-10" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-14 border-t border-gray-300">
|
||||
<StructuredList>
|
||||
<StructuredListHead class="bg-primary-50 py-0">
|
||||
<StructuredListRow head class="py-0">
|
||||
<StructuredListCell head class="py-0">
|
||||
<div class="flex-3 text-primary-600 flex w-full flex-row items-center justify-start text-sm font-medium">
|
||||
<Checkbox bind:checked />
|
||||
<p class="text-primary-900 w-full text-start text-sm font-medium">Name of student</p>
|
||||
</div>
|
||||
</StructuredListCell>
|
||||
<StructuredListCell head class="py-0">
|
||||
<div class="flex-2 text-primary-900 w-full items-center text-sm font-medium">
|
||||
<p class="pt-1 align-middle text-sm font-medium">Date issued</p>
|
||||
<p class="opacity-0">personalized</p>
|
||||
</div>
|
||||
</StructuredListCell>
|
||||
</StructuredListRow>
|
||||
</StructuredListHead>
|
||||
<StructuredListBody>
|
||||
{#each students as item (item.id)}
|
||||
<StructuredListRow label for="row-{item}">
|
||||
<StructuredListCell>
|
||||
<div class="flex-3 flex w-full flex-row items-center justify-start pt-4 text-sm font-medium">
|
||||
<Checkbox />
|
||||
<p class="w-full text-start text-sm font-normal">{item.name}</p>
|
||||
</div>
|
||||
</StructuredListCell>
|
||||
<StructuredListCell>
|
||||
<div class="flex-2 flex w-full flex-row items-center gap-10">
|
||||
<p class="align-middle text-sm font-normal">{item.issued}</p>
|
||||
{#if item.peronalized == true}
|
||||
<span class="bg-primary-100 rounded-lg px-3 py-2 text-center">
|
||||
<p class="text-primary-600 text-xs opacity-100">personalized</p>
|
||||
</span>
|
||||
{:else}
|
||||
<p class="opacity-0">personalized</p>
|
||||
<section class="mt-14">
|
||||
<div class="rounded-md border dark:border-neutral-600">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[50px]">
|
||||
<Checkbox checked={selectAll} onCheckedChange={toggleSelectAll} />
|
||||
</Table.Head>
|
||||
<Table.Head>Name of student</Table.Head>
|
||||
<Table.Head>Date issued</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each paginatedStudents as student (student.id)}
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
<Checkbox checked={selectedStudents.has(student.id)} onCheckedChange={() => toggleStudent(student.id)} />
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium">{student.name}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm">{student.issued}</span>
|
||||
{#if student.peronalized}
|
||||
<Badge variant="secondary" class="bg-primary-100 text-primary-600">personalized</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</StructuredListCell>
|
||||
</StructuredListRow>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</StructuredListBody>
|
||||
</StructuredList>
|
||||
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
|
||||
</section>
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.box {
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
</style>
|
||||
{#if students.length > pageSize}
|
||||
<div class="mt-4 flex items-center justify-end">
|
||||
<Pagination.Root
|
||||
count={students.length}
|
||||
perPage={pageSize}
|
||||
page={currentPage}
|
||||
onPageChange={(page) => (currentPage = page)}
|
||||
>
|
||||
{#snippet children({ pages, currentPage: activePage })}
|
||||
<Pagination.Content>
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton />
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === 'ellipsis'}
|
||||
<Pagination.Item>
|
||||
<Pagination.Ellipsis />
|
||||
</Pagination.Item>
|
||||
{:else}
|
||||
<Pagination.Item>
|
||||
<Pagination.Link {page} isActive={activePage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton />
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
{/snippet}
|
||||
</Pagination.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
+6
-6
@@ -16,17 +16,17 @@
|
||||
<img src="/images/blue-badge-pattern-background.svg" alt="" class="absolute z-0 h-full w-full object-cover" />
|
||||
<div class="relative z-20 m-2 flex h-full justify-between bg-white md:h-[17rem]">
|
||||
<div class="w-[70%] pl-3">
|
||||
<header class="absolute flex w-full items-center justify-center gap-1 text-sm font-bold">
|
||||
<header class="absolute flex w-full items-center justify-center gap-1 text-sm">
|
||||
<img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
|
||||
<h1 class="text-sm">{$currentOrg.name}</h1>
|
||||
</header>
|
||||
|
||||
<div class="mt-16 font-bold">
|
||||
<div class="mt-16">
|
||||
<h2 class="m-0 text-[7px]">This certificate is awarded to</h2>
|
||||
<h1 class="m-0 mt-0.5 text-lg leading-3 text-[#2D499F]">{studentName}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 w-[90%] font-bold">
|
||||
<div class="mt-3 w-[90%]">
|
||||
<h1 class="text-[7px]">has successfully completed training on</h1>
|
||||
<h2 class="m-0 text-lg">
|
||||
{$course.title}
|
||||
@@ -36,15 +36,15 @@
|
||||
<div class="my-8 flex items-center justify-between text-center">
|
||||
<div>
|
||||
<h1 class="m-0 h-2/4 text-[7px]">DATE ISSUED:</h1>
|
||||
<p class="border-b border-black text-[7px] font-bold">{dateIssued}</p>
|
||||
<p class="border-b border-black text-[7px]">{dateIssued}</p>
|
||||
</div>
|
||||
<div class="w-[20%] text-[7px]">
|
||||
<div></div>
|
||||
<p class="border-t border-black text-[7px] font-bold">Signature</p>
|
||||
<p class="border-t border-black text-[7px]">Signature</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="desc w-[90%] text-[8px] font-bold">
|
||||
<footer class="desc w-[90%] text-[8px]">
|
||||
{$course.description}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+6
-6
@@ -15,17 +15,17 @@
|
||||
<div class="w-full bg-[#2D499F] p-3 dark:text-black">
|
||||
<div class="flex w-full bg-white pb-5">
|
||||
<div class="w-[80%] pl-3">
|
||||
<header class="flex items-center justify-start gap-1 text-sm font-bold">
|
||||
<header class="flex items-center justify-start gap-1 text-sm">
|
||||
<img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
|
||||
<h1 class="text-sm">{$currentOrg.name}</h1>
|
||||
</header>
|
||||
|
||||
<div class="mt-7 font-bold">
|
||||
<div class="mt-7">
|
||||
<h2 class="m-0 text-[7px]">This certificate is awarded to</h2>
|
||||
<h1 class="m-0 mt-0.5 text-lg leading-3 text-[#2D499F]">{studentName}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 w-[90%] font-bold">
|
||||
<div class="mt-3 w-[90%]">
|
||||
<h1 class="text-[7px]">has succesfully completed training on</h1>
|
||||
<h2 class="m-0 text-lg">
|
||||
{$course.title}
|
||||
@@ -35,15 +35,15 @@
|
||||
<div class="mb-5 mt-8 flex items-center justify-between text-center">
|
||||
<div class="">
|
||||
<h1 class="m-0 h-2/4 text-[7px]">DATE ISSUED:</h1>
|
||||
<p class="border-b border-black text-[7px] font-bold">{dateIssued}</p>
|
||||
<p class="border-b border-black text-[7px]">{dateIssued}</p>
|
||||
</div>
|
||||
<div class="w-[20%] text-[7px]">
|
||||
<div class=""></div>
|
||||
<p class="border-t border-black text-[7px] font-bold">Signature</p>
|
||||
<p class="border-t border-black text-[7px]">Signature</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="desc w-[90%] text-[8px] font-bold">
|
||||
<footer class="desc w-[90%] text-[8px]">
|
||||
{$course.description}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+6
-6
@@ -16,17 +16,17 @@
|
||||
<img src="/images/purple-badge-pattern-background.svg" alt="" class="absolute z-0 h-full w-full object-cover" />
|
||||
<div class="relative z-20 m-2 flex h-full justify-between bg-white md:h-[17rem]">
|
||||
<div class="w-[70%] pl-3">
|
||||
<header class="absolute flex w-full items-center justify-center gap-1 text-sm font-bold">
|
||||
<header class="absolute flex w-full items-center justify-center gap-1 text-sm">
|
||||
<img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
|
||||
<h1 class="text-sm">{$currentOrg.name}</h1>
|
||||
</header>
|
||||
|
||||
<div class="mt-16 font-bold">
|
||||
<div class="mt-16">
|
||||
<h2 class="m-0 text-[7px]">This certificate is awarded to</h2>
|
||||
<h1 class="m-0 mt-0.5 text-lg leading-3 text-[#914397]">{studentName}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 w-[90%] font-bold">
|
||||
<div class="mt-3 w-[90%]">
|
||||
<h1 class="text-[7px]">has successfully completed training on</h1>
|
||||
<h2 class="m-0 text-lg">
|
||||
{$course.title}
|
||||
@@ -36,15 +36,15 @@
|
||||
<div class="my-8 flex items-center justify-between text-center">
|
||||
<div>
|
||||
<h1 class="m-0 h-2/4 text-[7px]">DATE ISSUED:</h1>
|
||||
<p class="border-b border-black text-[7px] font-bold">{dateIssued}</p>
|
||||
<p class="border-b border-black text-[7px]">{dateIssued}</p>
|
||||
</div>
|
||||
<div class="w-[20%] text-[7px]">
|
||||
<div></div>
|
||||
<p class="border-t border-black text-[7px] font-bold">Signature</p>
|
||||
<p class="border-t border-black text-[7px]">Signature</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="desc w-[90%] text-[8px] font-bold">
|
||||
<footer class="desc w-[90%] text-[8px]">
|
||||
{$course.description}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+6
-6
@@ -15,17 +15,17 @@
|
||||
<div class="w-full bg-[#95449A] p-3 dark:text-black">
|
||||
<div class="flex w-full bg-white pb-5">
|
||||
<div class="w-[80%] pl-3">
|
||||
<header class="flex items-center justify-start gap-1 text-sm font-bold">
|
||||
<header class="flex items-center justify-start gap-1 text-sm">
|
||||
<img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
|
||||
<h1 class="text-sm">{$currentOrg.name}</h1>
|
||||
</header>
|
||||
|
||||
<div class="mt-7 font-bold">
|
||||
<div class="mt-7">
|
||||
<h2 class="m-0 text-[7px]">This certificate is awarded to</h2>
|
||||
<h1 class="m-0 mt-0.5 text-lg leading-3 text-[#95449A]">{studentName}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 w-[90%] font-bold">
|
||||
<div class="mt-3 w-[90%]">
|
||||
<h1 class="text-[7px]">has succesfully completed training on</h1>
|
||||
<h2 class="m-0 text-lg">
|
||||
{$course.title}
|
||||
@@ -35,15 +35,15 @@
|
||||
<div class="mb-5 mt-8 flex items-center justify-between text-center">
|
||||
<div class="">
|
||||
<h1 class="m-0 h-2/4 text-[7px]">DATE ISSUED:</h1>
|
||||
<p class="border-b border-black text-[7px] font-bold">{dateIssued}</p>
|
||||
<p class="border-b border-black text-[7px]">{dateIssued}</p>
|
||||
</div>
|
||||
<div class="w-[20%] text-[7px]">
|
||||
<div class=""></div>
|
||||
<p class="border-t border-black text-[7px] font-bold">Signature</p>
|
||||
<p class="border-t border-black text-[7px]">Signature</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="desc w-[90%] text-[8px] font-bold">
|
||||
<footer class="desc w-[90%] text-[8px]">
|
||||
{$course.description}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+27
-29
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import CirclePlusIcon from '@lucide/svelte/icons/circle-plus';
|
||||
|
||||
import {
|
||||
questionnaire,
|
||||
@@ -12,28 +14,25 @@
|
||||
handleAnswerSelect,
|
||||
addDynamicValue
|
||||
} from '../store/exercise';
|
||||
import { Select, SelectItem } from 'carbon-components-svelte';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
import { deleteExercise } from '$lib/utils/services/courses';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { QUESTION_TYPE, QUESTION_TYPES } from '$lib/components/Question/constants';
|
||||
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import CirclePlusIcon from '@lucide/svelte/icons/circle-plus';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { lesson } from '../store/lessons';
|
||||
import OrderModal from './OrderModal.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import Checkbox from '$lib/components/Form/Checkbox.svelte';
|
||||
import RadioItem from '$lib/components/Form/RadioItem.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import DeleteConfirmationModal from './DeleteConfirmation.svelte';
|
||||
import ErrorMessage from '$lib/components/ErrorMessage/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import QuestionContainer from '$lib/components/QuestionContainer/index.svelte';
|
||||
import DeleteConfirmationModal from './DeleteConfirmation.svelte';
|
||||
import OrderModal from './OrderModal.svelte';
|
||||
import { QUESTION_TYPE, QUESTION_TYPES } from '$lib/components/Question/constants';
|
||||
import { deleteExercise } from '$lib/utils/services/courses';
|
||||
import { lesson } from '../store/lessons';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
const initialQuestionsLength = $questionnaire.questions.length;
|
||||
|
||||
@@ -171,23 +170,26 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
size="xl"
|
||||
class="w-[50px]"
|
||||
selected={question?.question_type?.id}
|
||||
on:change={(e) => {
|
||||
const id = (e.target as HTMLSelectElement)?.value;
|
||||
if (!id) return;
|
||||
<Select.Root
|
||||
type="single"
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
const id = parseInt(value);
|
||||
|
||||
question.question_type = QUESTION_TYPES.find((q) => q.id === parseInt(id)) ?? QUESTION_TYPES[0];
|
||||
question.question_type = QUESTION_TYPES.find((q) => q.id === id) ?? QUESTION_TYPES[0];
|
||||
question.question_type_id = question.question_type.id;
|
||||
question.is_dirty = true;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="w-[180px]">
|
||||
{question?.question_type?.label ? $t(question.question_type.label) : 'Select type'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each QUESTION_TYPES as type}
|
||||
<SelectItem value={type.id} text={$t(type.label)} />
|
||||
<Select.Item value={type.id.toString()} label={$t(type.label)} />
|
||||
{/each}
|
||||
</Select>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
{#if typeof question.code === 'string'}
|
||||
@@ -262,8 +264,4 @@
|
||||
:global(#dnd-action-dragged-el div) {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
:global(select) {
|
||||
background-image: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
+6
-6
@@ -1,13 +1,13 @@
|
||||
<script>
|
||||
import { SkeletonText } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<SkeletonText width="100%" heading />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<div class="space-y-3">
|
||||
<SkeletonText width="100%" heading />
|
||||
<SkeletonText width="100%" heading />
|
||||
<SkeletonText width="100%" heading />
|
||||
<SkeletonText width="100%" heading />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+75
-44
@@ -1,18 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import Preview from './Preview.svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
|
||||
import { STATUS } from './constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import type { SubmissionIdData } from '$lib/utils/types/submission';
|
||||
|
||||
import Preview from './Preview.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import { Dropdown, Tag } from 'carbon-components-svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
// import { useCompletion } from 'ai/svelte';
|
||||
// import { QUESTION_TYPE } from '$lib/components/Question/constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
||||
import type { SubmissionIdData } from '$lib/utils/types/submission';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
@@ -51,16 +56,17 @@
|
||||
}
|
||||
];
|
||||
|
||||
let status = SELECTABLE_STATUS[0];
|
||||
let selectedId = $state(status.id);
|
||||
let status = $state(SELECTABLE_STATUS[0]);
|
||||
let reasons = $state({});
|
||||
let isLoading = $state(false);
|
||||
let openMenu = $state(false);
|
||||
let openDeletePrompt = $state(false);
|
||||
let isDeleting = $state(false);
|
||||
|
||||
const total = calculateTotal(data.questionAnswerByPoint);
|
||||
const maxPoints = $derived(getMaxPoints(data.questions));
|
||||
const sortedQuestions = $derived(
|
||||
Array.isArray(data.questions) ? [...data.questions].sort((a, b) => a.order - b.order) : []
|
||||
);
|
||||
|
||||
function getMaxPoints(questions) {
|
||||
return (questions || []).reduce((acc, question) => acc + question.points, 0);
|
||||
@@ -71,36 +77,35 @@
|
||||
return Object.values(grades).reduce((acc, grade) => acc + parseInt(grade || '0'), 0);
|
||||
}
|
||||
|
||||
function handleStatusChange(event) {
|
||||
const newSelectedId = event.detail.selectedId;
|
||||
function handleStatusChange(value: string) {
|
||||
const newSelectedId = parseInt(value);
|
||||
const prevStatusId = data.statusId;
|
||||
|
||||
setStatus({
|
||||
...data,
|
||||
statusId: newSelectedId
|
||||
});
|
||||
// Find the new status
|
||||
const newStatus = SELECTABLE_STATUS.find((s) => s.id === newSelectedId);
|
||||
|
||||
if (newStatus) {
|
||||
// Update state
|
||||
status = newStatus;
|
||||
data.statusId = newSelectedId;
|
||||
|
||||
updateStatus({
|
||||
submissionId: data.id,
|
||||
prevStatusId: data.statusId,
|
||||
nextStatusId: status.id,
|
||||
prevStatusId: prevStatusId,
|
||||
nextStatusId: newSelectedId,
|
||||
total
|
||||
});
|
||||
|
||||
snackbar.success(`${$t('snackbar.exercise.submission_updated')} '${status.text}'`);
|
||||
snackbar.success(`${$t('snackbar.exercise.submission_updated')} '${newStatus.text}'`);
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(data: SubmissionIdData) {
|
||||
untrack(() => {
|
||||
const statusBySelectedId = SELECTABLE_STATUS.find((status) => status.id === data.statusId);
|
||||
|
||||
if (!statusBySelectedId) {
|
||||
return;
|
||||
}
|
||||
const statusBySelectedId = SELECTABLE_STATUS.find((s) => s.id === data.statusId);
|
||||
|
||||
if (statusBySelectedId) {
|
||||
status = statusBySelectedId;
|
||||
|
||||
if (data.statusId !== selectedId) {
|
||||
selectedId = data.statusId;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -261,7 +266,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<Preview
|
||||
questions={Array.isArray(data.questions) ? data.questions.sort((a, b) => a.order - b.order) : []}
|
||||
questions={sortedQuestions}
|
||||
questionnaireMetaData={{
|
||||
answers: data.answers || {},
|
||||
isFinished: true
|
||||
@@ -279,7 +284,7 @@
|
||||
<div
|
||||
class="flex items-center justify-between gap-1 border-b border-l-0 border-r-0 border-t-0 border-gray-300 p-3"
|
||||
>
|
||||
<p class="text-base font-bold dark:text-white">
|
||||
<p class="text-base dark:text-white">
|
||||
{$t('course.navItem.submissions.grading_modal.details')}
|
||||
{#if data.isEarly}
|
||||
<span class="badge ml-2 rounded-sm bg-green-500 px-2 text-sm text-white">
|
||||
@@ -292,16 +297,23 @@
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<OverflowMenu open={openMenu} flipped>
|
||||
<OverflowMenuItem
|
||||
text={$t('delete_modal.label')}
|
||||
on:click={() => {
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="flex h-8 w-8 items-center justify-center rounded-md hover:bg-gray-100 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item
|
||||
class="text-red-600"
|
||||
onclick={() => {
|
||||
openDeletePrompt = true;
|
||||
openMenu = false;
|
||||
}}
|
||||
danger
|
||||
/>
|
||||
</OverflowMenu>
|
||||
>
|
||||
{$t('delete_modal.label')}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4 px-3 py-2 text-sm">
|
||||
@@ -309,12 +321,12 @@
|
||||
{$t('course.navItem.submissions.grading_modal.total_grade')}:
|
||||
</p>
|
||||
|
||||
<Tag class="w-fit rounded-md bg-gray-100 font-semibold text-black dark:bg-neutral-700 dark:text-white">
|
||||
<Badge class="w-fit rounded-md bg-gray-100 font-semibold text-black dark:bg-neutral-700 dark:text-white">
|
||||
{total}/{maxPoints}
|
||||
</Tag>
|
||||
</Badge>
|
||||
</div>
|
||||
<!-- <div class="flex items-center text-sm p-3">
|
||||
<p class="dark:text-white font-bold w-1/2">Status</p>
|
||||
<p class="dark:text-white w-1/2">Status</p>
|
||||
<div class="flex items-center">
|
||||
<p class="dark:text-white rounded-full w-5 h-5 bg-yellow-300 mr-2" />
|
||||
<p class="dark:text-white">Grading</p>
|
||||
@@ -345,7 +357,26 @@
|
||||
<p class="font-semibold text-gray-500 dark:text-white">
|
||||
{$t('course.navItem.submissions.grading_modal.status')}:
|
||||
</p>
|
||||
<Dropdown bind:selectedId items={SELECTABLE_STATUS} class="w-full" on:select={handleStatusChange} />
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={status.id.toString()}
|
||||
onValueChange={(value) => {
|
||||
if (value) {
|
||||
handleStatusChange(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="w-full">
|
||||
{status.text}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each SELECTABLE_STATUS as statusOption}
|
||||
<Select.Item value={statusOption.id.toString()}>
|
||||
{statusOption.text}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-start px-3 py-2 text-sm">
|
||||
@@ -379,7 +410,7 @@
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="flex items-center text-sm p-3">
|
||||
<p class="dark:text-white font-bold w-1/2">Teacher</p>
|
||||
<p class="dark:text-white w-1/2">Teacher</p>
|
||||
<p class="dark:text-white">rotimi-best</p>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
+5
-4
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Loading } from 'carbon-components-svelte';
|
||||
import type { ExerciseSubmissions } from '$lib/utils/types';
|
||||
import { questionnaire } from '../../store/exercise';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
|
||||
import { submissions } from './store';
|
||||
import { questionnaire } from '../../store/exercise';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { ExerciseSubmissions } from '$lib/utils/types';
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
@@ -53,7 +54,7 @@
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
<Loading withOverlay={true} />
|
||||
<Spinner />
|
||||
{:else if $submissions?.length}
|
||||
<div class="mb-5 mt-2 flex w-full gap-1 overflow-auto">
|
||||
{#each $submissions as student, i}
|
||||
|
||||
+68
-44
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import type { ExerciseSubmissions } from '$lib/utils/types';
|
||||
import { questionnaire } from '../../store/exercise';
|
||||
import { replaceHTMLTag } from '$lib/utils/functions/course';
|
||||
import type { BarChartOptions, PieChartOptions, PieChart, BarChartSimple } from '@carbon/charts-svelte';
|
||||
import { getChartOptions } from './functions';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
// import * as Chart from '@cio/ui/base/chart';
|
||||
// import { scaleBand } from '@cio/ui/base/chart';
|
||||
|
||||
import { submissions } from './store';
|
||||
import '@carbon/charts-svelte/styles.css';
|
||||
import { Loading } from 'carbon-components-svelte';
|
||||
import { questionnaire } from '../../store/exercise';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { ExerciseSubmissions } from '$lib/utils/types';
|
||||
import { replaceHTMLTag } from '$lib/utils/functions/course';
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
@@ -17,22 +17,31 @@
|
||||
|
||||
let { isLoading = $bindable(true) }: Props = $props();
|
||||
|
||||
type TranformedQuestionChartData = {
|
||||
group: string;
|
||||
value: number;
|
||||
type TransformedQuestionChartData = {
|
||||
option: string;
|
||||
responses: number;
|
||||
isCorrect?: boolean;
|
||||
};
|
||||
interface TranformedQuestion {
|
||||
|
||||
interface TransformedQuestion {
|
||||
title: string;
|
||||
type: number;
|
||||
chartData: TranformedQuestionChartData[];
|
||||
chartData: TransformedQuestionChartData[];
|
||||
}
|
||||
|
||||
let transformedQuestions: TranformedQuestion[] = $state([]);
|
||||
let barChart: typeof BarChartSimple | undefined = $state();
|
||||
let pieChart: typeof PieChart | undefined = $state();
|
||||
let pieOptions: PieChartOptions | undefined = $derived(getChartOptions(isLoading).pieOptions);
|
||||
let barOptions: BarChartOptions | undefined = $derived(getChartOptions(isLoading).barOptions);
|
||||
let transformedQuestions: TransformedQuestion[] = $state([]);
|
||||
|
||||
// Create dynamic chart config based on question data
|
||||
function getChartConfig(chartData: TransformedQuestionChartData[]): any {
|
||||
const config: any = {};
|
||||
chartData.forEach((item, index) => {
|
||||
config[`option${index}`] = {
|
||||
label: item.option,
|
||||
color: item.isCorrect ? '#22c55e' : `hsl(${index * 60}, 70%, 50%)`
|
||||
};
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
function getAnswerToQuestionOfStudent(
|
||||
questionId: number,
|
||||
@@ -57,7 +66,7 @@
|
||||
}
|
||||
|
||||
const getTransformedData = ($submissions: ExerciseSubmissions[]): void => {
|
||||
const _transformedQuestions: TranformedQuestion[] = [];
|
||||
const _transformedQuestions: TransformedQuestion[] = [];
|
||||
|
||||
$questionnaire.questions.forEach(
|
||||
(question: {
|
||||
@@ -69,7 +78,7 @@
|
||||
const transformedQuestion: {
|
||||
title: string;
|
||||
type: number;
|
||||
chartData: TranformedQuestionChartData[];
|
||||
chartData: TransformedQuestionChartData[];
|
||||
} = {
|
||||
title: question.title,
|
||||
type: question.question_type_id,
|
||||
@@ -79,9 +88,9 @@
|
||||
// If textarea don't calculate the value just get the student's answer
|
||||
if (transformedQuestion.type === 3) {
|
||||
$submissions.forEach((submission) => {
|
||||
const chartData: TranformedQuestionChartData = {
|
||||
group: getAnswerToQuestionOfStudent(question.id, true, submission)[0],
|
||||
value: 0
|
||||
const chartData: TransformedQuestionChartData = {
|
||||
option: getAnswerToQuestionOfStudent(question.id, true, submission)[0],
|
||||
responses: 0
|
||||
};
|
||||
|
||||
// Update the transformed question chartData
|
||||
@@ -91,9 +100,9 @@
|
||||
// radio or checkbox
|
||||
question.options.forEach((option) => {
|
||||
const { value, is_correct, label } = option;
|
||||
const chartData: TranformedQuestionChartData = {
|
||||
group: replaceHTMLTag(label),
|
||||
value: 0,
|
||||
const chartData: TransformedQuestionChartData = {
|
||||
option: replaceHTMLTag(label),
|
||||
responses: 0,
|
||||
isCorrect: is_correct
|
||||
};
|
||||
|
||||
@@ -101,7 +110,7 @@
|
||||
const studentAnswer = getAnswerToQuestionOfStudent(question.id, false, studentSubmission);
|
||||
|
||||
if (studentAnswer.includes(value)) {
|
||||
chartData.value += 1;
|
||||
chartData.responses += 1;
|
||||
}
|
||||
});
|
||||
transformedQuestion.chartData.push(chartData);
|
||||
@@ -119,12 +128,6 @@
|
||||
console.log({ transformedQuestions });
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const charts = await import('@carbon/charts-svelte');
|
||||
barChart = charts.BarChartSimple;
|
||||
pieChart = charts.PieChart;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($submissions?.length) {
|
||||
getTransformedData($submissions);
|
||||
@@ -133,28 +136,49 @@
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
<Loading withOverlay={true} />
|
||||
<Spinner />
|
||||
{:else if browser}
|
||||
<div>
|
||||
<p class="mb-3 text-2xl">
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.analytics.summary.question_chart')}
|
||||
</p>
|
||||
{#each transformedQuestions as q}
|
||||
<div class="mb-4">
|
||||
<p>{q.title}</p>
|
||||
{#if q.type === 1 && pieOptions}
|
||||
{@const SvelteComponent = pieChart}
|
||||
<SvelteComponent data={q.chartData} options={pieOptions} />
|
||||
{:else if q.type === 2 && barOptions}
|
||||
{@const SvelteComponent_1 = barChart}
|
||||
<SvelteComponent_1 data={q.chartData} options={barOptions} />
|
||||
<div class="mb-6">
|
||||
<p class="mb-3 font-medium">{q.title}</p>
|
||||
{#if q.type === 1}
|
||||
<!-- Pie Chart for Radio Questions (single choice) -->
|
||||
{@const chartConfig = getChartConfig(q.chartData)}
|
||||
{console.log('chartConfig', chartConfig)}
|
||||
<!-- <Chart.Container config={chartConfig} class="min-h-[300px] w-full">
|
||||
<Chart.PieChart data={q.chartData} value="responses" name="option" />
|
||||
</Chart.Container> -->
|
||||
{:else if q.type === 2}
|
||||
<!-- Bar Chart for Checkbox Questions (multiple choice) -->
|
||||
{@const chartConfig = getChartConfig(q.chartData)}
|
||||
{console.log('chartConfig', chartConfig)}
|
||||
<!-- <Chart.Container config={chartConfig} class="min-h-[300px] w-full">
|
||||
<Chart.BarChart
|
||||
data={q.chartData}
|
||||
xScale={scaleBand().padding(0.25)}
|
||||
x="option"
|
||||
y="responses"
|
||||
axis="x"
|
||||
tooltip={false}
|
||||
series={q.chartData.map((item, idx) => ({
|
||||
key: 'responses',
|
||||
label: item.option,
|
||||
color: item.isCorrect ? '#22c55e' : `hsl(${idx * 60}, 70%, 50%)`
|
||||
}))}
|
||||
/>
|
||||
</Chart.Container> -->
|
||||
{:else}
|
||||
<!-- Text answers -->
|
||||
<div class="max-h-[250px] overflow-auto">
|
||||
<ul>
|
||||
{#each q.chartData as answer (answer)}
|
||||
{#if answer.group}
|
||||
{#if answer.option}
|
||||
<div class="my-1 w-full rounded bg-slate-100 p-2 dark:bg-slate-300">
|
||||
<li class="text-base font-medium text-black">{answer.group}</li>
|
||||
<li class="text-base font-medium text-black">{answer.option}</li>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
+8
-6
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import QuestionContainer from '$lib/components/QuestionContainer/index.svelte';
|
||||
import { questionnaire } from '../store/exercise';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import DateTime from '$lib/components/Form/DateTime.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { questionnaire } from '../store/exercise';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import QuestionContainer from '$lib/components/QuestionContainer/index.svelte';
|
||||
|
||||
interface Props {
|
||||
preview: boolean;
|
||||
@@ -43,14 +44,15 @@
|
||||
<p class="mb-1">
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.description.heading')}
|
||||
</p>
|
||||
|
||||
<TextEditor
|
||||
value={$questionnaire.description}
|
||||
content={$questionnaire.description}
|
||||
onChange={(html) => {
|
||||
$questionnaire.is_description_dirty = true;
|
||||
$questionnaire.description = html;
|
||||
}}
|
||||
editorClass="max-h-[300px]"
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.description.describe')}
|
||||
maxHeight={300}
|
||||
/>
|
||||
</div>
|
||||
{:else if preview}
|
||||
@@ -78,7 +80,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<article class="preview prose prose-sm sm:prose mt-3 p-2">
|
||||
<article class="preview prose prose-sm mt-3 p-2 sm:prose">
|
||||
{@html $questionnaire.description || $t('course.navItem.lessons.exercises.all_exercises.description.no')}
|
||||
</article>
|
||||
{/if}
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { PageBody } from '$lib/components/Page';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { upsertExercise } from '$lib/utils/services/courses';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
// ContentSwitcher,
|
||||
OverflowMenu,
|
||||
OverflowMenuItem
|
||||
// Switch
|
||||
} from 'carbon-components-svelte';
|
||||
import CirclePlusIcon from '@lucide/svelte/icons/circle-plus';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import EyeIcon from '@lucide/svelte/icons/eye';
|
||||
import { onDestroy, onMount, untrack } from 'svelte';
|
||||
import * as Breadcrumb from '@cio/ui/base/breadcrumb';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
// import * as Tabs from '@cio/ui/base/tabs';
|
||||
import CirclePlusIcon from '@lucide/svelte/icons/circle-plus';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
|
||||
import {
|
||||
handleAddQuestion,
|
||||
questionnaire,
|
||||
@@ -28,10 +17,20 @@
|
||||
reset,
|
||||
validateQuestionnaire
|
||||
} from '../store/exercise';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { upsertExercise } from '$lib/utils/services/courses';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
|
||||
import EditMode from './EditMode.svelte';
|
||||
import Analytics from './Submissions/index.svelte';
|
||||
import UpdateDescription from './UpdateDescription.svelte';
|
||||
import ViewMode from './ViewMode.svelte';
|
||||
import { PageBody } from '$lib/components/Page';
|
||||
import Analytics from './Submissions/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import UpdateDescription from './UpdateDescription.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
|
||||
interface Props {
|
||||
exerciseId?: string;
|
||||
@@ -45,7 +44,7 @@
|
||||
let preview: boolean = $state(false);
|
||||
let shouldDeleteExercise = $state(false);
|
||||
let isSaving = $state(false);
|
||||
let selectedIndex = $state(0);
|
||||
let selectedTab = $state('questions');
|
||||
|
||||
async function handleSave() {
|
||||
if ($globalStore.isStudent) return;
|
||||
@@ -84,21 +83,17 @@
|
||||
reset();
|
||||
});
|
||||
|
||||
function onSelectedIndexChange(index: number) {
|
||||
untrack(() => {
|
||||
goto($page.url.pathname + '?tabIndex=' + index);
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const tabIndex = $page.url.searchParams.get('tabIndex');
|
||||
if (tabIndex) {
|
||||
selectedIndex = parseInt(tabIndex);
|
||||
const tabParam = $page.url.searchParams.get('tab');
|
||||
if (tabParam) {
|
||||
selectedTab = tabParam;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
onSelectedIndexChange(selectedIndex);
|
||||
untrack(() => {
|
||||
goto($page.url.pathname + '?tab=' + selectedTab);
|
||||
});
|
||||
});
|
||||
// $effect(() => {
|
||||
// const addNewQ = $questionnaire?.questions?.length < 1;
|
||||
@@ -114,29 +109,35 @@
|
||||
|
||||
<PageBody isPageNavHidden={$globalStore.isStudent} padding="px-4 overflow-x-hidden">
|
||||
<div class="sticky top-0 z-10 mb-3 bg-gray-100 p-2 dark:bg-neutral-800">
|
||||
<Breadcrumb noTrailingSlash>
|
||||
<BreadcrumbItem href={path}>
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Link href={path}>
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.heading')}
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem href={`${path}/${exerciseId}`} isCurrentPage>
|
||||
</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator />
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Page>
|
||||
{$questionnaire.title}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</Breadcrumb.Page>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root>
|
||||
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<!-- <ContentSwitcher bind:selectedIndex class="mb-2">
|
||||
<Switch
|
||||
text="{$t('course.navItem.lessons.exercises.all_exercises.questions')} ({$questionnaire
|
||||
.questions.length})"
|
||||
/>
|
||||
<Switch
|
||||
text="{$t(
|
||||
'course.navItem.lessons.exercises.all_exercises.submissions'
|
||||
)} ({$questionnaire.totalSubmissions})"
|
||||
/>
|
||||
</ContentSwitcher> -->
|
||||
<!-- <Tabs.Root bind:value={selectedTab} class="w-full">
|
||||
<Tabs.List class="mb-2">
|
||||
<Tabs.Trigger value="questions">
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.questions')} ({$questionnaire.questions.length})
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="submissions">
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.submissions')} ({$questionnaire.totalSubmissions})
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs.Root> -->
|
||||
|
||||
{#if selectedIndex === 0}
|
||||
{#if selectedTab === 'questions'}
|
||||
<div class="right-0 flex w-full items-center justify-end">
|
||||
<div class="flex items-center">
|
||||
<PrimaryButton
|
||||
@@ -167,24 +168,33 @@
|
||||
>
|
||||
<CirclePlusIcon size={20} />
|
||||
</IconButton>
|
||||
<OverflowMenu flipped>
|
||||
<OverflowMenuItem
|
||||
text={$t('course.navItem.lessons.exercises.all_exercises.reorder')}
|
||||
on:click={() => ($questionnaireOrder.open = true)}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('course.navItem.lessons.exercises.all_exercises.delete_exercise')}
|
||||
on:click={() => (shouldDeleteExercise = true)}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8">
|
||||
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||
<span class="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={() => ($questionnaireOrder.open = true)}>
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.reorder')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
class="text-red-600 focus:text-red-600 dark:text-red-400"
|
||||
onclick={() => (shouldDeleteExercise = true)}
|
||||
>
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.delete_exercise')}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
|
||||
{#if selectedIndex === 0}
|
||||
{#if selectedTab === 'questions'}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<UpdateDescription {preview} />
|
||||
</RoleBasedSecurity>
|
||||
@@ -193,7 +203,7 @@
|
||||
{:else}
|
||||
<ViewMode {preview} {exerciseId} isFetchingExercise={isFetching} />
|
||||
{/if}
|
||||
{:else if selectedIndex === 1}
|
||||
{:else if selectedTab === 'submissions'}
|
||||
<Analytics bind:exerciseId />
|
||||
{/if}
|
||||
</PageBody>
|
||||
|
||||
+12
-20
@@ -1,24 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
// import { browser } from '$app/environment';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import Confetti from '$lib/components/Confetti/index.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import { Tag } from 'carbon-components-svelte';
|
||||
import { type GeneratedTemplates, getAllTemplates, TAGS } from '$lib/mocks';
|
||||
import type { ExerciseTemplate } from '$lib/utils/types';
|
||||
// import { lesson, lessonByTranslation } from '../store/lessons';
|
||||
// import { useCompletion } from 'ai/svelte';
|
||||
import Confetti from '$lib/components/Confetti/index.svelte';
|
||||
// import { toggleConfetti } from '$lib/components/Confetti/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
// import { getTextFromHTML } from '$lib/utils/functions/toHtml';
|
||||
// import { Circle3 } from 'svelte-loading-spinners';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
// import { lessonFallbackNote } from '$lib/utils/functions/translations';
|
||||
import type { ExerciseTemplate } from '$lib/utils/types';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { type GeneratedTemplates, getAllTemplates, TAGS } from '$lib/mocks';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -164,7 +158,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="flex items-center text-start font-bold">
|
||||
<p class="flex items-center text-start">
|
||||
<span class="mr-2 text-sm">{option.title}</span>
|
||||
{#if option.isDisabled}
|
||||
<ComingSoon />
|
||||
@@ -222,14 +216,12 @@
|
||||
<div>
|
||||
<div class="mb-5 flex items-center gap-2">
|
||||
{#each tags as tag}
|
||||
<Tag
|
||||
type={selectedTag === tag ? 'warm-gray' : 'outline'}
|
||||
<Badge
|
||||
class={selectedTag === tag ? 'bg-primary-400' : ''}
|
||||
interactive
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
selectedTag = tag;
|
||||
selectedTemplateId = '';
|
||||
}}>{tag}</Tag
|
||||
}}>{tag}</Badge
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -246,7 +238,7 @@
|
||||
onclick={() => (selectedTemplateId = template.id)}
|
||||
>
|
||||
<div class="flex h-full flex-col justify-evenly">
|
||||
<p class="flex items-center text-start text-sm font-bold">
|
||||
<p class="flex items-center text-start text-sm">
|
||||
{template.title}
|
||||
</p>
|
||||
<div class="flex flex-col items-start justify-between gap-1">
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import Backdrop from '$lib/components/Backdrop/index.svelte';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte';
|
||||
import { PageBody } from '$lib/components/Page';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { QUESTION_TYPES } from '$lib/components/Question/constants';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { formatDate } from '$lib/utils/functions/routes/dashboard';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { createExercise, createExerciseFromTemplate } from '$lib/utils/services/courses';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import type { ExerciseTemplate } from '$lib/utils/types';
|
||||
import { Breadcrumb, BreadcrumbItem } from 'carbon-components-svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { Moon } from 'svelte-loading-spinners';
|
||||
import Exercise from '../Exercise/index.svelte';
|
||||
import { isQuestionnaireFetching, questionnaire } from '../store/exercise';
|
||||
|
||||
import { lesson } from '../store/lessons';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Breadcrumb from '@cio/ui/base/breadcrumb';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import type { ExerciseTemplate } from '$lib/utils/types';
|
||||
import type { Exercise as ExerciseType } from '$lib/utils/types';
|
||||
import { formatDate } from '$lib/utils/functions/routes/dashboard';
|
||||
import { isQuestionnaireFetching, questionnaire } from '../store/exercise';
|
||||
import { createExercise, createExerciseFromTemplate } from '$lib/utils/services/courses';
|
||||
|
||||
import { PageBody } from '$lib/components/Page';
|
||||
import Exercise from '../Exercise/index.svelte';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import Backdrop from '$lib/components/Backdrop/index.svelte';
|
||||
import { QUESTION_TYPES } from '$lib/components/Question/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte';
|
||||
|
||||
interface Props {
|
||||
path?: string;
|
||||
@@ -177,9 +179,15 @@
|
||||
<PageBody isPageNavHidden={$globalStore.isStudent}>
|
||||
{#snippet header()}
|
||||
<slot:fragment>
|
||||
<Breadcrumb class="my-2">
|
||||
<BreadcrumbItem href={path}>{$t('course.navItem.lessons.exercises.heading')}</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
<Breadcrumb.Root class="my-2">
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Link href={path}>
|
||||
{$t('course.navItem.lessons.exercises.heading')}
|
||||
</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<PrimaryButton
|
||||
className="mr-2 my-2"
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { dndzone } from 'svelte-dnd-action';
|
||||
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import ListChecksIcon from '@lucide/svelte/icons/list-checks';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { lessons, handleSaveLesson } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { updateLesson } from '$lib/utils/services/courses';
|
||||
import type { Course, Lesson } from '$lib/utils/types';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { lessons, handleSaveLesson } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { Course, Lesson } from '$lib/utils/types';
|
||||
import { updateLesson } from '$lib/utils/services/courses';
|
||||
|
||||
const flipDurationMs = 300;
|
||||
|
||||
@@ -138,37 +142,48 @@
|
||||
<!-- 3 dot edit -->
|
||||
<div class="flex flex-row">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<OverflowMenu size="xl">
|
||||
<OverflowMenuItem
|
||||
text={lesson.is_unlocked
|
||||
? $t('course.navItem.lessons.add_lesson.lock')
|
||||
: $t('course.navItem.lessons.add_lesson.unlock')}
|
||||
on:click={() => {
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8">
|
||||
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||
<span class="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
lesson.is_unlocked = !lesson.is_unlocked;
|
||||
handleSaveLesson(lesson, $course.id);
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
text={lessonEditing === lesson.id
|
||||
? $t('course.navItem.lessons.add_lesson.save')
|
||||
: $t('course.navItem.lessons.add_lesson.edit')}
|
||||
on:click={() => {
|
||||
>
|
||||
{lesson.is_unlocked
|
||||
? $t('course.navItem.lessons.add_lesson.lock')
|
||||
: $t('course.navItem.lessons.add_lesson.unlock')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
if (lessonEditing === lesson.id) {
|
||||
saveLesson(lesson, $course.id);
|
||||
} else {
|
||||
lessonEditing = lesson.id;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('course.navItem.lessons.add_lesson.delete')}
|
||||
on:click={() => {
|
||||
>
|
||||
{lessonEditing === lesson.id
|
||||
? $t('course.navItem.lessons.add_lesson.save')
|
||||
: $t('course.navItem.lessons.add_lesson.edit')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
class="text-red-600 focus:text-red-600 dark:text-red-400"
|
||||
onclick={() => {
|
||||
lessonToDelete = lesson;
|
||||
openDeleteModal = true;
|
||||
}}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
>
|
||||
{$t('course.navItem.lessons.add_lesson.delete')}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+52
-29
@@ -4,7 +4,9 @@
|
||||
import { dndzone } from 'svelte-dnd-action';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import ListChecksIcon from '@lucide/svelte/icons/list-checks';
|
||||
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
@@ -222,18 +224,29 @@
|
||||
<IconButton contained size="small" onClick={() => handleAddLesson(section.id)} disabled={!!lessonEditing}>
|
||||
<PlusIcon size={16} />
|
||||
</IconButton>
|
||||
<OverflowMenu size="xl" flipped>
|
||||
<OverflowMenuItem
|
||||
text={$t('course.navItem.lessons.add_lesson.edit')}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8">
|
||||
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||
<span class="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item
|
||||
disabled={!!lessonEditing}
|
||||
on:click={() => onEdit({ sectionId: section.id, sectionTitle: section.title })}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('course.navItem.lessons.add_lesson.delete')}
|
||||
on:click={() => onDelete({ sectionId: section.id })}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
onclick={() => onEdit({ sectionId: section.id, sectionTitle: section.title })}
|
||||
>
|
||||
{$t('course.navItem.lessons.add_lesson.edit')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
class="text-red-600 focus:text-red-600 dark:text-red-400"
|
||||
onclick={() => onDelete({ sectionId: section.id })}
|
||||
>
|
||||
{$t('course.navItem.lessons.add_lesson.delete')}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
</div>
|
||||
</RoleBasedSecurity>
|
||||
@@ -309,26 +322,36 @@
|
||||
onClick={() => onSave({ lessonId: lesson.id }, lesson)}
|
||||
/>
|
||||
{:else}
|
||||
<OverflowMenu size="sm" flipped>
|
||||
<OverflowMenuItem
|
||||
text={lesson.is_unlocked
|
||||
? $t('course.navItem.lessons.add_lesson.lock')
|
||||
: $t('course.navItem.lessons.add_lesson.unlock')}
|
||||
on:click={() => {
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8">
|
||||
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||
<span class="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
lesson.is_unlocked = !lesson.is_unlocked;
|
||||
handleSaveLesson(lesson, $course.id);
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
text={$t('course.navItem.lessons.add_lesson.edit')}
|
||||
on:click={() => onEdit({ lessonId: lesson.id, lessonTitle: lesson.title })}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('course.navItem.lessons.add_lesson.delete')}
|
||||
on:click={() => onDelete({ lessonId: lesson.id })}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
>
|
||||
{lesson.is_unlocked
|
||||
? $t('course.navItem.lessons.add_lesson.lock')
|
||||
: $t('course.navItem.lessons.add_lesson.unlock')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => onEdit({ lessonId: lesson.id, lessonTitle: lesson.title })}>
|
||||
{$t('course.navItem.lessons.add_lesson.edit')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
class="text-red-600 focus:text-red-600 dark:text-red-400"
|
||||
onclick={() => onDelete({ lessonId: lesson.id })}
|
||||
>
|
||||
{$t('course.navItem.lessons.add_lesson.delete')}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
</div>
|
||||
</RoleBasedSecurity>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 10rem;" />
|
||||
<Skeleton class="h-40 w-full" />
|
||||
<div class="my-3">
|
||||
<SkeletonText style="width: 100%;" />
|
||||
<SkeletonText style="width: 100%;" />
|
||||
<SkeletonText style="width: 100%;" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+13
-15
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { LessonVideoType } from '$lib/utils/types';
|
||||
import { isLessonDirty, lesson } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { copyToClipboard, getVideoUrls, removeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { CopyButton, Tag } from 'carbon-components-svelte';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
|
||||
let genericLinks = $state('');
|
||||
let error = $state('');
|
||||
|
||||
function getVideoUrls(urls = '') {
|
||||
return (urls || '').split(',').filter((url) => !!url.trim());
|
||||
}
|
||||
|
||||
function addVideo() {
|
||||
const links = getVideoUrls(genericLinks);
|
||||
const validLinks = links.filter(isValidLink);
|
||||
@@ -53,10 +53,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeVideo(index = 0) {
|
||||
$lesson.materials.videos = $lesson.materials.videos.filter((_v, i) => i !== index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-{error ? 'center' : 'end'} justify-between gap-5">
|
||||
@@ -85,10 +81,12 @@
|
||||
{#each $lesson?.materials?.videos as video, index}
|
||||
{#if video.type === 'generic'}
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag type="blue">
|
||||
<Badge class="max-w-md truncate" variant="secondary">
|
||||
{video.link}
|
||||
</Tag>
|
||||
<CopyButton text={video.link} feedback="Copied to clipboard" />
|
||||
</Badge>
|
||||
<IconButton value="copy-video" onClick={() => copyToClipboard(video.link)}>
|
||||
<CopyIcon size={16} />
|
||||
</IconButton>
|
||||
<IconButton value="delete-video" onClick={() => removeVideo(index)}>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
|
||||
+7
-13
@@ -1,15 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Progress } from '@cio/ui/base/progress';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { lesson, lessonVideoUpload, cancelVideoUpload } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { ProgressBar } from 'carbon-components-svelte';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { lesson, lessonVideoUpload, cancelVideoUpload } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { VideoUploader } from '$lib/utils/services/courses/presign';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
interface Props {
|
||||
lessonId?: string;
|
||||
@@ -142,12 +141,7 @@
|
||||
<p class="mt-5 text-center">
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.uploading')}
|
||||
</p>
|
||||
<ProgressBar
|
||||
class="w-full"
|
||||
value={$lessonVideoUpload.uploadProgress}
|
||||
max={100}
|
||||
status={$lessonVideoUpload.uploadProgress === 100 ? 'finished' : 'active'}
|
||||
/>
|
||||
<Progress class="w-full" value={$lessonVideoUpload.uploadProgress} max={100} />
|
||||
<p class="text-sm">{helperText}</p>
|
||||
<div class="mt-3">
|
||||
<PrimaryButton
|
||||
|
||||
+14
-16
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { CopyButton, Tag } from 'carbon-components-svelte';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import type { LessonVideoType } from '$lib/utils/types';
|
||||
import { copyToClipboard, getVideoUrls, removeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
let youtubeLinks = $state('');
|
||||
let error = $state('');
|
||||
|
||||
function getVideoUrls(urls = '') {
|
||||
return (urls || '').split(',').filter((url) => !!url.trim());
|
||||
}
|
||||
|
||||
function addVideo() {
|
||||
const links = getVideoUrls(youtubeLinks);
|
||||
const validLinks = links.filter(isValidYouTubeLink);
|
||||
@@ -42,10 +42,6 @@
|
||||
|
||||
return youtubeRegex.test(link.trim());
|
||||
}
|
||||
|
||||
function removeVideo(index = 0) {
|
||||
$lesson.materials.videos = $lesson.materials.videos.filter((_v, i) => i !== index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-{error ? 'center' : 'end'} justify-between gap-5">
|
||||
@@ -74,10 +70,12 @@
|
||||
{#each $lesson?.materials?.videos as video, index}
|
||||
{#if video.type === 'youtube'}
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag type="blue">
|
||||
<Badge class="max-w-md truncate" variant="secondary">
|
||||
{video.link}
|
||||
</Tag>
|
||||
<CopyButton text={video.link} feedback="Copied to clipboard" />
|
||||
</Badge>
|
||||
<IconButton value="copy-video" onClick={() => copyToClipboard(video.link)}>
|
||||
<CopyIcon size={16} />
|
||||
</IconButton>
|
||||
<IconButton value="delete-video" onClick={() => removeVideo(index)}>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
|
||||
+31
-20
@@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { lesson, lessonCommentsChannel } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
import type { PostgrestSingleResponse, RealtimePostgresChangesPayload } from '@supabase/supabase-js';
|
||||
|
||||
import { group } from '$lib/components/Course/store';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import DeleteModal from '$lib/components/Modal/DeleteModal.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { lesson, lessonCommentsChannel } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import type { GroupPerson, LessonComment, LessonCommentInsertPayload } from '$lib/utils/types';
|
||||
import type { PostgrestSingleResponse, RealtimePostgresChangesPayload } from '@supabase/supabase-js';
|
||||
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
const supabase = getSupabase();
|
||||
const PAGE_SIZE = 20;
|
||||
@@ -261,7 +264,7 @@
|
||||
<div class="mx-auto w-full max-w-[65ch]">
|
||||
<!-- <hr class="my-5" /> -->
|
||||
<div class="mb-5">
|
||||
<p class="text-xl font-bold capitalize">
|
||||
<p class="text-xl capitalize">
|
||||
{$t('course.navItem.lessons.comments.title')} ({pagination.count})
|
||||
</p>
|
||||
</div>
|
||||
@@ -294,7 +297,7 @@
|
||||
|
||||
<div class="w-full rounded-md border px-4 pb-4 pt-2 dark:border-neutral-700">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<p class="text-md font-bold dark:text-white">
|
||||
<p class="text-md dark:text-white">
|
||||
{commentItem.name}
|
||||
<span
|
||||
class="ml-1 text-xs font-normal text-gray-800 dark:text-white"
|
||||
@@ -305,17 +308,25 @@
|
||||
</p>
|
||||
|
||||
{#if groupmember?.id === commentItem.groupmember_id}
|
||||
<OverflowMenu flipped>
|
||||
<OverflowMenuItem text="Edit" on:click={() => (editCommentId = commentItem.id)} />
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text="Delete"
|
||||
on:click={() => {
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="flex h-8 w-8 items-center justify-center rounded-md hover:bg-gray-100 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={() => (editCommentId = commentItem.id)}>Edit</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
class="text-red-600"
|
||||
onclick={() => {
|
||||
deleteCommentId = commentItem.id;
|
||||
openDeleteModal = true;
|
||||
}}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
>
|
||||
Delete
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
+86
-102
@@ -1,9 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { writable } from 'svelte/store';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import AddVideoToLesson from '$lib/components/Course/components/Lesson/Materials/Video/AddVideoToLesson.svelte';
|
||||
import AddDocumentToLesson from '$lib/components/Course/components/Lesson/Materials/Document/AddDocumentToLesson.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import * as Popover from '@cio/ui/base/popover';
|
||||
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import ListTodoIcon from '@lucide/svelte/icons/list-todo';
|
||||
import ListChecksIcon from '@lucide/svelte/icons/list-checks';
|
||||
import NotepadTextIcon from '@lucide/svelte/icons/notepad-text';
|
||||
import WandSparklesIcon from '@lucide/svelte/icons/wand-sparkles';
|
||||
|
||||
import * as CONSTANTS from './constants';
|
||||
import { orderedTabs } from './constants';
|
||||
import MODES from '$lib/utils/constants/mode';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import type { Content } from '@cio/ui/custom/editor';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import type { LessonPage, LOCALE } from '$lib/utils/types';
|
||||
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
|
||||
import {
|
||||
deleteLessonVideo,
|
||||
handleUpdateLessonMaterials,
|
||||
@@ -13,41 +30,26 @@
|
||||
lessonVideoUpload,
|
||||
lessonDocUpload
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import TabContent from '$lib/components/TabContent/index.svelte';
|
||||
import Tabs from '$lib/components/Tabs/index.svelte';
|
||||
// import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import MODES from '$lib/utils/constants/mode';
|
||||
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
|
||||
import { lessonFallbackNote, t } from '$lib/utils/functions/translations';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import type { LessonPage, LOCALE } from '$lib/utils/types';
|
||||
// import { useCompletion } from 'ai/svelte';
|
||||
import { Popover } from 'carbon-components-svelte';
|
||||
import NotepadTextIcon from '@lucide/svelte/icons/notepad-text';
|
||||
import ListTodoIcon from '@lucide/svelte/icons/list-todo';
|
||||
import ListChecksIcon from '@lucide/svelte/icons/list-checks';
|
||||
import WandSparklesIcon from '@lucide/svelte/icons/wand-sparkles';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import Loader from './Loader.svelte';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import Comments from './components/Comments.svelte';
|
||||
import Tabs from '$lib/components/Tabs/index.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import ComponentNote from './components/ComponentNote.svelte';
|
||||
import ComponentSlide from './components/ComponentSlide.svelte';
|
||||
import ComponentVideo from './components/ComponentVideo.svelte';
|
||||
import TabContent from '$lib/components/TabContent/index.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import ComponentDocument from './components/ComponentDocument.svelte';
|
||||
import * as CONSTANTS from './constants';
|
||||
import { orderedTabs } from './constants';
|
||||
import Loader from './Loader.svelte';
|
||||
import AddVideoToLesson from '$lib/components/Course/components/Lesson/Materials/Video/AddVideoToLesson.svelte';
|
||||
import AddDocumentToLesson from '$lib/components/Course/components/Lesson/Materials/Document/AddDocumentToLesson.svelte';
|
||||
|
||||
interface Props {
|
||||
mode?: any;
|
||||
@@ -67,30 +69,20 @@
|
||||
toggleMode = () => {}
|
||||
}: Props = $props();
|
||||
|
||||
// let content = $state<EdraEditorProps['Content']>();
|
||||
// let editor = $state<EdraEditorProps['Editor']>();
|
||||
// function onUpdate() {
|
||||
// content = editor?.getHTML();
|
||||
// console.log('content updated:', content);
|
||||
// if (mode === MODES.view) return;
|
||||
|
||||
// $lessonByTranslation[lessonId][$lesson.locale] = content;
|
||||
|
||||
// try {
|
||||
// localStorage.setItem(`lesson-${lessonId}-${$lesson.locale}`, content);
|
||||
// } catch (error) {}
|
||||
// $isLessonDirty = true;
|
||||
// }
|
||||
|
||||
let openPopover = $state<boolean>(false);
|
||||
let aiButtonClass =
|
||||
'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2';
|
||||
let player = $state<HTMLVideoElement>();
|
||||
let localeExists: Record<string, boolean> = {};
|
||||
// let prevContent = '';
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
let errors: Record<string, string> = {};
|
||||
// let editorWindowRef: Window | undefined = $state();
|
||||
// let editorInstance = $state<Editor>(); // Add proper typing
|
||||
('flex items-center gap-2 px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2');
|
||||
|
||||
let isLoading = writable(false);
|
||||
const lessonTitle = $derived($lesson.title);
|
||||
|
||||
const tabs = $derived.by(() => {
|
||||
const ordered = orderedTabs(CONSTANTS.tabs, $course.metadata?.lessonTabsOrder);
|
||||
const content = $lessonByTranslation[lessonId]?.[$lesson.locale] || '';
|
||||
@@ -114,9 +106,24 @@
|
||||
});
|
||||
});
|
||||
const componentsToRender = $derived(getComponentOrder(tabs));
|
||||
|
||||
let currentTab = $derived(tabs[0].value);
|
||||
|
||||
function onEditorChange(content: Content) {
|
||||
console.log('content updated:', content);
|
||||
|
||||
if (mode === MODES.view) return;
|
||||
|
||||
$lessonByTranslation[lessonId][$lesson.locale] = `${content}`;
|
||||
|
||||
try {
|
||||
localStorage.setItem(`lesson-${lessonId}-${$lesson.locale}`, `${content}`);
|
||||
} catch (error) {
|
||||
console.error('Error saving lesson note to localStorage', error);
|
||||
}
|
||||
|
||||
$isLessonDirty = true;
|
||||
}
|
||||
|
||||
const onChange = (tab) => {
|
||||
return () => {
|
||||
currentTab = tab;
|
||||
@@ -211,24 +218,29 @@
|
||||
$lessonVideoUpload.isModalOpen = true;
|
||||
};
|
||||
|
||||
let isLoading = writable(false);
|
||||
// const { input, handleSubmit, completion, isLoading } = useCompletion({
|
||||
// api: '/api/completion'
|
||||
// });
|
||||
|
||||
// function updateNoteByCompletion(completion: string) {
|
||||
// if (!completion) return;
|
||||
|
||||
// if ($lessonByTranslation[lessonId]) {
|
||||
// $lessonByTranslation[lessonId][$lesson.locale] = `${prevContent}${completion}`;
|
||||
// }
|
||||
|
||||
// autoSave($lesson.materials, $lessonByTranslation[lessonId], false, lessonId);
|
||||
|
||||
// if (editorWindowRef) {
|
||||
// const tmceBody = editorWindowRef?.document?.querySelector('body');
|
||||
// if (typeof tmceBody?.scrollHeight === 'number') {
|
||||
// editorWindowRef?.scrollTo(0, tmceBody.scrollHeight);
|
||||
// if (editorInstance && editorInstance.view) {
|
||||
// try {
|
||||
// // For TipTap editor, we can use the editor's commands to scroll
|
||||
// const { view } = editorInstance;
|
||||
// const { state } = view;
|
||||
// const endPos = state.doc.content.size;
|
||||
// // Move cursor to end and scroll into view
|
||||
// editorInstance.commands.setTextSelection(endPos);
|
||||
// editorInstance.commands.scrollIntoView();
|
||||
// // Alternative: Focus the editor
|
||||
// editorInstance.commands.focus();
|
||||
// } catch (error) {
|
||||
// console.warn('Error scrolling editor:', error);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -237,7 +249,7 @@
|
||||
// prevContent = $lessonByTranslation[lessonId]?.[$lesson.locale] || '';
|
||||
// const _lesson = $lessons.find((les) => les.id === $lesson.id);
|
||||
// $input = JSON.stringify({
|
||||
// type,
|
||||
// _type,
|
||||
// lessonTitle: _lesson?.title || '',
|
||||
// courseTitle: $course.title,
|
||||
// locale: $lesson.locale
|
||||
@@ -373,7 +385,9 @@
|
||||
<TabContent value={getValue('course.navItem.lessons.materials.tabs.note.title')} index={currentTab}>
|
||||
<div class="flex justify-end gap-1">
|
||||
<!-- Update this when ai-sdk is updated -->
|
||||
<div bind:this={aiButtonRef} class="hidden flex-row-reverse">
|
||||
<div class="hidden flex-row-reverse">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<PrimaryButton
|
||||
className="flex items-center relative"
|
||||
onClick={() => {
|
||||
@@ -386,14 +400,9 @@
|
||||
>
|
||||
<WandSparklesIcon size={16} />
|
||||
AI
|
||||
<Popover
|
||||
caret
|
||||
align="left"
|
||||
bind:open={openPopover}
|
||||
on:click:outside={({ detail }) => {
|
||||
openPopover = aiButtonRef?.contains(detail.target) || false;
|
||||
}}
|
||||
>
|
||||
</PrimaryButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="start" class="w-80">
|
||||
<div class="p-2">
|
||||
<button class={aiButtonClass} onclick={() => callAI('outline')}>
|
||||
<ListChecksIcon size={16} />
|
||||
@@ -408,48 +417,23 @@
|
||||
{$t('course.navItem.lessons.materials.tabs.note.ai.activities')}
|
||||
</button>
|
||||
</div>
|
||||
</Popover>
|
||||
</PrimaryButton>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 h-[60vh]">
|
||||
<!-- <div class="bg-background z-50 mt-12 size-full max-w-5xl rounded-md border border-dashed">
|
||||
{#if editor && !editor.isDestroyed}
|
||||
<EdraToolBar
|
||||
class="bg-secondary/50 flex w-full items-center overflow-x-auto border-b border-dashed p-0.5"
|
||||
{editor}
|
||||
/>
|
||||
{:else}
|
||||
<div>
|
||||
Debug: Editor state - {editor ? 'exists' : 'undefined'}, destroyed: {editor?.isDestroyed || 'N/A'}
|
||||
</div>
|
||||
{/if}
|
||||
<EdraEditor
|
||||
bind:editor
|
||||
content={editorValue}
|
||||
autofocus={true}
|
||||
class="h-[300px] outline-none"
|
||||
{onUpdate}
|
||||
/>
|
||||
</div> -->
|
||||
<!-- <TextEditor
|
||||
id={lessonId}
|
||||
bind:editorWindowRef
|
||||
value={editorValue}
|
||||
onChange={(html) => {
|
||||
if (mode === MODES.view) return;
|
||||
|
||||
$lessonByTranslation[lessonId][$lesson.locale] = html;
|
||||
|
||||
try {
|
||||
// Backup locale of lesson content
|
||||
localStorage.setItem(`lesson-${lessonId}-${$lesson.locale}`, html);
|
||||
} catch (error) {}
|
||||
$isLessonDirty = true;
|
||||
<TextEditor
|
||||
content={_editorValue}
|
||||
onChange={(content) => onEditorChange(content)}
|
||||
onReady={() =>
|
||||
// editor
|
||||
{
|
||||
// editorInstance = editor;
|
||||
// editorWindowRef = editor.view.dom.ownerDocument.defaultView;
|
||||
}}
|
||||
placeholder={$t('course.navItem.lessons.materials.tabs.note.placeholder')}
|
||||
/> -->
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import ZapIcon from '@lucide/svelte/icons/zap';
|
||||
import { SkeletonText } from 'carbon-components-svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import { handleAddLessonWidget } from '../Lesson/store';
|
||||
import NavIcons from './NavIcons.svelte';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { COURSE_VERSION } from '$lib/utils/types';
|
||||
|
||||
interface Props {
|
||||
handleClick?: any;
|
||||
id?: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
isGroupActive?: boolean;
|
||||
isExpanded: boolean | undefined;
|
||||
total?: number;
|
||||
isLoading?: boolean;
|
||||
isLesson?: boolean;
|
||||
isSection?: boolean;
|
||||
isStudent?: boolean;
|
||||
isPaidFeature?: boolean;
|
||||
className?: string;
|
||||
btnPadding?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
handleClick = () => {},
|
||||
id = '',
|
||||
name = '',
|
||||
label = '',
|
||||
isGroupActive = false,
|
||||
isExpanded = $bindable(),
|
||||
total = 0,
|
||||
isLoading = true,
|
||||
isLesson = false,
|
||||
isSection = false,
|
||||
isStudent = true,
|
||||
isPaidFeature = false,
|
||||
className = '',
|
||||
btnPadding = 'py-3 px-4',
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
function addLesson() {
|
||||
goto('/courses/' + $course.id + '/lessons');
|
||||
$handleAddLessonWidget.open = true;
|
||||
|
||||
if ($course.version === COURSE_VERSION.V2) {
|
||||
// If it is section, then add lesson to section
|
||||
$handleAddLessonWidget.isSection = isSection ? false : true;
|
||||
|
||||
if (isSection) {
|
||||
$handleAddLessonWidget.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
if (isSection) {
|
||||
toggleIsExpanded();
|
||||
}
|
||||
|
||||
handleClick();
|
||||
}
|
||||
|
||||
function toggleIsExpanded() {
|
||||
isExpanded = !isExpanded;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={className}>
|
||||
<button
|
||||
class="item group relative flex items-center {btnPadding} mb-1 ml-2 {NavClasses.item} {isGroupActive &&
|
||||
!isLoading &&
|
||||
NavClasses.active} w-[95%]"
|
||||
tabindex="0"
|
||||
onclick={onClick}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<div class="flex w-full items-center gap-2.5">
|
||||
<NavIcons {name} />
|
||||
{#if isLoading}
|
||||
<div class="mx-auto w-11/12">
|
||||
<SkeletonText class="rounded-md" style="margin: 0px; height: 20px;" />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-md line-clamp-2 text-start font-bold leading-4">{label}</span>
|
||||
{#if total}
|
||||
<span class="ml-1">({total})</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<span class="grow"></span>
|
||||
|
||||
{#if isPaidFeature && $isFreePlan}
|
||||
<ZapIcon size={16} class="filled" />
|
||||
{/if}
|
||||
{#if (isLesson || isSection) && !isLoading && !isStudent}
|
||||
<IconButton stopPropagation={true} onClick={addLesson} size="small">
|
||||
<PlusIcon size={16} />
|
||||
</IconButton>
|
||||
<IconButton size="small" stopPropagation={true} onClick={toggleIsExpanded}>
|
||||
{#if isExpanded}
|
||||
<ChevronUpIcon size={16} class="carbon-icon dark:text-white" />
|
||||
{:else}
|
||||
<ChevronDownIcon size={16} class="carbon-icon dark:text-white" />
|
||||
{/if}
|
||||
</IconButton>
|
||||
{/if}
|
||||
</button>
|
||||
{#if isExpanded && !isLoading}
|
||||
<div in:slide out:slide class="flex flex-col">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,44 +0,0 @@
|
||||
<script lang="ts">
|
||||
import TableOfContentsIcon from '@lucide/svelte/icons/table-of-contents';
|
||||
import BookCheckIcon from '@lucide/svelte/icons/book-check';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import AwardIcon from '@lucide/svelte/icons/award';
|
||||
import ScrollTextIcon from '@lucide/svelte/icons/scroll-text';
|
||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||
import MegaphoneIcon from '@lucide/svelte/icons/megaphone';
|
||||
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
|
||||
import SquareLibrary from '@lucide/svelte/icons/square-library';
|
||||
import BookOpenCheck from '@lucide/svelte/icons/book-open-check';
|
||||
import PanelTop from '@lucide/svelte/icons/panel-top';
|
||||
import { NAV_IDS } from './constants';
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
let { name = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if name === NAV_IDS.SECTION}
|
||||
<TableOfContentsIcon size={16} class="group-hover:animate-flip-book" />
|
||||
{:else if name === NAV_IDS.NEWS_FEED}
|
||||
<MegaphoneIcon size={16} class="group-hover:animate-shake-announcement" />
|
||||
{:else if name === NAV_IDS.LESSONS}
|
||||
<ScrollTextIcon size={16} class="group-hover:animate-scroll-unroll" />
|
||||
{:else if name === NAV_IDS.ATTENDANCE}
|
||||
<BookOpenCheck size={16} class="group-hover:animate-check-items" />
|
||||
{:else if name === NAV_IDS.SUBMISSIONS}
|
||||
<SquareLibrary size={16} class="group-hover:animate-task-complete" />
|
||||
{:else if name === NAV_IDS.MARKS}
|
||||
<BookCheckIcon size={16} class="group-hover:animate-grade-glow" />
|
||||
{:else if name === NAV_IDS.PEOPLE}
|
||||
<UsersIcon size={16} class="group-hover:animate-people-wave" />
|
||||
{:else if name === NAV_IDS.ANALYTICS}
|
||||
<ChartLineIcon size={16} class="group-hover:animate-chart-grow" />
|
||||
{:else if name === NAV_IDS.LANDING_PAGE}
|
||||
<PanelTop size={16} class="group-hover:animate-screen-glow" />
|
||||
{:else if name === NAV_IDS.CERTIFICATES}
|
||||
<AwardIcon size={16} class="group-hover:animate-bounce" />
|
||||
{:else if name === NAV_IDS.SETTINGS}
|
||||
<SettingsIcon size={16} class="group-hover:animate-spin" />
|
||||
{/if}
|
||||
@@ -1,24 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { browser } from '$app/environment';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||
import LockIcon from '@lucide/svelte/icons/lock';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import NavExpandable from './NavExpandable.svelte';
|
||||
import { getNavItemRoute, getLessonsRoute, getLectureNo } from '$lib/components/Course/function';
|
||||
import { lessons, lessonSections } from '../Lesson/store/lessons';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { sideBar } from '$lib/components/Org/store';
|
||||
import * as Collapsible from '@cio/ui/base/collapsible';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import AwardIcon from '@lucide/svelte/icons/award';
|
||||
import PanelTop from '@lucide/svelte/icons/panel-top';
|
||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||
import MegaphoneIcon from '@lucide/svelte/icons/megaphone';
|
||||
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
|
||||
import BookCheckIcon from '@lucide/svelte/icons/book-check';
|
||||
import ScrollTextIcon from '@lucide/svelte/icons/scroll-text';
|
||||
import SquareLibrary from '@lucide/svelte/icons/square-library';
|
||||
import BookOpenCheck from '@lucide/svelte/icons/book-open-check';
|
||||
import TableOfContentsIcon from '@lucide/svelte/icons/table-of-contents';
|
||||
|
||||
import { NAV_IDS } from './constants';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { sideBar } from '$lib/components/Org/store';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { handleAddLessonWidget } from '../Lesson/store';
|
||||
import { getIsLessonComplete } from '../Lesson/functions';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { COURSE_TYPE, COURSE_VERSION, type Lesson } from '$lib/utils/types';
|
||||
import { NAV_IDS } from './constants';
|
||||
import { COURSE_TYPE, COURSE_VERSION } from '$lib/utils/types';
|
||||
import { lessons, lessonSections } from '../Lesson/store/lessons';
|
||||
import { getNavItemRoute, getLessonsRoute, getLectureNo } from '$lib/components/Course/function';
|
||||
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
@@ -27,356 +42,366 @@
|
||||
|
||||
let { path, isStudent = false }: Props = $props();
|
||||
|
||||
interface Section {
|
||||
id: string;
|
||||
order: number;
|
||||
label: string;
|
||||
lessons: Lesson[];
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
interface NavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
to: string;
|
||||
hideSortIcon: boolean;
|
||||
isLesson?: boolean;
|
||||
isPaidFeature: boolean;
|
||||
isExpanded?: boolean;
|
||||
sections?: Section[];
|
||||
show?: () => boolean;
|
||||
}
|
||||
|
||||
let resize = $state(false);
|
||||
let isDragging = false;
|
||||
let startX: number;
|
||||
let initialWidth: number;
|
||||
let sidebarRef: HTMLElement | undefined = $state();
|
||||
let menuContentRef: HTMLUListElement | undefined = $state();
|
||||
|
||||
const navItems: NavItem[] = $derived([
|
||||
const navItems = $derived(
|
||||
[
|
||||
{
|
||||
id: NAV_IDS.NEWS_FEED,
|
||||
label: $t('course.navItems.nav_news_feed'),
|
||||
to: getNavItemRoute($course.id),
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: false,
|
||||
title: $t('course.navItems.nav_news_feed'),
|
||||
url: getNavItemRoute($course.id),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id),
|
||||
show() {
|
||||
return isStudent ? $currentOrg.customization.course.newsfeed : true;
|
||||
}
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.NEWS_FEED)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.LESSONS,
|
||||
label: $t('course.navItems.nav_content'),
|
||||
to: getLessonsRoute($course.id),
|
||||
hideSortIcon: false,
|
||||
isPaidFeature: false,
|
||||
title: $t('course.navItems.nav_content'),
|
||||
url: getLessonsRoute($course.id),
|
||||
isActive:
|
||||
(path || page.url.pathname).includes('/lessons') ||
|
||||
(path || page.url.pathname) === getLessonsRoute($course.id),
|
||||
isLesson: true,
|
||||
sections: $lessonSections.map((section) => ({
|
||||
...section,
|
||||
label: section.title,
|
||||
isExpanded: true
|
||||
})),
|
||||
isExpanded: isStudent ? true : page.url.pathname.includes('/lessons')
|
||||
icon: getNavIcon(NAV_IDS.LESSONS),
|
||||
items:
|
||||
$course.version === COURSE_VERSION.V1
|
||||
? ($lessons || []).map((lesson, index) => ({
|
||||
title: lesson.title,
|
||||
url: getLessonsRoute($course.id, lesson.id),
|
||||
lesson,
|
||||
index,
|
||||
isV1: true
|
||||
}))
|
||||
: $lessonSections.flatMap((section) =>
|
||||
section.lessons.map((lesson) => ({
|
||||
title: lesson.title,
|
||||
url: getLessonsRoute($course.id, lesson.id),
|
||||
lesson,
|
||||
sectionTitle: section.title,
|
||||
isV1: false
|
||||
}))
|
||||
)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.ANALYTICS,
|
||||
label: $t('course.navItems.nav_analytics'),
|
||||
to: getNavItemRoute($course.id, 'analytics'),
|
||||
isPaidFeature: false,
|
||||
hideSortIcon: true,
|
||||
title: $t('course.navItems.nav_analytics'),
|
||||
url: getNavItemRoute($course.id, 'analytics'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'analytics'),
|
||||
show() {
|
||||
return !isStudent;
|
||||
}
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.ANALYTICS)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.ATTENDANCE,
|
||||
label: $t('course.navItems.nav_attendance'),
|
||||
to: getNavItemRoute($course.id, 'attendance'),
|
||||
isPaidFeature: false,
|
||||
hideSortIcon: true,
|
||||
title: $t('course.navItems.nav_attendance'),
|
||||
url: getNavItemRoute($course.id, 'attendance'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'attendance'),
|
||||
show() {
|
||||
if ($course.type !== COURSE_TYPE.LIVE_CLASS) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.ATTENDANCE)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.SUBMISSIONS,
|
||||
label: $t('course.navItems.nav_submissions'),
|
||||
to: getNavItemRoute($course.id, 'submissions'),
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: false,
|
||||
title: $t('course.navItems.nav_submissions'),
|
||||
url: getNavItemRoute($course.id, 'submissions'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'submissions'),
|
||||
show() {
|
||||
if (isStudent) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.SUBMISSIONS)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.MARKS,
|
||||
label: $t('course.navItems.nav_marks'),
|
||||
to: getNavItemRoute($course.id, 'marks'),
|
||||
isPaidFeature: false,
|
||||
hideSortIcon: true,
|
||||
title: $t('course.navItems.nav_marks'),
|
||||
url: getNavItemRoute($course.id, 'marks'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'marks'),
|
||||
show() {
|
||||
if ($course.type == COURSE_TYPE.LIVE_CLASS) {
|
||||
return isStudent ? $currentOrg.customization.course.grading : true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.MARKS)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.CERTIFICATES,
|
||||
label: $t('course.navItems.nav_certificates'),
|
||||
to: getNavItemRoute($course.id, 'certificates'),
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: true,
|
||||
title: $t('course.navItems.nav_certificates'),
|
||||
url: getNavItemRoute($course.id, 'certificates'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'certificates'),
|
||||
show() {
|
||||
// Dont show students if org on free plan
|
||||
if (isStudent && $isFreePlan) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.CERTIFICATES)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.LANDING_PAGE,
|
||||
label: $t('course.navItems.nav_landing_page'),
|
||||
to: getNavItemRoute($course.id, 'landingpage'),
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: false,
|
||||
title: $t('course.navItems.nav_landing_page'),
|
||||
url: getNavItemRoute($course.id, 'landingpage'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'landingpage'),
|
||||
show() {
|
||||
return !isStudent;
|
||||
}
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.LANDING_PAGE)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.PEOPLE,
|
||||
label: $t('course.navItems.nav_people'),
|
||||
to: getNavItemRoute($course.id, 'people'),
|
||||
isPaidFeature: false,
|
||||
hideSortIcon: true,
|
||||
title: $t('course.navItems.nav_people'),
|
||||
url: getNavItemRoute($course.id, 'people'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'people'),
|
||||
show() {
|
||||
return !isStudent;
|
||||
}
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.PEOPLE)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.SETTINGS,
|
||||
label: $t('course.navItems.nav_settings'),
|
||||
to: getNavItemRoute($course.id, 'settings'),
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: false,
|
||||
title: $t('course.navItems.nav_settings'),
|
||||
url: getNavItemRoute($course.id, 'settings'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'settings'),
|
||||
show() {
|
||||
return !isStudent;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.SETTINGS)
|
||||
}
|
||||
}
|
||||
]);
|
||||
].filter((item) => !item.show || item.show())
|
||||
);
|
||||
|
||||
const toggleSidebar = (defaultValue?: boolean) => {
|
||||
$sideBar.hidden = defaultValue ?? !$sideBar.hidden;
|
||||
};
|
||||
const toggleSidebarOnMobile = () => $isMobile && toggleSidebar();
|
||||
function addLesson(isSection: boolean, id: string) {
|
||||
goto('/courses/' + $course.id + '/lessons');
|
||||
$handleAddLessonWidget.open = true;
|
||||
|
||||
function handleMainGroupClick(href: string) {
|
||||
return () => {
|
||||
goto(href);
|
||||
toggleSidebarOnMobile();
|
||||
};
|
||||
}
|
||||
if ($course.version === COURSE_VERSION.V2) {
|
||||
$handleAddLessonWidget.isSection = isSection ? false : true;
|
||||
|
||||
function handleCursor(event: MouseEvent) {
|
||||
if (!resize && sidebarRef) {
|
||||
const isNearLeftBorder = event.clientX - sidebarRef.getBoundingClientRect().left < 8;
|
||||
const isNearRightBorder = sidebarRef.getBoundingClientRect().right - event.clientX < 8;
|
||||
|
||||
if (isNearLeftBorder || isNearRightBorder) {
|
||||
sidebarRef.style.cursor = 'ew-resize';
|
||||
} else {
|
||||
sidebarRef.style.cursor = 'auto';
|
||||
if (isSection) {
|
||||
$handleAddLessonWidget.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startDragging(event: MouseEvent) {
|
||||
if (event.button === 0 && sidebarRef) {
|
||||
event.preventDefault();
|
||||
const toggleSidebarOnMobile = () => $isMobile && ($sideBar.hidden = !$sideBar.hidden);
|
||||
|
||||
const isNearRightBorder = sidebarRef.getBoundingClientRect().right - event.clientX < 8;
|
||||
const isNearLeftBorder = event.clientX - sidebarRef.getBoundingClientRect().left < 8;
|
||||
function getNavIcon(id: string) {
|
||||
if (!id) return null;
|
||||
|
||||
if ((isNearRightBorder || isNearLeftBorder) && event.clientX >= 0 && event.clientX <= window.innerWidth) {
|
||||
isDragging = true;
|
||||
resize = true;
|
||||
startX = event.clientX;
|
||||
initialWidth = parseInt(getComputedStyle(sidebarRef).width, 10);
|
||||
}
|
||||
}
|
||||
if (id === NAV_IDS.SECTION) {
|
||||
return TableOfContentsIcon;
|
||||
} else if (id === NAV_IDS.NEWS_FEED) {
|
||||
return MegaphoneIcon;
|
||||
} else if (id === NAV_IDS.LESSONS) {
|
||||
return ScrollTextIcon;
|
||||
} else if (id === NAV_IDS.ATTENDANCE) {
|
||||
return BookOpenCheck;
|
||||
} else if (id === NAV_IDS.SUBMISSIONS) {
|
||||
return SquareLibrary;
|
||||
} else if (id === NAV_IDS.MARKS) {
|
||||
return BookCheckIcon;
|
||||
} else if (id === NAV_IDS.PEOPLE) {
|
||||
return UsersIcon;
|
||||
} else if (id === NAV_IDS.ANALYTICS) {
|
||||
return ChartLineIcon;
|
||||
} else if (id === NAV_IDS.LANDING_PAGE) {
|
||||
return PanelTop;
|
||||
} else if (id === NAV_IDS.CERTIFICATES) {
|
||||
return AwardIcon;
|
||||
} else if (id === NAV_IDS.SETTINGS) {
|
||||
return SettingsIcon;
|
||||
}
|
||||
|
||||
function stopDragging() {
|
||||
isDragging = false;
|
||||
resize = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
function dragSidebar(event: MouseEvent) {
|
||||
if (!(window.innerWidth >= 1024)) return;
|
||||
if (!isDragging || !sidebarRef || !menuContentRef) return;
|
||||
|
||||
const deltaX = event.clientX - startX;
|
||||
let newWidth = initialWidth + deltaX;
|
||||
|
||||
if (newWidth < 150) {
|
||||
sidebarRef.style.width = '0';
|
||||
menuContentRef.style.display = 'none';
|
||||
$sideBar.hidden = true;
|
||||
isDragging = false;
|
||||
resize = false;
|
||||
} else if (newWidth > window.innerWidth / 3 && window.innerWidth >= 1280) {
|
||||
sidebarRef.style.width = '40vw';
|
||||
} else if (newWidth > window.innerWidth / 4 && window.innerWidth < 1280) {
|
||||
sidebarRef.style.width = '28vw';
|
||||
} else {
|
||||
sidebarRef.style.width = newWidth + 'px';
|
||||
menuContentRef.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($isMobile || !sidebarRef) return;
|
||||
|
||||
sidebarRef.addEventListener('mousedown', startDragging);
|
||||
document.addEventListener('mousemove', dragSidebar);
|
||||
document.addEventListener('mouseup', stopDragging);
|
||||
document.addEventListener('mousemove', handleCursor);
|
||||
toggleSidebar(false);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isMobile || !sidebarRef) return;
|
||||
|
||||
sidebarRef.removeEventListener('mousedown', startDragging);
|
||||
document.removeEventListener('mousemove', dragSidebar);
|
||||
document.removeEventListener('mouseup', stopDragging);
|
||||
document.removeEventListener('mousemove', handleCursor);
|
||||
});
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class={`
|
||||
${$sideBar.hidden ? 'absolute z-[40] -translate-x-[100%]' : 'absolute z-[40] translate-x-0 md:relative'}
|
||||
h-[calc(100vh-48px)] w-[90vw] bg-gray-100 transition md:w-[300px] lg:w-[350px] dark:bg-black
|
||||
<Sidebar.Provider class="flex w-fit items-start gap-2">
|
||||
<Sidebar.Root collapsible="icon" class="inset-y-12 h-[calc(100vh-48px)] {$sideBar.hidden ? 'hidden' : ''}">
|
||||
<Sidebar.Content>
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel>Course Navigation</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each navItems as item (item.id)}
|
||||
<Collapsible.Root open={item.isActive} class="group/collapsible">
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuItem {...props}>
|
||||
{#if item.isLesson && item.items}
|
||||
<Collapsible.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<a href={item.url} onclick={toggleSidebarOnMobile}>
|
||||
<Sidebar.MenuButton
|
||||
{...props}
|
||||
tooltipContent={item.title}
|
||||
class="flex w-full items-center gap-4 py-2 pl-1.5"
|
||||
>
|
||||
{@const Icon = item.icon}
|
||||
<Icon size={16} class="nav-icon group-hover:animate-{item.id}" />
|
||||
{item.title}
|
||||
|
||||
${
|
||||
resize ? 'border-r-8 border-r-blue-500' : 'dark:border-r-neutral-600'
|
||||
} border-r-1 overflow-y-auto border border-b-0 border-l-0 border-t-0`}
|
||||
style={$sideBar.hidden === true ? 'width:0' : 'width:300px'}
|
||||
bind:this={sidebarRef}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<ul class="sidebar-content my-5" bind:this={menuContentRef} style={$sideBar.hidden === true ? '' : 'display:block'}>
|
||||
{#each navItems as navItem}
|
||||
{#if !navItem.show || (typeof navItem.show === 'function' && navItem.show())}
|
||||
<NavExpandable
|
||||
id={navItem.id}
|
||||
name={navItem.id}
|
||||
label={navItem.label}
|
||||
handleClick={handleMainGroupClick(navItem.to)}
|
||||
isGroupActive={(path || page.url.pathname) === navItem.to}
|
||||
total={navItem.isLesson ? ($lessons || []).length : 0}
|
||||
isLoading={!$course.id}
|
||||
isLesson={navItem.isLesson}
|
||||
isPaidFeature={navItem.isPaidFeature}
|
||||
isExpanded={navItem.isExpanded}
|
||||
{isStudent}
|
||||
>
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<Plus
|
||||
size={20}
|
||||
class="rounded-full p-1 hover:bg-gray-200"
|
||||
onclick={() => addLesson(false, item.id)}
|
||||
/>
|
||||
|
||||
<ChevronRightIcon
|
||||
size={20}
|
||||
class="rounded-full p-1 transition-transform duration-200 hover:bg-gray-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</div>
|
||||
</Sidebar.MenuButton>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub>
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
{#each $lessons as item, index}
|
||||
{#each item.items as lessonItem}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton
|
||||
isActive={(path || page.url.pathname).includes(lessonItem.lesson.id)}
|
||||
>
|
||||
{#snippet child({ props })}
|
||||
<a
|
||||
class="mb-2 w-[95%] pl-7 text-[0.80rem] text-black dark:text-white {isStudent && !item.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
: ''}"
|
||||
href={isStudent && !item.is_unlocked ? page.url.pathname : getLessonsRoute($course.id, item.id)}
|
||||
href={isStudent && !lessonItem.lesson.is_unlocked
|
||||
? page.url.pathname
|
||||
: lessonItem.url}
|
||||
onclick={toggleSidebarOnMobile}
|
||||
aria-disabled={!item.is_unlocked}
|
||||
title={item.title}
|
||||
>
|
||||
<div
|
||||
class="flex items-center px-4 py-2 {NavClasses.item} {(path || page.url.pathname).includes(
|
||||
item.id
|
||||
) && NavClasses.active}"
|
||||
aria-disabled={!lessonItem.lesson.is_unlocked}
|
||||
title={lessonItem.lesson.title}
|
||||
class="flex w-full items-center gap-2 {isStudent && !lessonItem.lesson.is_unlocked
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''}"
|
||||
{...props}
|
||||
>
|
||||
{#if lessonItem.isV1 && 'index' in lessonItem}
|
||||
<TextChip
|
||||
value={getLectureNo(index + 1)}
|
||||
className="bg-primary-200 text-primary-600 text-xs mr-2"
|
||||
value={getLectureNo(lessonItem.index + 1)}
|
||||
className="bg-primary-200 text-primary-600 text-xs"
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<span class="line-clamp-2 w-[85%] text-ellipsis">{item.title}</span>
|
||||
<span class="grow"></span>
|
||||
{#if !item.is_unlocked}
|
||||
<span class="text-md ml-2" title="This lesson is locked.">
|
||||
<LockIcon size={16} class="carbon-icon dark:text-white" />
|
||||
</span>
|
||||
{:else if getIsLessonComplete(item.lesson_completion, $profile.id)}
|
||||
<span class="ml-2" title="You have completed this lesson">
|
||||
{/if}
|
||||
<span class="flex-1 truncate">{lessonItem.lesson.title}</span>
|
||||
{#if !lessonItem.lesson.is_unlocked}
|
||||
<LockIcon size={16} class="shrink-0" />
|
||||
{:else if getIsLessonComplete(lessonItem.lesson.lesson_completion, $profile.id)}
|
||||
<span class="shrink-0">
|
||||
<CircleCheckIcon size={16} filled />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
{:else if navItem.sections}
|
||||
{#each navItem.sections as section}
|
||||
<NavExpandable
|
||||
id={section.id}
|
||||
name={NAV_IDS.SECTION}
|
||||
label={section.label}
|
||||
isLoading={!$course.id}
|
||||
isSection={true}
|
||||
isExpanded={section.isExpanded}
|
||||
className="ml-4"
|
||||
btnPadding="py-2 px-3"
|
||||
{isStudent}
|
||||
{:else}
|
||||
{#each $lessonSections as section}
|
||||
<Collapsible.Root open={true} class="group/section">
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuSubItem {...props}>
|
||||
<Collapsible.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuSubButton
|
||||
{...props}
|
||||
class="flex w-full items-center gap-2 font-medium"
|
||||
>
|
||||
{#each section.lessons as item}
|
||||
{@const Icon = getNavIcon(NAV_IDS.SECTION)}
|
||||
<Icon size={14} />
|
||||
<span class="flex-1 truncate">{section.title}</span>
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<Plus
|
||||
size={20}
|
||||
class="rounded-full p-1 hover:bg-gray-200"
|
||||
onclick={() => addLesson(true, section.id)}
|
||||
/>
|
||||
|
||||
<ChevronRightIcon
|
||||
size={20}
|
||||
class="rounded-full p-1 transition-transform duration-200 hover:bg-gray-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</div>
|
||||
</Sidebar.MenuSubButton>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub class="ml-2">
|
||||
{#each section.lessons as lesson}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton
|
||||
isActive={(path || page.url.pathname).includes(lesson.id)}
|
||||
>
|
||||
{#snippet child({ props })}
|
||||
<a
|
||||
class="mb-2 w-[95%] pl-7 text-[0.80rem] text-black dark:text-white {isStudent && !item.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
: ''}"
|
||||
href={isStudent && !item.is_unlocked ? page.url.pathname : getLessonsRoute($course.id, item.id)}
|
||||
href={isStudent && !lesson.is_unlocked
|
||||
? page.url.pathname
|
||||
: getLessonsRoute($course.id, lesson.id)}
|
||||
onclick={toggleSidebarOnMobile}
|
||||
aria-disabled={!item.is_unlocked}
|
||||
title={item.title}
|
||||
aria-disabled={!lesson.is_unlocked}
|
||||
title={lesson.title}
|
||||
class="flex w-full items-center gap-2 {isStudent && !lesson.is_unlocked
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''}"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
class="flex items-center px-4 py-2 {NavClasses.item} {(path || page.url.pathname).includes(
|
||||
item.id
|
||||
) && NavClasses.active}"
|
||||
>
|
||||
<span class="line-clamp-2 w-[85%] text-ellipsis">{item.title}</span>
|
||||
<span class="grow"></span>
|
||||
{#if !item.is_unlocked}
|
||||
<span class="text-md ml-2" title="This lesson is locked.">
|
||||
<LockIcon size={16} class="carbon-icon dark:text-white" />
|
||||
</span>
|
||||
{:else if getIsLessonComplete(item.lesson_completion, $profile.id)}
|
||||
<span class="ml-2" title="You have completed this lesson">
|
||||
<span class="flex-1 truncate">{lesson.title}</span>
|
||||
{#if !lesson.is_unlocked}
|
||||
<LockIcon size={16} class="shrink-0" />
|
||||
{:else if getIsLessonComplete(lesson.lesson_completion, $profile.id)}
|
||||
<span class="shrink-0">
|
||||
<CircleCheckIcon size={16} filled />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
</NavExpandable>
|
||||
</Sidebar.MenuSub>
|
||||
</Collapsible.Content>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/snippet}
|
||||
</Collapsible.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</NavExpandable>
|
||||
</Sidebar.MenuSub>
|
||||
</Collapsible.Content>
|
||||
{:else}
|
||||
<a href={item.url} onclick={toggleSidebarOnMobile}>
|
||||
<Sidebar.MenuButton
|
||||
tooltipContent={item.title}
|
||||
class="flex w-full cursor-pointer items-center gap-4 px-1.5 py-2 {item.isActive
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: ''}"
|
||||
>
|
||||
{@const Icon = item.icon}
|
||||
<Icon size={16} class="nav-icon group-hover:animate-{item.id}" />
|
||||
{item.title}
|
||||
</Sidebar.MenuButton>
|
||||
</a>
|
||||
{/if}
|
||||
</Sidebar.MenuItem>
|
||||
{/snippet}
|
||||
</Collapsible.Root>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
</Sidebar.Content>
|
||||
<Sidebar.Rail />
|
||||
</Sidebar.Root>
|
||||
|
||||
<Sidebar.Inset>
|
||||
<Sidebar.Trigger />
|
||||
</Sidebar.Inset>
|
||||
</Sidebar.Provider>
|
||||
|
||||
<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import { isNewFeedModal } from '$lib/components/Course/components/NewsFeed/store';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import { createNewFeed } from '$lib/utils/services/newsfeed';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import type { Feed, Author } from '$lib/utils/types/feed';
|
||||
import { NOTIFICATION_NAME, triggerSendEmail } from '$lib/utils/services/notification/notification';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { createNewsfeedValidation } from '$lib/utils/functions/validator';
|
||||
import { createNewFeed } from '$lib/utils/services/newsfeed';
|
||||
import { getTextFromHTML } from '$lib/utils/functions/toHtml';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { createNewsfeedValidation } from '$lib/utils/functions/validator';
|
||||
import { isNewFeedModal } from '$lib/components/Course/components/NewsFeed/store';
|
||||
import { NOTIFICATION_NAME, triggerSendEmail } from '$lib/utils/services/notification/notification';
|
||||
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
interface Props {
|
||||
author?: Author | any;
|
||||
@@ -123,12 +124,12 @@
|
||||
>
|
||||
<section class="w-2/ flex h-full flex-col rounded-xl pb-3">
|
||||
<TextEditor
|
||||
value={newPost}
|
||||
content={newPost}
|
||||
onChange={(text) => {
|
||||
newPost = text;
|
||||
}}
|
||||
editorClass="max-h-[400px]"
|
||||
placeholder={$t('course.navItem.news_feed.heading_button.placeholder')}
|
||||
maxHeight={400}
|
||||
/>
|
||||
{#if errors.newPost}
|
||||
<p class="text-sm text-red-500">{errors.newPost}</p>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
||||
import SendHorizontalIcon from '@lucide/svelte/icons/send-horizontal';
|
||||
import Chip from '$lib/components/Chip/index.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import pluralize from 'pluralize';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import SendHorizontalIcon from '@lucide/svelte/icons/send-horizontal';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import type { Author, Feed } from '$lib/utils/types/feed';
|
||||
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
|
||||
import { addNewsfeedCommentValidation } from '$lib/utils/functions/validator';
|
||||
import { isNewFeedModal } from '$lib/components/Course/components/NewsFeed/store';
|
||||
|
||||
import Chip from '$lib/components/Chip/index.svelte';
|
||||
import DeleteFeedConfirmation from './DeleteFeedConfirmation.svelte';
|
||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
import { isNewFeedModal } from '$lib/components/Course/components/NewsFeed/store';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
|
||||
import pluralize from 'pluralize';
|
||||
import { onMount } from 'svelte';
|
||||
import { addNewsfeedCommentValidation } from '$lib/utils/functions/validator';
|
||||
|
||||
interface Props {
|
||||
feed: Feed;
|
||||
@@ -138,11 +141,22 @@
|
||||
</span>
|
||||
</span>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<OverflowMenu flipped>
|
||||
<OverflowMenuItem text={feed.isPinned ? 'Unpin' : 'Pin'} on:click={() => onPin(feed.id, feed.isPinned)} />
|
||||
<OverflowMenuItem text="Edit" on:click={openEditFeed} />
|
||||
<OverflowMenuItem danger text="Delete" on:click={() => (isDeleteFeedModal = true)} />
|
||||
</OverflowMenu>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="flex h-8 w-8 items-center justify-center rounded-md hover:bg-gray-100 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={() => onPin(feed.id, feed.isPinned)}>
|
||||
{feed.isPinned ? 'Unpin' : 'Pin'}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={openEditFeed}>Edit</DropdownMenu.Item>
|
||||
<DropdownMenu.Item class="text-red-600" onclick={() => (isDeleteFeedModal = true)}>
|
||||
Delete
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
{#if !isHtmlValueEmpty(feed.content)}
|
||||
@@ -207,9 +221,18 @@
|
||||
</span>
|
||||
|
||||
{#if comment.author?.profile?.id === $profile.id || $isOrgAdmin}
|
||||
<OverflowMenu flipped class="hidden group-hover:flex">
|
||||
<OverflowMenuItem danger text="Delete" on:click={() => handleDeleteComment(comment.id)} />
|
||||
</OverflowMenu>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="hidden h-8 w-8 items-center justify-center rounded-md hover:bg-gray-100 group-hover:flex dark:hover:bg-neutral-700"
|
||||
>
|
||||
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item class="text-red-600" onclick={() => handleDeleteComment(comment.id)}>
|
||||
Delete
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script>
|
||||
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div class="mb-7 flex max-w-3xl flex-col gap-5 rounded-md border border-gray-300 p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<SkeletonPlaceholder style="height: 2rem; width: 2rem;" class="overflow-hidden rounded-full" />
|
||||
<SkeletonText width="30%" />
|
||||
<Skeleton class="h-8 w-8 overflow-hidden rounded-full" />
|
||||
<Skeleton class="h-4 w-[30%]" />
|
||||
</div>
|
||||
<SkeletonText paragraph lines={3} />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Popover } from 'carbon-components-svelte';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import QRCode from 'qrcode';
|
||||
import { untrack } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { toPng } from 'html-to-image';
|
||||
import { goto } from '$app/navigation';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import * as Popover from '@cio/ui/base/popover';
|
||||
|
||||
import { qrInviteNodeStore } from './store';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
import { getOrgTeam } from '$lib/utils/services/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
import { course, setCourse } from '$lib/components/Course/store';
|
||||
import { getStudentInviteLink } from '$lib/utils/functions/course';
|
||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { addGroupMember, fetchGroup } from '$lib/utils/services/courses';
|
||||
import { triggerSendEmail, NOTIFICATION_NAME } from '$lib/utils/services/notification/notification';
|
||||
|
||||
import ShareQrImage from './ShareQRImage.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
import { addGroupMember, fetchGroup } from '$lib/utils/services/courses';
|
||||
import { course, setCourse } from '$lib/components/Course/store';
|
||||
import { MultiSelect, Loading } from 'carbon-components-svelte';
|
||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { getOrgTeam } from '$lib/utils/services/org';
|
||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||
import { qrInviteNodeStore } from './store';
|
||||
import { getStudentInviteLink } from '$lib/utils/functions/course';
|
||||
import ShareQrImage from './ShareQRImage.svelte';
|
||||
import { triggerSendEmail, NOTIFICATION_NAME } from '$lib/utils/services/notification/notification';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Tutor {
|
||||
id: number;
|
||||
@@ -32,9 +36,10 @@
|
||||
}
|
||||
|
||||
let tutors: Tutor[] = $state([]);
|
||||
let selectedIds: Array<number> = $state([]);
|
||||
let selectedTutors: Tutor[] = $state([]);
|
||||
let selectedIds: string[] = $state([]);
|
||||
let isLoadingTutors = $state(false);
|
||||
|
||||
const selectedTutors = $derived(tutors.filter((tutor) => selectedIds.includes(tutor.id.toString())));
|
||||
let copied = $state(false);
|
||||
let qrImage = $state('');
|
||||
let isLoadingQRDownload = $state(false);
|
||||
@@ -192,18 +197,47 @@
|
||||
<form onsubmit={preventDefault(onSubmit)}>
|
||||
<div class="mb-8">
|
||||
<p class="mb-1 text-base font-semibold">{$t('course.navItem.people.invite_modal.invite')}</p>
|
||||
<MultiSelect
|
||||
disabled={isLoadingTutors}
|
||||
label={$t('course.navItem.people.invite_modal.select')}
|
||||
bind:selectedIds
|
||||
items={tutors}
|
||||
/>
|
||||
{#if isLoadingTutors}
|
||||
<span>
|
||||
<Loading withOverlay={false} small />
|
||||
</span>
|
||||
<Select.Root type="multiple" bind:value={selectedIds} disabled={isLoadingTutors}>
|
||||
<Select.Trigger class="w-full">
|
||||
<div>
|
||||
{#if selectedTutors.length > 0}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each selectedTutors as tutor}
|
||||
<Badge variant="secondary" class="flex items-center gap-1">
|
||||
{tutor.text}
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectedIds = selectedIds.filter((id) => id !== tutor.id.toString());
|
||||
}}
|
||||
class="rounded-sm hover:bg-gray-200"
|
||||
>
|
||||
<XIcon size={14} />
|
||||
</button>
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<span>
|
||||
{$t('course.navItem.people.invite_modal.select')}
|
||||
{/if}
|
||||
</div>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each tutors as tutor}
|
||||
<Select.Item value={tutor.id.toString()}>
|
||||
{tutor.text}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{#if isLoadingTutors}
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<Spinner class="h-4 w-4" />
|
||||
<span class="text-sm">Loading...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="mt-2 block text-sm">
|
||||
{$t('course.navItem.people.invite_modal.to_add')}
|
||||
<a href={`/org/${$currentOrg.siteName}/settings/teams`} class="text-primary-600 underline">
|
||||
{$t('course.navItem.people.invite_modal.go_to')}
|
||||
@@ -220,15 +254,16 @@
|
||||
<p class=" text-sm">{$t('course.navItem.people.invite_modal.via_link')}</p>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<button type="button" onclick={copyLink} class="text-primary-800 cursor-pointer font-bold capitalize underline">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<button type="button" onclick={copyLink} class="text-primary-800 cursor-pointer capitalize underline">
|
||||
{$t('course.navItem.people.invite_modal.copy_link')}
|
||||
</button>
|
||||
|
||||
<Popover caret open={copied} align="left">
|
||||
<div style="padding: 5px">{$t('course.navItem.people.invite_modal.success')}</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="start" class="w-auto">
|
||||
<div class="p-1 text-sm">{$t('course.navItem.people.invite_modal.success')}</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -25,17 +25,17 @@
|
||||
class="flex h-[60rem] w-[40rem] flex-col items-center justify-center rounded-xl bg-blue-900 p-10 pb-20"
|
||||
>
|
||||
<div class="rounded-3xl bg-white p-6 pb-3 text-center">
|
||||
<div class="my-4 rounded-xl bg-gray-100 p-2 text-xl font-bold">Scan QR</div>
|
||||
<div class="my-4 rounded-xl bg-gray-100 p-2 text-xl">Scan QR</div>
|
||||
<img src={qrImage} alt="qrcode" />
|
||||
<div class="pb-4 pt-1">
|
||||
<p class="text-primary-600 mt-2 text-2xl font-bold">{course.title}</p>
|
||||
<p class="text-primary-600 mt-2 text-2xl">{course.title}</p>
|
||||
<p class="mt-1 text-lg font-semibold text-black">{currentOrg.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if $isFreePlan}
|
||||
<div class="relative top-16 flex items-center gap-x-2 rounded-md bg-white px-4 py-2">
|
||||
<img src="/logo.svg" alt="logo" class="w-6" />
|
||||
<span class="text-base font-bold">ClassroomIO.com</span>
|
||||
<span class="text-base">ClassroomIO.com</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { CodeSnippet, Column, Grid, RadioButton, RadioButtonGroup, Row, Toggle } from 'carbon-components-svelte';
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import * as RadioGroup from '@cio/ui/base/radio-group';
|
||||
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
|
||||
import DragAndDrop from './DragAndDrop.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import Row from '$lib/components/Org/Settings/Layout/Row.svelte';
|
||||
import SectionTitle from '$lib/components/Org/SectionTitle.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
import DragAndDrop from './DragAndDrop.svelte';
|
||||
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import DeleteModal from '$lib/components/Modal/DeleteModal.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import Grid from '$lib/components/Org/Settings/Layout/Grid.svelte';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import generateSlug from '$lib/utils/functions/generateSlug';
|
||||
import { isObject } from '$lib/utils/functions/isObject';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { deleteCourse, updateCourse } from '$lib/utils/services/courses';
|
||||
import { currentOrg, currentOrgDomain, currentOrgPath, isFreePlan } from '$lib/utils/store/org';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Column from '$lib/components/Org/Settings/Layout/Column.svelte';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
|
||||
import { settings } from './store';
|
||||
import Copy from '@lucide/svelte/icons/copy';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
import { lessons } from '../Lesson/store/lessons';
|
||||
import { settings } from './store';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { isObject } from '$lib/utils/functions/isObject';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import generateSlug from '$lib/utils/functions/generateSlug';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import DeleteModal from '$lib/components/Modal/DeleteModal.svelte';
|
||||
import { deleteCourse, updateCourse } from '$lib/utils/services/courses';
|
||||
import { copyToClipboard } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
import { currentOrg, currentOrgDomain, currentOrgPath, isFreePlan } from '$lib/utils/store/org';
|
||||
|
||||
let isSaving = $state(false);
|
||||
let isLoading = $state(false);
|
||||
@@ -284,12 +291,24 @@
|
||||
<ArrowUpRightIcon size={16} />
|
||||
</IconButton>
|
||||
</p>
|
||||
<div class="flex items-center justify-between rounded-md border p-3">
|
||||
{#if $course.slug}
|
||||
<CodeSnippet wrapText type="multi" code={courseLink} />
|
||||
<p class="text-sm">{courseLink}</p>
|
||||
|
||||
<IconButton
|
||||
contained={true}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
copyToClipboard(courseLink);
|
||||
}}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<CodeSnippet code="Setup landing page to get course link" />
|
||||
<p class="text-sm">Setup landing page to get course link</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
<!-- <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
@@ -327,16 +346,18 @@
|
||||
{#if $isFreePlan}
|
||||
<UpgradeBanner>{$t('upgrade.download_lessons')}</UpgradeBanner>
|
||||
{:else}
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:toggled={$settings.lesson_download}
|
||||
on:toggle={() => {
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="lesson-download"
|
||||
bind:checked={$settings.lesson_download}
|
||||
onCheckedChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
>
|
||||
<span slot="labelA" style="color: gray">{$t('course.navItem.settings.disabled')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('course.navItem.settings.enabled')}</span>
|
||||
</Toggle>
|
||||
/>
|
||||
<Label for="lesson-download" class="text-sm text-gray-500">
|
||||
{$settings.lesson_download ? $t('course.navItem.settings.enabled') : $t('course.navItem.settings.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
@@ -345,7 +366,7 @@
|
||||
<SectionTitle>{$t('course.navItem.settings.course_download')}</SectionTitle>
|
||||
<p>{$t('course.navItem.settings.course_avail')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
{#if $isFreePlan}
|
||||
<UpgradeBanner>{$t('upgrade.download_course')}</UpgradeBanner>
|
||||
{:else}
|
||||
@@ -366,17 +387,23 @@
|
||||
<p>{$t('course.navItem.settings.course_type_desc')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<RadioButtonGroup
|
||||
hideLegend
|
||||
bind:selected={$settings.type}
|
||||
on:click={() => {
|
||||
<RadioGroup.Root
|
||||
value={$settings.type}
|
||||
onValueChange={(value) => {
|
||||
$settings.type = value as COURSE_TYPE;
|
||||
if (hasUnsavedChanges) return;
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
>
|
||||
<RadioButton labelText={$t('course.navItem.settings.live_class')} value={COURSE_TYPE.LIVE_CLASS} />
|
||||
<RadioButton labelText={$t('course.navItem.settings.self_paced')} value={COURSE_TYPE.SELF_PACED} />
|
||||
</RadioButtonGroup>
|
||||
<div class="mb-3 flex items-center space-x-2">
|
||||
<RadioGroup.Item value={COURSE_TYPE.LIVE_CLASS} id="live-class" />
|
||||
<Label for="live-class">{$t('course.navItem.settings.live_class')}</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value={COURSE_TYPE.SELF_PACED} id="self-paced" />
|
||||
<Label for="self-paced">{$t('course.navItem.settings.self_paced')}</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
@@ -386,16 +413,20 @@
|
||||
<p>{$t('course.navItem.settings.access')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:toggled={$settings.allow_new_students}
|
||||
on:click={() => {
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="allow-new-students"
|
||||
bind:checked={$settings.allow_new_students}
|
||||
onCheckedChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
>
|
||||
<span slot="labelA" style="color: gray">{$t('course.navItem.settings.disabled')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('course.navItem.settings.enabled')}</span>
|
||||
</Toggle>
|
||||
/>
|
||||
<Label for="allow-new-students" class="text-sm text-gray-500">
|
||||
{$settings.allow_new_students
|
||||
? $t('course.navItem.settings.enabled')
|
||||
: $t('course.navItem.settings.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
@@ -406,22 +437,22 @@
|
||||
<p>{$t('course.navItem.settings.determines')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:toggled={$settings.is_published}
|
||||
on:click={() => {
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="is-published"
|
||||
bind:checked={$settings.is_published}
|
||||
onCheckedChange={(checked) => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
on:toggle={(e) => {
|
||||
$settings.allow_new_students = e.detail.toggled;
|
||||
$settings.allow_new_students = checked;
|
||||
if (!$course.slug) {
|
||||
generateNewCourseLink();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span slot="labelA" style="color: gray">{$t('course.navItem.settings.unpublished')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('course.navItem.settings.published')}</span>
|
||||
</Toggle>
|
||||
/>
|
||||
<Label for="is-published" class="text-sm text-gray-500">
|
||||
{$settings.is_published ? $t('course.navItem.settings.published') : $t('course.navItem.settings.unpublished')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
@@ -440,12 +471,12 @@
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="flex w-full items-center justify-end p-5">
|
||||
<div class="flex w-full items-center justify-end p-5">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.settings.save')}
|
||||
isLoading={isSaving}
|
||||
isDisabled={isSaving}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
@@ -131,8 +131,8 @@ export async function setCourse(data: Course, setLesson = true) {
|
||||
people: []
|
||||
};
|
||||
|
||||
if (Array.isArray(groupData.members)) {
|
||||
for (const member of groupData.members) {
|
||||
if (Array.isArray(data.group.members)) {
|
||||
for (const member of data.group.members) {
|
||||
if (member.role_id === ROLE.STUDENT) {
|
||||
groupData.students.push(member);
|
||||
} else if (member.profile) {
|
||||
@@ -143,11 +143,9 @@ export async function setCourse(data: Course, setLesson = true) {
|
||||
}
|
||||
}
|
||||
|
||||
groupData.people = groupData.members;
|
||||
groupData.people = data.group.members;
|
||||
}
|
||||
|
||||
delete groupData.members;
|
||||
|
||||
group.set(groupData);
|
||||
}
|
||||
|
||||
|
||||
+20
-13
@@ -1,12 +1,15 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import { Toggle } from 'carbon-components-svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import { uploadAvatar } from '$lib/utils/services/courses';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { NAV_ITEM_KEY } from '../../constants';
|
||||
import { untrack } from 'svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { NAV_ITEM_KEY } from '../../constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { uploadAvatar } from '$lib/utils/services/courses';
|
||||
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
@@ -45,20 +48,24 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="border-bottom-c mt-5">
|
||||
<p class="font-bold">
|
||||
<p class="text-center font-bold">
|
||||
{$t('course.navItem.landing_page.editor.certificate_form.upload_template')}
|
||||
</p>
|
||||
<UploadImage bind:avatar src={templateUrl} shape="rounded-none" bind:isUploading />
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<p class="font-bold">
|
||||
<div class="mt-5 flex flex-col gap-2">
|
||||
<p class="font-semibold">
|
||||
{$t('course.navItem.landing_page.editor.display_section')}
|
||||
</p>
|
||||
|
||||
<Toggle bind:toggled={show} size="sm">
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
|
||||
</Toggle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={show} />
|
||||
<Label class="text-gray-600">
|
||||
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+16
-10
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import get from 'lodash/get';
|
||||
import { Toggle } from 'carbon-components-svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { NAV_ITEM_KEY } from '../../constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
@@ -21,16 +24,19 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<TextEditor bind:value={course.metadata.description} />
|
||||
<TextEditor content={course.metadata.description} onChange={(content) => setter(content, `metadata.description`)} />
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<p class="font-bold">
|
||||
<div class="mt-5 flex flex-col gap-2">
|
||||
<p class="font-semibold">
|
||||
{$t('course.navItem.landing_page.editor.display_section')}
|
||||
</p>
|
||||
|
||||
<Toggle bind:toggled={show} size="sm">
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
|
||||
</Toggle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={show} />
|
||||
|
||||
<Label class="font-bold">
|
||||
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+16
-10
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import get from 'lodash/get';
|
||||
import { Toggle } from 'carbon-components-svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { NAV_ITEM_KEY } from '../../constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
@@ -21,16 +24,19 @@
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
<TextEditor bind:value={course.metadata.goals} />
|
||||
<TextEditor content={course.metadata.goals} onChange={(content) => setter(content, `metadata.goals`)} />
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<p class="font-bold">
|
||||
<div class="mt-5 flex flex-col gap-2">
|
||||
<p class="font-semibold">
|
||||
{$t('course.navItem.landing_page.editor.display_section')}
|
||||
</p>
|
||||
|
||||
<Toggle bind:toggled={show} size="sm">
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
|
||||
</Toggle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={show} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@
|
||||
bind:value={course.metadata.videoUrl}
|
||||
/>
|
||||
<div class="mt-7">
|
||||
<p class="mb-3 font-bold">{$t('course.navItem.landing_page.editor.header_form.replace_cover')}</p>
|
||||
<p class="mb-3">{$t('course.navItem.landing_page.editor.header_form.replace_cover')}</p>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.landing_page.editor.header_form.replace')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
|
||||
+49
-22
@@ -1,12 +1,15 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import { Toggle, Select, SelectItem } from 'carbon-components-svelte';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { isCourseFree } from '$lib/utils/functions/course';
|
||||
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import { isCourseFree } from '$lib/utils/functions/course';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
@@ -20,8 +23,8 @@
|
||||
let showDiscount = $derived(get(course, 'metadata.showDiscount', false));
|
||||
let giftToggled = $derived(get(course, 'metadata.reward.show', false));
|
||||
|
||||
function handleChange(html: string) {
|
||||
setter(html, 'metadata.reward.description');
|
||||
function handleChange(content: string) {
|
||||
setter(content, 'metadata.reward.description');
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -39,10 +42,18 @@
|
||||
</script>
|
||||
|
||||
{#if typeof course !== 'undefined'}
|
||||
<Select labelText={$t('course.navItem.landing_page.editor.pricing_form.currency')} bind:selected={course.currency}>
|
||||
<SelectItem value="NGN" />
|
||||
<SelectItem value="USD" />
|
||||
</Select>
|
||||
<div>
|
||||
<Label class="mb-2 font-bold">{$t('course.navItem.landing_page.editor.pricing_form.currency')}</Label>
|
||||
<Select.Root type="single" value={course.currency} onValueChange={(value) => setter(value, 'currency')}>
|
||||
<Select.Trigger class="w-full">
|
||||
<p>{course.currency === 'NGN' ? 'NGN' : 'USD'}</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="NGN">NGN</Select.Item>
|
||||
<Select.Item value="USD">USD</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
className="mt-5"
|
||||
@@ -63,11 +74,17 @@
|
||||
{/if}
|
||||
|
||||
<div class="mt-5">
|
||||
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
|
||||
<Toggle labelText={$t('course.navItem.landing_page.editor.pricing_form.discount')} bind:toggled={showDiscount}>
|
||||
<span slot="labelA">{$t('course.navItem.landing_page.editor.pricing_form.no')}</span>
|
||||
<span slot="labelB">{$t('course.navItem.landing_page.editor.pricing_form.yes')}</span>
|
||||
</Toggle>
|
||||
<div class="mb-2">
|
||||
<Label class="font-bold">{$t('course.navItem.landing_page.editor.pricing_form.discount')}</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={showDiscount} />
|
||||
<Label class="text-gray-600">
|
||||
{showDiscount
|
||||
? $t('course.navItem.landing_page.editor.pricing_form.yes')
|
||||
: $t('course.navItem.landing_page.editor.pricing_form.no')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showDiscount}
|
||||
@@ -82,20 +99,30 @@
|
||||
{/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">{$t('course.navItem.landing_page.editor.pricing_form.no')}</span>
|
||||
<span slot="labelB">{$t('course.navItem.landing_page.editor.pricing_form.yes')}</span>
|
||||
</Toggle>
|
||||
<div class="mb-2">
|
||||
<Label class="font-bold">Gift on Completion</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={giftToggled} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{giftToggled
|
||||
? $t('course.navItem.landing_page.editor.pricing_form.yes')
|
||||
: $t('course.navItem.landing_page.editor.pricing_form.no')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if giftToggled}
|
||||
<p class="mt-5 font-bold dark:text-white">
|
||||
<p class="mt-5 dark:text-white">
|
||||
{$t('course.navItem.landing_page.editor.pricing_form.gift')}
|
||||
</p>
|
||||
|
||||
<div class="h-2/5">
|
||||
<TextEditor value={get(course, 'metadata.reward.description', '')} onChange={handleChange} />
|
||||
<TextEditor
|
||||
content={get(course, 'metadata.reward.description', '')}
|
||||
onChange={(content) => handleChange(content)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
+16
-10
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import get from 'lodash/get';
|
||||
import { Toggle } from 'carbon-components-svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { NAV_ITEM_KEY } from '../../constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
@@ -21,16 +24,19 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<TextEditor bind:value={course.metadata.requirements} />
|
||||
<TextEditor content={course.metadata.requirements} onChange={(content) => setter(content, `metadata.requirements`)} />
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<p class="font-bold">
|
||||
<div class="mt-5 flex flex-col gap-2">
|
||||
<p class="font-semibold">
|
||||
{$t('course.navItem.landing_page.editor.display_section')}
|
||||
</p>
|
||||
|
||||
<Toggle bind:toggled={show} size="sm">
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
|
||||
</Toggle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={show} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+15
-12
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import SaveIcon from '@lucide/svelte/icons/save';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import { Toggle } from 'carbon-components-svelte';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { uploadAvatar } from '$lib/utils/services/courses';
|
||||
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { uploadAvatar } from '$lib/utils/services/courses';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
|
||||
let { courseId, reviews = $bindable([]), review = $bindable({}), errors = {}, onExpand = () => {} } = $props();
|
||||
|
||||
@@ -53,10 +56,10 @@
|
||||
errorMessage={errors.description}
|
||||
isAIEnabled={true}
|
||||
initAIPrompt="Generate a 20 word review from a student saying good things about me teaching"
|
||||
aiAlignPopover="top-right"
|
||||
aiAlignPopover="end"
|
||||
/>
|
||||
|
||||
<div class="mt-2 flex w-full items-center justify-between">
|
||||
<div class="mt-2 flex w-full items-end justify-between">
|
||||
<!-- Rating -->
|
||||
<TextField
|
||||
className="mt-2 w-20"
|
||||
@@ -73,12 +76,12 @@
|
||||
/>
|
||||
|
||||
<!-- Toggle -->
|
||||
<div class="w-24">
|
||||
<Toggle
|
||||
labelText={$t('course.navItem.landing_page.editor.reviews_form.hide')}
|
||||
size="sm"
|
||||
bind:toggled={review.hide}
|
||||
/>
|
||||
<div class="flex w-24 flex-col items-center gap-2 space-x-2">
|
||||
<Switch bind:checked={review.hide} />
|
||||
|
||||
<Label class="text-sm">
|
||||
{review.hide ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { coursePaymentValidation } from '$lib/utils/functions/validator';
|
||||
import { Link } from 'carbon-components-svelte';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
import { coursePaymentValidation } from '$lib/utils/functions/validator';
|
||||
import { triggerSendEmail, NOTIFICATION_NAME } from '$lib/utils/services/notification/notification';
|
||||
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
paymentLink?: string;
|
||||
@@ -104,7 +103,7 @@
|
||||
{:else if step === STEPS.STEP_2}
|
||||
<p>You will now be taken to a payment page, once you've paid send a proof of payment to the course admin</p>
|
||||
<div class="mt-5 flex flex-row-reverse items-center">
|
||||
<Link href={paymentLink} target="_blank" on:click={onClickPaymentLink}>Go to payment</Link>
|
||||
<a href={paymentLink} target="_blank" onclick={onClickPaymentLink} class="text-sm font-semibold">Go to payment</a>
|
||||
</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import get from 'lodash/get';
|
||||
import pluralize from 'pluralize';
|
||||
import { page } from '$app/state';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import PlayIcon from '@lucide/svelte/icons/play';
|
||||
import { ImageLoader, InlineLoading } from 'carbon-components-svelte';
|
||||
|
||||
import { getLectureNo } from '../Course/function';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { course, sortLesson } from '$lib/components/Course/store';
|
||||
import { NAV_ITEM_KEY, NAV_ITEMS } from './constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { handleOpenWidget, reviewsModalStore } from './store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { COURSE_VERSION, type Course } from '$lib/utils/types';
|
||||
import { course, sortLesson } from '$lib/components/Course/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { getExerciseCount, getLessonSections, getTotalLessons, filterNavItems } from './utils';
|
||||
|
||||
import Chip from '../Chip/index.svelte';
|
||||
import Modal from '../Modal/index.svelte';
|
||||
import ImageRenderer from '../Org/ImageRenderer.svelte';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import PricingSection from './components/PricingSection.svelte';
|
||||
import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte';
|
||||
import SectionsDisplay from './components/SectionsDisplay.svelte';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { observeIntersection } from './components/IntersectionObserver';
|
||||
import SectionsDisplay from './components/SectionsDisplay.svelte';
|
||||
|
||||
import { getExerciseCount, getLessonSections, getTotalLessons, filterNavItems } from './utils';
|
||||
import { NAV_ITEM_KEY, NAV_ITEMS } from './constants';
|
||||
|
||||
interface Props {
|
||||
editMode?: boolean;
|
||||
@@ -116,7 +115,7 @@
|
||||
<div class="flex w-full flex-col-reverse items-center justify-between md:w-5/6 md:flex-row">
|
||||
<!-- Course Description -->
|
||||
<div class="w-11/12 py-10 md:w-2/5">
|
||||
<h1 class="my-4 text-5xl font-bold text-white dark:text-white">
|
||||
<h1 class="my-4 text-5xl text-white dark:text-white">
|
||||
{get(courseData, 'title', '')}
|
||||
</h1>
|
||||
<p class="text-md mb-6 text-white dark:text-white">
|
||||
@@ -218,7 +217,7 @@
|
||||
transition:fade={{ delay: 250, duration: 300 }}
|
||||
class="mt-8 border-b border-gray-300 pb-10"
|
||||
>
|
||||
<h3 class="mb-3 mt-0 text-2xl font-bold">
|
||||
<h3 class="mb-3 mt-0 text-2xl">
|
||||
{$t('course.navItem.landing_page.requirement')}
|
||||
</h3>
|
||||
|
||||
@@ -235,7 +234,7 @@
|
||||
transition:fade={{ delay: 250, duration: 300 }}
|
||||
class="mt-8 border-b border-gray-300 pb-10"
|
||||
>
|
||||
<h3 class="mb-3 mt-0 text-2xl font-bold">
|
||||
<h3 class="mb-3 mt-0 text-2xl">
|
||||
{$t('course.navItem.landing_page.description')}
|
||||
</h3>
|
||||
|
||||
@@ -248,7 +247,7 @@
|
||||
<!-- Sections - Goal -->
|
||||
{#if navItemKeys.includes(NAV_ITEM_KEY.GOALS)}
|
||||
<section id="goals" transition:fade={{ delay: 250, duration: 300 }} class="mt-8 pb-10">
|
||||
<h3 class="mb-3 mt-0 text-2xl font-bold">{$t('course.navItem.landing_page.learn')}</h3>
|
||||
<h3 class="mb-3 mt-0 text-2xl">{$t('course.navItem.landing_page.learn')}</h3>
|
||||
<ul class="list font-light">
|
||||
<HtmlRender>{@html get(courseData, 'metadata.goals', '')}</HtmlRender>
|
||||
</ul>
|
||||
@@ -262,21 +261,16 @@
|
||||
transition:fade={{ delay: 250, duration: 300 }}
|
||||
class="mt-8 border-b border-gray-300 pb-10"
|
||||
>
|
||||
<h3 class="mt-0 text-2xl font-bold">{$t('course.navItem.landing_page.certificate')}</h3>
|
||||
<h3 class="mt-0 text-2xl">{$t('course.navItem.landing_page.certificate')}</h3>
|
||||
<p class="mb-3 text-sm font-light dark:text-white">
|
||||
{$t('course.navItem.landing_page.certificate_text')}
|
||||
</p>
|
||||
|
||||
<ImageLoader
|
||||
class="certificate-img max-h-[215px]"
|
||||
<ImageRenderer
|
||||
src={certificate?.templateUrl}
|
||||
alt="certificate template"
|
||||
>
|
||||
<svelte:fragment slot="loading">
|
||||
<InlineLoading />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="error">An error occurred.</svelte:fragment>
|
||||
</ImageLoader>
|
||||
className="certificate-img max-h-[215px]"
|
||||
/>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -284,7 +278,7 @@
|
||||
{#if courseData.version === COURSE_VERSION.V1}
|
||||
<section id="lessons" class="mt-8 border-b border-gray-300 pb-10">
|
||||
<div class="mb-3 flex w-full items-center justify-between">
|
||||
<h3 class="mb-3 mt-0 text-2xl font-bold">
|
||||
<h3 class="mb-3 mt-0 text-2xl">
|
||||
{$t('course.navItem.landing_page.content')}
|
||||
</h3>
|
||||
<p class="text-sm font-light dark:text-white">
|
||||
@@ -391,7 +385,7 @@
|
||||
<div class="flex">
|
||||
<!-- ratings -->
|
||||
<div class="w-1/3">
|
||||
<h2 class="text-xl font-bold">
|
||||
<h2 class="text-xl">
|
||||
{averageRating}
|
||||
{$t('course.navItem.landing_page.reviews_modal.rating')}
|
||||
</h2>
|
||||
@@ -439,7 +433,7 @@
|
||||
|
||||
<!-- Sections - Instructor -->
|
||||
<section id="instructor" class="mt-8 pb-10">
|
||||
<h3 class="mb-3 mt-0 text-2xl font-bold">
|
||||
<h3 class="mb-3 mt-0 text-2xl">
|
||||
{$t('course.navItem.landing_page.instructor')}
|
||||
</h3>
|
||||
<div class="mb-4 flex items-center">
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div class="root relative mb-4 mr-4 w-72 rounded-md border bg-gray-100 transition ease-in-out dark:bg-black">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 10rem;" />
|
||||
<Skeleton class="h-40 w-full" />
|
||||
<div class="p-3 pb-0">
|
||||
<!-- Title -->
|
||||
<SkeletonText style="width: 50%;" />
|
||||
<Skeleton class="h-4 w-2/4" />
|
||||
|
||||
<!-- Description -->
|
||||
<SkeletonText style="width: 100%; margin-top: 1rem;" />
|
||||
<SkeletonText style="width: 100%; margin-bottom: 1rem;" />
|
||||
<Skeleton class="h-4 w-2/4" />
|
||||
<Skeleton class="h-4 w-2/4" />
|
||||
<Skeleton class="mt-4 h-4 w-full" />
|
||||
<Skeleton class="mb-4 h-4 w-full" />
|
||||
|
||||
<div class="mb-3 flex justify-between">
|
||||
<SkeletonText style="width: 25%" />
|
||||
<SkeletonText style="width: 25%" />
|
||||
<Skeleton class="h-4 w-1/4" />
|
||||
</div>
|
||||
<SkeletonText style="width: 25%" />
|
||||
<Skeleton class="h-4 w-1/4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import CircleDotIcon from '@lucide/svelte/icons/circle-dot';
|
||||
import TrendingUpIcon from '@lucide/svelte/icons/trending-up';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import ImageRenderer from '$lib/components/Org/ImageRenderer.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { calcCourseDiscount } from '$lib/utils/functions/course';
|
||||
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
import { ImageLoader, OverflowMenu, OverflowMenuItem, SkeletonPlaceholder, Tag } from 'carbon-components-svelte';
|
||||
import TrendingUpIcon from '@lucide/svelte/icons/trending-up';
|
||||
import CircleDotIcon from '@lucide/svelte/icons/circle-dot';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
import { copyCourseModal, deleteCourseModal } from '$lib/components/Courses/store';
|
||||
|
||||
interface Props {
|
||||
@@ -129,30 +134,37 @@
|
||||
<div class="p-4">
|
||||
<div class="relative mb-5">
|
||||
{#if !isLMS && !isOnLandingPage}
|
||||
<OverflowMenu
|
||||
class="absolute right-2 top-2 z-40 rounded-full bg-gray-200 opacity-0 transition-all delay-150 duration-200 ease-in-out group-hover:opacity-100 dark:bg-neutral-800"
|
||||
size="sm"
|
||||
on:click={(e) => e.stopPropagation()}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="absolute right-2 top-2 z-40 flex items-center justify-center rounded-full bg-gray-200 p-2 opacity-0 transition-all delay-150 duration-200 ease-in-out group-hover:opacity-100 dark:bg-neutral-800"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.clone')} on:click={handleCloneCourse} />
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.share')} on:click={handleShareCourse} />
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.invite')} on:click={handleInvite} />
|
||||
<OverflowMenuItem danger text={$t('courses.course_card.context_menu.delete')} on:click={handleDeleteCourse} />
|
||||
</OverflowMenu>
|
||||
<EllipsisVerticalIcon size={16} />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={handleCloneCourse}>
|
||||
{$t('courses.course_card.context_menu.clone')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleShareCourse}>
|
||||
{$t('courses.course_card.context_menu.share')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleInvite}>
|
||||
{$t('courses.course_card.context_menu.invite')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item class="text-red-600" onclick={handleDeleteCourse}>
|
||||
{$t('courses.course_card.context_menu.delete')}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
|
||||
<ImageLoader
|
||||
<ImageRenderer
|
||||
src={bannerImage}
|
||||
alt="Course Logo"
|
||||
class="relative h-[200px] w-full rounded dark:border dark:border-neutral-600"
|
||||
>
|
||||
<svelte:fragment slot="loading">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 200px;" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="errror">
|
||||
{$t('courses.course_card.error_message')}
|
||||
</svelte:fragment>
|
||||
</ImageLoader>
|
||||
alt="Banner Image"
|
||||
className="relative h-[200px] w-full rounded dark:border dark:border-neutral-600"
|
||||
/>
|
||||
|
||||
{#if type}
|
||||
{@const tag = COURSE_TAG[type]}
|
||||
<span
|
||||
@@ -203,13 +215,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Tag type={isPublished ? 'green' : 'cool-gray'}>
|
||||
<Badge variant={isPublished ? 'default' : 'secondary'}>
|
||||
{#if isPublished}
|
||||
{$t('courses.course_card.published')}
|
||||
{:else}
|
||||
{$t('courses.course_card.unpublished')}
|
||||
{/if}
|
||||
</Tag>
|
||||
</Badge>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { SkeletonText } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div class="my-2">
|
||||
<SkeletonText heading />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { StructuredListRow, StructuredListCell, Tag, OverflowMenuItem, OverflowMenu } from 'carbon-components-svelte';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import * as Table from '@cio/ui/base/table';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
@@ -49,37 +53,48 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<StructuredListRow label for="row-{id}" on:click={() => goto(`/courses/${id}`)}>
|
||||
<StructuredListCell><p class="font-semibold">{title}</p></StructuredListCell>
|
||||
<StructuredListCell>
|
||||
<p class="line-clamp-2">{description}</p>
|
||||
</StructuredListCell>
|
||||
<Table.Row class="cursor-pointer" onclick={() => goto(`/courses/${id}`)}>
|
||||
<Table.Cell class="truncate"><p class="font-semibold">{title}</p></Table.Cell>
|
||||
<Table.Cell class="truncate">
|
||||
<p>{description}</p>
|
||||
</Table.Cell>
|
||||
{#if !$isMobile}
|
||||
<StructuredListCell>{type}</StructuredListCell>
|
||||
<StructuredListCell>{totalLessons}</StructuredListCell>
|
||||
<StructuredListCell>{totalStudents}</StructuredListCell>
|
||||
<StructuredListCell>
|
||||
<Tag class="break-normal" type={isPublished ? 'green' : 'cool-gray'}>
|
||||
<Table.Cell class="truncate">{type}</Table.Cell>
|
||||
<Table.Cell>{totalLessons}</Table.Cell>
|
||||
<Table.Cell>{totalStudents}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant={isPublished ? 'default' : 'secondary'}>
|
||||
{#if isPublished}
|
||||
{$t('courses.course_card.published')}
|
||||
{:else}
|
||||
{$t('courses.course_card.unpublished')}
|
||||
{/if}
|
||||
</Tag>
|
||||
</StructuredListCell>
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
<StructuredListCell>
|
||||
<OverflowMenu
|
||||
id="course-list"
|
||||
flipped
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
<Table.Cell class="text-center">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="flex items-center justify-center rounded-md p-1 hover:bg-gray-100"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.clone')} on:click={handleCloneCourse} />
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.share')} on:click={handleShareCourse} />
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.invite')} on:click={handleInvite} />
|
||||
<OverflowMenuItem danger text={$t('courses.course_card.context_menu.delete')} on:click={handleDeleteCourse} />
|
||||
</OverflowMenu>
|
||||
</StructuredListCell>
|
||||
</StructuredListRow>
|
||||
<EllipsisVerticalIcon size={16} />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={handleCloneCourse}>
|
||||
{$t('courses.course_card.context_menu.clone')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleShareCourse}>
|
||||
{$t('courses.course_card.context_menu.share')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleInvite}>
|
||||
{$t('courses.course_card.context_menu.invite')}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item class="text-red-600" onclick={handleDeleteCourse}>
|
||||
{$t('courses.course_card.context_menu.delete')}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
@@ -30,16 +30,14 @@
|
||||
|
||||
const options = [
|
||||
{
|
||||
title: 'Live Class',
|
||||
subtitle:
|
||||
'This course type is ideal for bootcamps where lessons are time based and you need attendance and grading',
|
||||
title: $t('new_course_modal.live_class_label'),
|
||||
subtitle: $t('new_course_modal.live_class_subtitle'),
|
||||
type: COURSE_TYPE.LIVE_CLASS,
|
||||
isDisabled: false
|
||||
},
|
||||
{
|
||||
title: 'Self Paced',
|
||||
subtitle:
|
||||
'This course type is ideal for courses where students can take lessons on their own pace without a teacher',
|
||||
title: $t('new_course_modal.self_paced_label'),
|
||||
subtitle: $t('new_course_modal.self_paced_subtitle'),
|
||||
type: COURSE_TYPE.SELF_PACED,
|
||||
isDisabled: false
|
||||
}
|
||||
@@ -153,10 +151,10 @@
|
||||
{$t('courses.new_course_modal.type_selector_title')}
|
||||
</h2>
|
||||
|
||||
<div class="my-8 flex flex-col items-center justify-evenly gap-4 md:flex-row">
|
||||
<div class="my-8 flex flex-col items-center justify-evenly gap-4 md:flex-row!">
|
||||
{#each options as option}
|
||||
<button
|
||||
class="w-11/12 rounded-md border-2 p-5 md:h-[240px] md:w-[261px] dark:bg-neutral-700 {option.type === type
|
||||
class="w-11/12 rounded-md border-2 p-5 md:h-60 md:w-[261px] dark:bg-neutral-700 {option.type === type
|
||||
? 'border-primary-400'
|
||||
: `border-gray-200 dark:border-neutral-600 ${
|
||||
!option.isDisabled && 'hover:scale-95'
|
||||
@@ -169,7 +167,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="flex items-center text-start font-bold">
|
||||
<p class="flex items-center text-start">
|
||||
<span class="mr-2 text-sm">{option.title}</span>
|
||||
{#if option.isDisabled}
|
||||
<ComingSoon />
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
<script lang="ts">
|
||||
import * as Table from '@cio/ui/base/table';
|
||||
|
||||
import Box from '../Box/index.svelte';
|
||||
import Card from './components/Card/index.svelte';
|
||||
import List from './components/List/index.svelte';
|
||||
import CardLoader from './components/Card/Loader.svelte';
|
||||
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte';
|
||||
import DeleteModal from '$lib/components/Modal/DeleteModal.svelte';
|
||||
import CopyCourseModal from './components/CopyCourseModal/index.svelte';
|
||||
|
||||
import { courseMetaDeta } from './store';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import {
|
||||
StructuredList,
|
||||
StructuredListHead,
|
||||
StructuredListRow,
|
||||
StructuredListCell,
|
||||
StructuredListBody
|
||||
} from 'carbon-components-svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import CopyCourseModal from './components/CopyCourseModal/index.svelte';
|
||||
import DeleteModal from '$lib/components/Modal/DeleteModal.svelte';
|
||||
import { deleteCourse } from '$lib/utils/services/courses';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { deleteCourse } from '$lib/utils/services/courses';
|
||||
import { deleteCourseModal, deleteCourseModalInitialState, courses as coursesStore } from './store';
|
||||
|
||||
interface Props {
|
||||
@@ -84,31 +80,20 @@
|
||||
<CardLoader />
|
||||
</section>
|
||||
{:else if $courseMetaDeta.view === 'list'}
|
||||
<StructuredList selection class="w-full">
|
||||
<StructuredListHead>
|
||||
<StructuredListRow head>
|
||||
<StructuredListCell head>
|
||||
{$t('courses.course_card.list_view.title')}
|
||||
</StructuredListCell>
|
||||
<StructuredListCell head>
|
||||
{$t('courses.course_card.list_view.description')}
|
||||
</StructuredListCell>
|
||||
<StructuredListCell head>
|
||||
{$t('courses.course_card.list_view.type')}
|
||||
</StructuredListCell>
|
||||
<StructuredListCell head>
|
||||
{$t('courses.course_card.list_view.lessons')}
|
||||
</StructuredListCell>
|
||||
<StructuredListCell head>
|
||||
{$t('courses.course_card.list_view.students')}
|
||||
</StructuredListCell>
|
||||
<StructuredListCell head>
|
||||
{$t('courses.course_card.list_view.published')}
|
||||
</StructuredListCell>
|
||||
<StructuredListCell head></StructuredListCell>
|
||||
</StructuredListRow>
|
||||
</StructuredListHead>
|
||||
<StructuredListBody>
|
||||
<div class="w-full overflow-hidden rounded-md border">
|
||||
<Table.Root class="w-full table-fixed">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[20%]">{$t('courses.course_card.list_view.title')}</Table.Head>
|
||||
<Table.Head class="w-[30%]">{$t('courses.course_card.list_view.description')}</Table.Head>
|
||||
<Table.Head class="w-[10%]">{$t('courses.course_card.list_view.type')}</Table.Head>
|
||||
<Table.Head class="w-[10%]">{$t('courses.course_card.list_view.lessons')}</Table.Head>
|
||||
<Table.Head class="w-[10%]">{$t('courses.course_card.list_view.students')}</Table.Head>
|
||||
<Table.Head class="w-[12%]">{$t('courses.course_card.list_view.published')}</Table.Head>
|
||||
<Table.Head class="w-[8%]"></Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each courses as courseData}
|
||||
<List
|
||||
id={courseData.id}
|
||||
@@ -120,8 +105,9 @@
|
||||
totalStudents={courseData.total_students}
|
||||
/>
|
||||
{/each}
|
||||
</StructuredListBody>
|
||||
</StructuredList>
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
{:else}
|
||||
<section class={`relative ${$courseMetaDeta.isLoading || courses ? 'cards-container' : ''} `}>
|
||||
{#each courses as courseData}
|
||||
@@ -148,7 +134,7 @@
|
||||
</div>
|
||||
{#if !$courseMetaDeta.isLoading && !courses.length}
|
||||
<Box className="w-full">
|
||||
<CoursesEmptyIcon size={16} />
|
||||
<CoursesEmptyIcon />
|
||||
<h3 class="my-5 text-2xl dark:text-white">{emptyTitle}</h3>
|
||||
<p class="w-1/3 text-center dark:text-white">
|
||||
{emptyDescription}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Editor } from '@cio/ui/custom/editor';
|
||||
import defaultContent from './default-content';
|
||||
</script>
|
||||
|
||||
<Editor content={defaultContent} />
|
||||
@@ -1,825 +0,0 @@
|
||||
export default {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: {
|
||||
textAlign: 'center',
|
||||
level: 1
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Introducing ClassroomIO'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: 'justify',
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'The AI native LMS. We have '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'bold'
|
||||
}
|
||||
],
|
||||
text: 'bold'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ', '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'italic'
|
||||
}
|
||||
],
|
||||
text: 'italic'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ', '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'strike'
|
||||
}
|
||||
],
|
||||
text: 'strike'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ', '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'underline'
|
||||
}
|
||||
],
|
||||
text: 'underline'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ', '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'link',
|
||||
attrs: {
|
||||
href: 'https://github.com',
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
class: null
|
||||
}
|
||||
}
|
||||
],
|
||||
text: 'link'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ', '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'code'
|
||||
}
|
||||
],
|
||||
text: 'code'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ', Superscript A'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'superscript'
|
||||
}
|
||||
],
|
||||
text: 'b'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ', subscript A'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'subscript'
|
||||
}
|
||||
],
|
||||
text: 'b'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ', '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'highlight',
|
||||
attrs: {
|
||||
color: '#800080'
|
||||
}
|
||||
}
|
||||
],
|
||||
text: 'highlight1'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'highlight',
|
||||
attrs: {
|
||||
color: '#1a1600'
|
||||
}
|
||||
}
|
||||
],
|
||||
text: ','
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ' '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'highlight',
|
||||
attrs: {
|
||||
color: '#0000FF'
|
||||
}
|
||||
}
|
||||
],
|
||||
text: 'highlight2'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ', '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'textStyle',
|
||||
attrs: {
|
||||
color: '#008000',
|
||||
fontSize: ''
|
||||
}
|
||||
}
|
||||
],
|
||||
text: 'textcolor1'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'textStyle',
|
||||
attrs: {
|
||||
color: 'rgb(78, 211, 21)',
|
||||
fontSize: ''
|
||||
}
|
||||
}
|
||||
],
|
||||
text: ', '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'textStyle',
|
||||
attrs: {
|
||||
color: '#FFA500',
|
||||
fontSize: ''
|
||||
}
|
||||
}
|
||||
],
|
||||
text: 'textcolor2'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'blockquote',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: 'justify',
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'This is a block Quote'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'codeBlock',
|
||||
attrs: {
|
||||
language: 'javascript'
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'console.log("Hello World")'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'horizontalRule'
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: {
|
||||
textAlign: 'left',
|
||||
level: 2
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Support For Media Placeholders and Medias'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'image-placeholder'
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: 'https://placehold.co/800x400/6A00F5/white',
|
||||
alt: null,
|
||||
title: 'A Sample Image',
|
||||
width: 658,
|
||||
height: null,
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'video-placeholder'
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
attrs: {
|
||||
src: 'https://videos.pexels.com/video-files/2491284/2491284-uhd_2732_1440_24fps.mp4',
|
||||
alt: null,
|
||||
title: 'A Sample Video',
|
||||
width: 673,
|
||||
height: null,
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'audio-placeholder'
|
||||
},
|
||||
{
|
||||
type: 'audio',
|
||||
attrs: {
|
||||
src: 'https://cdn.pixabay.com/audio/2024/10/30/audio_42e6870f29.mp3',
|
||||
alt: null,
|
||||
title: 'A Sample Audio',
|
||||
width: 670,
|
||||
height: null,
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'horizontalRule'
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
level: 2
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'IFrames'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'iframe-placeholder'
|
||||
},
|
||||
{
|
||||
type: 'iframe',
|
||||
attrs: {
|
||||
src: 'https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3748.5645078258503!2d75.17451197505285!3d20.026784381383496!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3bdb93bd138ae4bd%3A0x574c6482cf0b89cf!2sEllora%20Caves!5e0!3m2!1sen!2sin!4v1742466514831!5m2!1sen!2sin',
|
||||
alt: null,
|
||||
title: 'Ellora Caves, Maharastra, India',
|
||||
width: 621,
|
||||
height: null,
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'iframe',
|
||||
attrs: {
|
||||
src: 'https://www.youtube.com/embed/jfKfPfyJRdk?si=SYBE0PkI5v79-54W',
|
||||
alt: null,
|
||||
title: 'Lofi Hip Hop Youtube Streaming Video',
|
||||
width: 623,
|
||||
height: null,
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'horizontalRule'
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
level: 2
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Typography'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Smilipicker, type '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'code'
|
||||
}
|
||||
],
|
||||
text: ':)'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ' and it becomes 🙂 or 😉. Typography support e.g. type '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'code'
|
||||
}
|
||||
],
|
||||
text: '!='
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ' and it becomes ≠. Similarly (c) becomes ©, -> becomes → and many more like 1×2, ½. We also have color visualizer. #FFF, #000, #FF00FF can be visualized.'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'horizontalRule'
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
level: 2
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Tables'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'table',
|
||||
content: [
|
||||
{
|
||||
type: 'tableRow',
|
||||
content: [
|
||||
{
|
||||
type: 'tableCell',
|
||||
attrs: {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
colwidth: [323],
|
||||
style: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello World'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'tableCell',
|
||||
attrs: {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
colwidth: [328],
|
||||
style: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello World'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'tableCell',
|
||||
attrs: {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
colwidth: [330],
|
||||
style: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello World'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'tableRow',
|
||||
content: [
|
||||
{
|
||||
type: 'tableCell',
|
||||
attrs: {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
colwidth: [323],
|
||||
style: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello World'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'tableCell',
|
||||
attrs: {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
colwidth: [328],
|
||||
style: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello World'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'tableCell',
|
||||
attrs: {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
colwidth: [330],
|
||||
style: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello World'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
level: 3
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'textStyle',
|
||||
attrs: {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontSize: '1.5em'
|
||||
}
|
||||
}
|
||||
],
|
||||
text: 'Rending Math and '
|
||||
},
|
||||
{
|
||||
type: 'inlineMath',
|
||||
attrs: {
|
||||
latex: '\\LaTeX',
|
||||
evaluate: 'no',
|
||||
display: 'no'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'The editor supports the '
|
||||
},
|
||||
{
|
||||
type: 'inlineMath',
|
||||
attrs: {
|
||||
latex: '\\LaTeX',
|
||||
evaluate: 'no',
|
||||
display: 'no'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ' rendering. e.g.'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'orderedList',
|
||||
attrs: {
|
||||
start: 1
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'listItem',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Functions like, '
|
||||
},
|
||||
{
|
||||
type: 'inlineMath',
|
||||
attrs: {
|
||||
latex: 'sin²\\theta + cos²\\theta=1, a²+b²=c²',
|
||||
evaluate: 'no',
|
||||
display: 'no'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'listItem',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Matrix like unit matrix'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'inlineMath',
|
||||
attrs: {
|
||||
latex: '\\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix}',
|
||||
evaluate: 'no',
|
||||
display: 'no'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'or more matrices like'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'inlineMath',
|
||||
attrs: {
|
||||
latex:
|
||||
'\\begin{pmatrix} 1 & 0 & \\cdots & 0 \\\\ 0 & 1 & \\cdots & 0 \\\\ \\vdots & \\vdots & \\ddots & \\vdots \\\\ 0 & 0 & \\cdots & 1 \\end{pmatrix}',
|
||||
evaluate: 'no',
|
||||
display: 'no'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'listItem',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Integrals and differential'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'inlineMath',
|
||||
attrs: {
|
||||
latex:
|
||||
'\\int_{0}^{\\frac{\\pi}{2}} \\sin(x) \\,dx = \\left[-\\cos(x)\\right]_0^{\\frac{\\pi}{2}} = -\\cos\\left(\\frac{\\pi}{2}\\right) + \\cos(0)',
|
||||
evaluate: 'no',
|
||||
display: 'no'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'inlineMath',
|
||||
attrs: {
|
||||
latex: '\\frac{\\mathrm{d}}{\\mathrm{d}x}(\\sin x) = \\cos x',
|
||||
evaluate: 'no',
|
||||
display: 'no'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
level: 3
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Explore More…'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: null,
|
||||
class: null
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -39,7 +39,7 @@
|
||||
tabindex="0"
|
||||
onclick={handleClick}
|
||||
>
|
||||
<p class="flex items-center font-bold dark:text-white {titleClass}">
|
||||
<p class="flex items-center dark:text-white {titleClass}">
|
||||
{title}
|
||||
{#if supportsLink}
|
||||
<a class="ml-2" href="{page.url.pathname}#{id}" onclick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
{/if}
|
||||
<select
|
||||
bind:value
|
||||
class="form-select mt-1 block w-auto dark:bg-neutral-700 dark:text-white {selectClassName}"
|
||||
class="form-select mt-1 block w-auto rounded-md border-2 px-2 py-1.5 dark:bg-neutral-700 dark:text-white {selectClassName}"
|
||||
required={isRequired}
|
||||
onblur={() => {}}
|
||||
onchange={onChange}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
// import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte';
|
||||
import type { PopoverProps } from 'carbon-components-svelte/types/Popover/Popover.svelte';
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
@@ -20,7 +20,7 @@
|
||||
ref?: HTMLTextAreaElement | null;
|
||||
isAIEnabled?: boolean;
|
||||
initAIPrompt?: string;
|
||||
aiAlignPopover?: PopoverProps['align']; // $: minHeight = `${1 + parseInt(rows, 10) * 1.2}em`;
|
||||
aiAlignPopover?: PopoverPrimitive.ContentProps['align'];
|
||||
iconbutton?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
bind:value
|
||||
bind:this={ref}
|
||||
required={isRequired}
|
||||
class="form-textarea focus:border-primary-600 mt-1 block w-full rounded-t-md border-b-2 border-l-0 border-r-0 border-t-0 border-gray-200 focus:border-b-2 focus:border-l-0 focus:border-r-0 focus:border-t-0 dark:border-neutral-600
|
||||
class="form-textarea focus:border-primary-600 mt-1 block w-full rounded-t-md border-b-2 border-l-0 border-r-0 border-t-0 border-gray-200 p-3 focus:border-b-2 focus:border-l-0 focus:border-r-0 focus:border-t-0 dark:border-neutral-600
|
||||
{bgColor} {errorMessage ? 'border-red-500' : 'border-gray-300'}"
|
||||
onchange={onChange}
|
||||
></textarea>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import BadgeHelpIcon from '@lucide/svelte/icons/badge-help';
|
||||
import ListTodoIcon from '@lucide/svelte/icons/list-todo';
|
||||
import MapPlusIcon from '@lucide/svelte/icons/map-plus';
|
||||
import LayoutDashboardIcon from '@lucide/svelte/icons/layout-dashboard';
|
||||
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
||||
import type { Component } from 'svelte';
|
||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import MapPlusIcon from '@lucide/svelte/icons/map-plus';
|
||||
import ListTodoIcon from '@lucide/svelte/icons/list-todo';
|
||||
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
||||
import LayoutDashboardIcon from '@lucide/svelte/icons/layout-dashboard';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import { profileMenu, sideBar } from '$lib/components/Org/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import ProfileMenu from '$lib/components/Org/ProfileMenu/index.svelte';
|
||||
import { sideBar } from '$lib/components/Org/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import NavUser from '../Sidebar/components/nav-user.svelte';
|
||||
|
||||
interface SideLinks {
|
||||
name: string;
|
||||
@@ -23,6 +22,12 @@
|
||||
show?: () => boolean;
|
||||
}
|
||||
|
||||
const user = $derived.by(() => ({
|
||||
name: $profile.fullname,
|
||||
email: $profile.email,
|
||||
avatar: $profile.avatar_url
|
||||
}));
|
||||
|
||||
function isActive(pagePath: string, itemPath: string) {
|
||||
const pageLinkItems = pagePath.split('/');
|
||||
const itemLinkItems = itemPath.split('/');
|
||||
@@ -75,73 +80,44 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div bind:this={$profileMenu.ref} class="static md:relative">
|
||||
<aside
|
||||
class={`${
|
||||
$sideBar.hidden
|
||||
? 'absolute top-[48px] z-40 -translate-x-[100%] md:relative md:top-0 md:translate-x-0'
|
||||
: 'absolute top-[48px] z-40 translate-x-0 md:relative md:top-0'
|
||||
} h-[calc(100vh-48px)] w-[250px] min-w-[250px] overflow-y-auto bg-gray-100 transition dark:bg-neutral-900`}
|
||||
<Sidebar.Provider class="w-fit">
|
||||
<Sidebar.Root collapsible="icon" class="inset-y-12 h-[calc(100vh-48px)] {$sideBar.hidden ? 'hidden' : ''}">
|
||||
<Sidebar.Content>
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel>LMS Navigation</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each sideLinks as item (item.link)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton tooltipContent={item.name}>
|
||||
<a
|
||||
href={item.link}
|
||||
onclick={toggleSidebar}
|
||||
class="flex w-full items-center gap-4 {isActive(page.url.pathname, item.link)
|
||||
? 'bg-accent text-accent-foreground rounded-md px-3 py-2'
|
||||
: ''}"
|
||||
>
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="border-b border-gray-200 px-4 pt-5 dark:border-neutral-600">
|
||||
<div class="flex w-full flex-col items-center">
|
||||
<Avatar src={$profile.avatar_url} name={$profile.fullname} shape="rounded-full" width="w-20" height="h-20" />
|
||||
|
||||
<div class="mt-5 flex w-full justify-center">
|
||||
<p class="max-w-[80%] truncate whitespace-nowrap text-center text-lg font-bold dark:text-white">
|
||||
{$profile.fullname}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="my-5">
|
||||
{#each sideLinks as item}
|
||||
<a href={item.link} class="text-black" onclick={toggleSidebar}>
|
||||
<li
|
||||
class="mb-2 flex items-center px-4 py-3 {NavClasses.item} {isActive(page.url.pathname, `${item.link}`)
|
||||
? NavClasses.active
|
||||
: 'dark:text-white'}"
|
||||
>
|
||||
<item.icon />
|
||||
<p class="ml-2 dark:text-white">{item.name}</p>
|
||||
</li>
|
||||
<item.icon size={16} />
|
||||
{item.name}
|
||||
</a>
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<span class="flex-grow"></span>
|
||||
<ul class="my-5 px-4 pb-5">
|
||||
<a href="/lms" class="text-black" onclick={toggleSidebar}>
|
||||
<li class="mb-2 flex items-center rounded px-4 py-3">
|
||||
<BadgeHelpIcon size={16} />
|
||||
<p class="ml-2 dark:text-white">{$t('lms_navigation.help')}</p>
|
||||
</li>
|
||||
</a>
|
||||
<button
|
||||
class="w-full"
|
||||
onclick={() => {
|
||||
$profileMenu.open = !$profileMenu.open;
|
||||
$sideBar.hidden = true;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="mb-2 flex w-full cursor-pointer items-center justify-between px-2.5 py-1.5 text-black no-underline {NavClasses.item}"
|
||||
>
|
||||
<div class="flex items-center justify-start space-x-1 text-start">
|
||||
<Avatar src={$profile.avatar_url} name={$profile.username} width="w-[1.2rem]" height="h-[1.2rem]" />
|
||||
<p class="max-w-full truncate text-sm font-medium dark:text-white">
|
||||
{$profile.fullname}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<ChevronRightIcon size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
</Sidebar.Content>
|
||||
|
||||
<ProfileMenu />
|
||||
</div>
|
||||
{#if user}
|
||||
<Sidebar.Footer>
|
||||
<NavUser {user} />
|
||||
</Sidebar.Footer>
|
||||
{/if}
|
||||
|
||||
<Sidebar.Rail />
|
||||
</Sidebar.Root>
|
||||
|
||||
<!-- TODO: FIGURE OUT WHY THIS APPEARANCE IS BUGGY -->
|
||||
<!-- TODO: THE SIDEBAR COLLAPSE VIEW IS BAD -->
|
||||
<!-- <Sidebar.Inset>
|
||||
<Sidebar.Trigger class="-ml-1" />
|
||||
</Sidebar.Inset> -->
|
||||
</Sidebar.Provider>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script>
|
||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import Row from '$lib/components/Org/Settings/Layout/Row.svelte';
|
||||
import Grid from '$lib/components/Org/Settings/Layout/Grid.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import Column from '$lib/components/Org/Settings/Layout/Column.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import LanguagePicker from '$lib/components/Org/Settings/LanguagePicker.svelte';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import generateUUID from '$lib/utils/functions/generateUUID';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import LanguagePicker from '$lib/components/Org/Settings/LanguagePicker.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { handleLocaleChange } from '$lib/utils/functions/translations';
|
||||
|
||||
let avatar = $state('');
|
||||
|
||||
@@ -80,9 +80,9 @@
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
}
|
||||
.body {
|
||||
/* .body {
|
||||
max-height: 60vh;
|
||||
}
|
||||
} */
|
||||
|
||||
.small {
|
||||
max-width: 388px;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<nav class="{navClass} bg-primary-700 flex h-[48px] w-full p-1 transition delay-150 duration-300 ease-in-out md:px-6">
|
||||
<nav class="{navClass} bg-primary-700 flex h-12 w-full p-1 transition delay-150 duration-300 ease-in-out md:px-6">
|
||||
<ul class="flex w-full items-center gap-2">
|
||||
<div class="flex items-center text-white">
|
||||
<li class="md:hidden">
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
className="mr-2"
|
||||
/>
|
||||
{:else}
|
||||
<TextChip value={shortenName($currentOrg.name)} className="bg-primary-200 font-bold mr-2 dark:text-black" />
|
||||
<TextChip value={shortenName($currentOrg.name)} className="bg-primary-200 mr-2 dark:text-black" />
|
||||
{/if}
|
||||
<span class="line-clamp-1">
|
||||
{$currentOrg.name}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import * as Table from '@cio/ui/base/table';
|
||||
import { page as pageStore } from '$app/state';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
import * as Pagination from '@cio/ui/base/pagination';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { currentOrg, orgAudience } from '$lib/utils/store/org';
|
||||
import {
|
||||
DataTable,
|
||||
DataTableSkeleton,
|
||||
Link,
|
||||
Pagination,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarSearch
|
||||
} from 'carbon-components-svelte';
|
||||
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
@@ -25,34 +22,107 @@
|
||||
{ key: 'date_joined', value: $t('audience.date_joined') }
|
||||
];
|
||||
let pageSize = $state(5);
|
||||
let page = $state(1);
|
||||
let filteredRowIds = $state([]);
|
||||
let searchValue = '';
|
||||
let currentPage = $state(1);
|
||||
let searchValue = $state('');
|
||||
|
||||
const filteredRows = $derived(
|
||||
searchValue
|
||||
? $orgAudience.filter((row) =>
|
||||
Object.values(row).some((val) => String(val).toLowerCase().includes(searchValue.toLowerCase()))
|
||||
)
|
||||
: $orgAudience
|
||||
);
|
||||
|
||||
const totalPages = $derived(Math.ceil(filteredRows.length / pageSize));
|
||||
const startIndex = $derived((currentPage - 1) * pageSize);
|
||||
const endIndex = $derived(startIndex + pageSize);
|
||||
const paginatedRows = $derived(filteredRows.slice(startIndex, endIndex));
|
||||
|
||||
$effect(() => {
|
||||
if (filteredRows.length > 0 && currentPage > totalPages) {
|
||||
currentPage = 1;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full space-y-4">
|
||||
<!-- Search Toolbar -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative flex-1">
|
||||
<Search class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input type="text" placeholder="Search..." bind:value={searchValue} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
{#if isLoading}
|
||||
<DataTableSkeleton {headers} rows={pageSize} />
|
||||
<div class="space-y-2">
|
||||
{#each Array(pageSize) as _}
|
||||
<Skeleton class="h-12 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable size="tall" {headers} rows={$orgAudience} {pageSize} {page}>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarSearch persistent value={searchValue} shouldFilterRows bind:filteredRowIds />
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<svelte:fragment slot="cell-header" let:header>
|
||||
{header.value}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="cell" let:row let:cell>
|
||||
{#if cell.key === 'name'}
|
||||
<Link class="flex items-center gap-2" href={`${pageStore.url.href}/${row.id}/${$currentOrg.id}`}>
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{#each headers as header}
|
||||
<Table.Head>{header.value}</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each paginatedRows as row}
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
<a
|
||||
href={`${pageStore.url.href}/${row.id}/${$currentOrg.id}`}
|
||||
class="text-primary-700 flex items-center gap-2 hover:underline"
|
||||
>
|
||||
<Avatar src={row.avatar_url} width="w-5" height="h-5" />
|
||||
{cell.value}
|
||||
</Link>
|
||||
{:else}
|
||||
{cell.value}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</DataTable>
|
||||
{row.name}
|
||||
</a>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{row.email}</Table.Cell>
|
||||
<Table.Cell>{row.date_joined}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Pagination bind:pageSize bind:page totalItems={filteredRowIds.length} pageSizeInputDisabled />
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-between">
|
||||
<Pagination.Root
|
||||
count={filteredRows.length}
|
||||
perPage={pageSize}
|
||||
page={currentPage}
|
||||
onPageChange={(page) => (currentPage = page)}
|
||||
>
|
||||
{#snippet children({ pages, currentPage: activePage })}
|
||||
<Pagination.Content>
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton />
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === 'ellipsis'}
|
||||
<Pagination.Item>
|
||||
<Pagination.Ellipsis />
|
||||
</Pagination.Item>
|
||||
{:else}
|
||||
<Pagination.Item>
|
||||
<Pagination.Link {page} isActive={activePage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton />
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
{/snippet}
|
||||
</Pagination.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative mx-4 my-2 flex h-[70px] w-full rounded-md border border-gray-100 px-3 pt-3 transition ease-in-out dark:border-neutral-600 dark:bg-neutral-800"
|
||||
>
|
||||
<div class="mr-2 flex w-10 flex-col justify-evenly rounded-md px-1">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 40%; border-radius: 4px;" />
|
||||
<SkeletonPlaceholder style="width: 100%; height: 40%; border-radius: 4px;" />
|
||||
<Skeleton class="h-[40%] w-full rounded-sm" />
|
||||
<Skeleton class="h-[40%] w-full rounded-sm" />
|
||||
<Skeleton class="h-[40%] w-full rounded-sm" />
|
||||
</div>
|
||||
|
||||
<div class="mt-1.5 flex w-9/12 flex-col justify-evenly rounded-md">
|
||||
<!-- first text -->
|
||||
<div class="h-full w-3/6">
|
||||
<SkeletonText style="height: 50%; border-radius: 4px;" />
|
||||
<Skeleton class="h-[50%] w-full rounded-sm" />
|
||||
</div>
|
||||
<!-- second text -->
|
||||
<SkeletonText style="width: 10%; height: 50%; border-radius: 4px;" />
|
||||
<Skeleton class="h-4 w-1/4 rounded-sm" />
|
||||
</div>
|
||||
|
||||
<div class="ml-auto mt-1.5 w-10 flex-col justify-evenly rounded-md px-1">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 41%; border-radius: 4px;" />
|
||||
<Skeleton class="h-[41%] w-full rounded-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
|
||||
import Vote from '$lib/components/Vote/index.svelte';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { Dropdown, Search } from 'carbon-components-svelte';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import MessageCirclePlusIcon from '@lucide/svelte/icons/message-circle-plus';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
|
||||
import CommunityLoader from './Loader.svelte';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import Vote from '$lib/components/Vote/index.svelte';
|
||||
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
|
||||
|
||||
interface Props {
|
||||
isLMS?: boolean;
|
||||
@@ -102,23 +105,31 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<Search
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="relative flex-1">
|
||||
<Search class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={$t('community.find_question')}
|
||||
bind:value={searchValue}
|
||||
searchClass="mr-2"
|
||||
class="bg-gray-100 dark:bg-neutral-800"
|
||||
/>
|
||||
<Dropdown
|
||||
class="h-full w-[25%]"
|
||||
size="xl"
|
||||
label={$t('community.ask.select_course')}
|
||||
items={[
|
||||
{ id: '', text: $t('community.all') },
|
||||
...allCourses.map((course) => ({ id: course.id, text: course.title }))
|
||||
]}
|
||||
bind:selectedId
|
||||
/>
|
||||
</div>
|
||||
<div class="w-[25%]">
|
||||
<Select.Root type="single" bind:value={selectedId}>
|
||||
<Select.Trigger class="w-full bg-gray-100 dark:bg-neutral-800">
|
||||
<p class="truncate">
|
||||
{selectedId ? allCourses.find((course) => course.id === selectedId)?.title : $t('community.all')}
|
||||
</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="">{$t('community.all')}</Select.Item>
|
||||
{#each allCourses as course}
|
||||
<Select.Item value={course.id}>{course.title}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="border-c m-auto my-4 flex flex-wrap items-center justify-center rounded bg-gray-100 lg:justify-start dark:bg-neutral-800"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
|
||||
import NewsSection from './components/NewsSection.svelte';
|
||||
import LogoutButton from './components/LogoutButton.svelte';
|
||||
</script>
|
||||
|
||||
<div class="cursor-pointer space-y-2">
|
||||
{#if !$globalStore.isOrgSite}
|
||||
<NewsSection />
|
||||
{/if}
|
||||
|
||||
<LogoutButton />
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import posthog from 'posthog-js';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import LogOutIcon from '@lucide/svelte/icons/log-out';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { currentOrg, orgs, defaultCurrentOrgState } from '$lib/utils/store/org';
|
||||
import { user, profile, defaultProfileState, defaultUserState } from '$lib/utils/store/user';
|
||||
|
||||
async function logout() {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
console.error('Error logging out: ', error);
|
||||
}
|
||||
|
||||
currentOrg.set(defaultCurrentOrgState);
|
||||
orgs.set([]);
|
||||
user.set(defaultUserState);
|
||||
profile.set(defaultProfileState);
|
||||
|
||||
capturePosthogEvent('user_logged_out');
|
||||
posthog.reset();
|
||||
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item>
|
||||
<button onclick={logout} class="w-full cursor-pointer">
|
||||
<span class="flex items-center gap-2">
|
||||
<LogOutIcon />
|
||||
<p class="text-sm">{$t('settings.profile.logout')}</p>
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import RocketIcon from '@lucide/svelte/icons/rocket';
|
||||
import BellPlusIcon from '@lucide/svelte/icons/bell-plus';
|
||||
import BadgeHelpIcon from '@lucide/svelte/icons/badge-help';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<DropdownMenu.Item class="m-0">
|
||||
<a href="https://classroomio.com/roadmap" target="_blank" class="flex w-full items-center gap-2">
|
||||
<BellPlusIcon size={16} />
|
||||
<p class="text-sm">{$t('profileMenu.whats_new')}</p>
|
||||
</a>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item class="m-0">
|
||||
<a href="https://classroomio.com/blog/launch-week" target="_blank" class="flex w-full items-center gap-2">
|
||||
<RocketIcon size={16} />
|
||||
<p class="text-sm">{$t('profileMenu.launch_week')}</p>
|
||||
</a>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item class="m-0">
|
||||
<a href={$currentOrgPath} class="flex h-full w-full items-center justify-start space-x-2 text-start">
|
||||
<BadgeHelpIcon size={20} />
|
||||
<p class="text-sm dark:text-white">{$t('org_navigation.help')}</p>
|
||||
</a>
|
||||
</DropdownMenu.Item>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let { src, className, alt }: { src: string | undefined; className?: string; alt?: string } = $props();
|
||||
|
||||
function loadImage(src: string | undefined): Promise<void> {
|
||||
if (!src) return Promise.reject();
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve();
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await loadImage(src)}
|
||||
<div class="flex max-h-[215px] w-full items-center justify-center rounded bg-gray-100 dark:bg-neutral-800">
|
||||
<Skeleton class={className} />
|
||||
</div>
|
||||
{:then}
|
||||
<img {src} {alt} class={className} />
|
||||
{:catch}
|
||||
<div class="flex h-[200px] w-full items-center justify-center rounded bg-gray-100 dark:bg-neutral-800">
|
||||
<p class="text-sm text-red-600">{$t('courses.course_card.error_message')}</p>
|
||||
</div>
|
||||
{/await}
|
||||
@@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import { Search } from 'carbon-components-svelte';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
|
||||
let searchValue = $state('');
|
||||
</script>
|
||||
@@ -9,14 +11,17 @@
|
||||
<div class="flex w-full flex-col-reverse items-center justify-center md:flex-row">
|
||||
<!-- Course Description -->
|
||||
<div class="course-descr w-11/12 py-10">
|
||||
<h1 class="my-4 text-center text-5xl font-bold text-white dark:text-white">
|
||||
<h1 class="my-4 text-center text-5xl text-white dark:text-white">
|
||||
{$t('course.navItem.landing_page.explore')}
|
||||
</h1>
|
||||
<p class="text-md mb-6 text-center text-white dark:text-white">
|
||||
{$t('course.navItem.landing_page.find')}🌎.
|
||||
</p>
|
||||
|
||||
<Search placeholder={$t('course.navItem.landing_page.find_course')} bind:value={searchValue} />
|
||||
<div class="relative mx-auto max-w-xl">
|
||||
<Input type="text" placeholder={$t('course.navItem.landing_page.find_course')} bind:value={searchValue} />
|
||||
<Search class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script>
|
||||
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte';
|
||||
</script>
|
||||
|
||||
<section class="h-screen w-screen p-5">
|
||||
<!-- navbar -->
|
||||
<div class="flex w-full items-center justify-between overflow-hidden rounded-md">
|
||||
<SkeletonText style="width:80%; margin-right:10rem;border-radius:10px" />
|
||||
<Skeleton class="mr-40 h-4 w-[80%] rounded-[10px]" />
|
||||
<div class="flex w-[20%] items-center justify-between">
|
||||
<SkeletonText style="width:2%; margin-right:1rem; border-radius:10px" />
|
||||
<SkeletonText style="width:2%; margin-right:1rem; border-radius:10px" />
|
||||
<SkeletonText style="width:2%; border-radius:10px" />
|
||||
<Skeleton class="mr-1 h-4 w-[2%] rounded-[10px]" />
|
||||
<Skeleton class="mr-1 h-4 w-[2%] rounded-[10px]" />
|
||||
<Skeleton class="h-4 w-[2%] rounded-[10px]" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- hero -->
|
||||
<div class="my-8 flex w-full flex-row flex-wrap items-start justify-center gap-5 overflow-hidden rounded-md">
|
||||
<SkeletonPlaceholder style="width:100%; height:20rem" />
|
||||
<Skeleton class="h-80 w-full rounded-md" />
|
||||
</div>
|
||||
<!-- cards -->
|
||||
<div class="my-4 flex flex-wrap items-center justify-center gap-5 overflow-hidden rounded-md">
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import { untrack } from 'svelte';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import Card from '$lib/components/Courses/components/Card/index.svelte';
|
||||
import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte';
|
||||
import { courseMetaDeta, courses } from '$lib/components/Courses/store';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
|
||||
import Navigation from '$lib/components/Navigation/index.svelte';
|
||||
import { landingPageSettings } from '$lib/components/Org/Settings/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { validateEmail } from '$lib/utils/functions/validateEmail';
|
||||
import { orgLandingpageValidation } from '$lib/utils/functions/validator';
|
||||
import { getCourseBySiteName } from '$lib/utils/services/org';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
import { Accordion, AccordionItem, Column, Grid, Row } from 'carbon-components-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import MailIcon from '@lucide/svelte/icons/mail';
|
||||
import MapPinIcon from '@lucide/svelte/icons/map-pin';
|
||||
import FacebookIcon from '@lucide/svelte/icons/facebook';
|
||||
import LinkedinIcon from '@lucide/svelte/icons/linkedin';
|
||||
import TwitterIcon from '@lucide/svelte/icons/twitter';
|
||||
import PhoneIcon from '@lucide/svelte/icons/phone';
|
||||
import RocketIcon from '@lucide/svelte/icons/rocket';
|
||||
import get from 'lodash/get';
|
||||
import { fade } from 'svelte/transition';
|
||||
import MapPinIcon from '@lucide/svelte/icons/map-pin';
|
||||
import TwitterIcon from '@lucide/svelte/icons/twitter';
|
||||
import FacebookIcon from '@lucide/svelte/icons/facebook';
|
||||
import LinkedinIcon from '@lucide/svelte/icons/linkedin';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import Navigation from '$lib/components/Navigation/index.svelte';
|
||||
import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Card from '$lib/components/Courses/components/Card/index.svelte';
|
||||
import { landingPageSettings } from '$lib/components/Org/Settings/store';
|
||||
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
|
||||
import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte';
|
||||
|
||||
import PageLoader from './PageLoader.svelte';
|
||||
import Row from '../Settings/Layout/Row.svelte';
|
||||
import Grid from '../Settings/Layout/Grid.svelte';
|
||||
import * as Accordion from '@cio/ui/base/accordion';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import Column from '../Settings/Layout/Column.svelte';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { getCourseBySiteName } from '$lib/utils/services/org';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { validateEmail } from '$lib/utils/functions/validateEmail';
|
||||
import { courseMetaDeta, courses } from '$lib/components/Courses/store';
|
||||
import { orgLandingpageValidation } from '$lib/utils/functions/validator';
|
||||
|
||||
interface Props {
|
||||
orgSiteName?: string;
|
||||
@@ -219,7 +223,7 @@
|
||||
<p class=" text-primary-600 text-2xl font-semibold capitalize">
|
||||
{org.name}
|
||||
</p>
|
||||
<h1 class="my-4 text-center text-4xl font-bold md:text-start md:text-5xl lg:text-6xl">
|
||||
<h1 class="my-4 text-center text-4xl md:text-start md:text-5xl lg:text-6xl">
|
||||
{$landingPageSettings.header.title} <br /><span class="text-primary-600"
|
||||
>{$landingPageSettings.header.titleHighlight}</span
|
||||
>
|
||||
@@ -264,7 +268,7 @@
|
||||
<div class="relative z-20 flex h-full w-full items-center justify-center md:flex-row">
|
||||
<div class="mx-auto flex w-11/12 max-w-[600px] flex-col items-center py-10">
|
||||
<p class=" text-primary-600 text-2xl font-semibold capitalize">{org.name}</p>
|
||||
<h1 class="my-4 text-center text-4xl font-bold md:text-5xl lg:text-6xl">
|
||||
<h1 class="my-4 text-center text-4xl md:text-5xl lg:text-6xl">
|
||||
{$landingPageSettings.header.title} <br /><span class="text-primary-600"
|
||||
>{$landingPageSettings.header.titleHighlight}</span
|
||||
>
|
||||
@@ -309,7 +313,7 @@
|
||||
<section id="courses" transition:fade class="mx-auto my-10 w-full max-w-6xl">
|
||||
<div class="w-full">
|
||||
<div class="mx-auto w-11/12 max-w-[500px] py-10">
|
||||
<h1 class="my-4 text-center text-4xl font-bold md:text-5xl lg:text-6xl">
|
||||
<h1 class="my-4 text-center text-4xl md:text-5xl lg:text-6xl">
|
||||
{$landingPageSettings.courses.title}
|
||||
<span class="text-primary-600">{$landingPageSettings.courses.titleHighlight}</span>
|
||||
</h1>
|
||||
@@ -349,7 +353,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<Box>
|
||||
<CoursesEmptyIcon size={16} />
|
||||
<CoursesEmptyIcon />
|
||||
<h3 class="my-5 text-2xl dark:text-white">
|
||||
{$t('course.navItem.landing_page.no_course_published')}
|
||||
</h3>
|
||||
@@ -376,20 +380,25 @@
|
||||
{#if $landingPageSettings.faq.show}
|
||||
<section id="faq" transition:fade class="mx-auto my-10 w-full max-w-[700px]">
|
||||
<div class="py-10">
|
||||
<h1 class="my-4 text-center text-4xl font-bold md:text-5xl lg:text-6xl">
|
||||
<h1 class="my-4 text-center text-4xl md:text-5xl lg:text-6xl">
|
||||
{$landingPageSettings.faq.title}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="mx-2">
|
||||
<Accordion size="xl">
|
||||
{#each $landingPageSettings.faq.questions as faq}
|
||||
<AccordionItem title={faq.title} class="text-3xl">
|
||||
<Accordion.Root type="single" class="w-full">
|
||||
{#each $landingPageSettings.faq.questions as faq, index}
|
||||
<Accordion.Item value="item-{index}">
|
||||
<Accordion.Trigger class="text-left text-lg font-medium">
|
||||
{faq.title}
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
<p class="text-lg">
|
||||
{faq.content}
|
||||
</p>
|
||||
</AccordionItem>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
{/each}
|
||||
</Accordion>
|
||||
</Accordion.Root>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
@@ -399,7 +408,7 @@
|
||||
<section id="contact" transition:fade class="bg-primary-50 my-10 w-full">
|
||||
<div class="mx-auto w-full max-w-6xl">
|
||||
<div class="mx-auto w-11/12 max-w-[500px] py-10">
|
||||
<h1 class="my-4 text-center text-4xl font-bold md:text-5xl lg:text-6xl">
|
||||
<h1 class="my-4 text-center text-4xl md:text-5xl lg:text-6xl">
|
||||
{$landingPageSettings.contact.title}
|
||||
<span class="text-primary-600">{$landingPageSettings.contact.titleHighlight}</span>
|
||||
</h1>
|
||||
@@ -491,7 +500,7 @@
|
||||
<section id="waitlist" transition:fade class="mx-auto my-10 w-[95%] max-w-6xl">
|
||||
<div class="bg-primary-700 flex flex-col rounded-lg px-4 py-14 md:px-10 lg:flex-row lg:items-center">
|
||||
<div class="w-full md:mr-4 md:w-[65%]">
|
||||
<h1 class="mb-5 mt-0 text-4xl font-bold text-white">
|
||||
<h1 class="mb-5 mt-0 text-4xl text-white">
|
||||
{$landingPageSettings.mailinglist.title}
|
||||
</h1>
|
||||
<p class="text-lg text-white">
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import * as Popover from '@cio/ui/base/popover';
|
||||
import * as Command from '@cio/ui/base/command';
|
||||
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
|
||||
import Menu from '../FooterMenu/Menu.svelte';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
|
||||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-center">
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger class="w-full">
|
||||
<button
|
||||
class="flex w-full items-center justify-between rounded-md p-1.5 hover:bg-gray-200 dark:hover:bg-neutral-800"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
open = !open;
|
||||
}}
|
||||
>
|
||||
<div class="flex w-full items-center justify-start space-x-2 text-start">
|
||||
<Avatar src={$profile.avatar_url} name={$profile.username} width="w-[1.2rem]" height="h-[1.2rem]" />
|
||||
<p class="max-w-full truncate text-sm font-medium dark:text-white">
|
||||
{$profile.fullname}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ChevronsUpDownIcon size={16} />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content class="w-[200px] p-0" side="right" align="start">
|
||||
<Command.Root>
|
||||
<Command.List>
|
||||
<Menu />
|
||||
</Command.List>
|
||||
</Command.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
@@ -1,144 +0,0 @@
|
||||
<script>
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { currentOrg, currentOrgDomain, currentOrgPath } from '$lib/utils/store/org';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import LogOutIcon from '@lucide/svelte/icons/log-out';
|
||||
import RocketIcon from '@lucide/svelte/icons/rocket';
|
||||
import BellPlusIcon from '@lucide/svelte/icons/bell-plus';
|
||||
import { profileMenu } from '../store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { logout } from '$lib/utils/functions/logout';
|
||||
|
||||
async function handleLogout() {
|
||||
await logout();
|
||||
closeMenu();
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
$profileMenu.open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="cursor-pointer rounded-md px-2 py-2 md:px-4 md:py-4">
|
||||
<div class="space-y-4 border-b py-3">
|
||||
<p class="text-xs font-semibold text-gray-500">{$t('profileMenu.profile')}</p>
|
||||
<a
|
||||
href={`${!$globalStore.isOrgSite ? $currentOrgPath : '/lms'}/settings`}
|
||||
class="flex items-center justify-between hover:no-underline"
|
||||
onclick={closeMenu}
|
||||
>
|
||||
<span class="flex max-w-[70%] items-center gap-2">
|
||||
<img src={$profile.avatar_url} alt="profile" class="h-8 w-8 rounded-full" />
|
||||
<div>
|
||||
<p class="w-[80%] truncate text-sm font-semibold">{$profile.fullname}</p>
|
||||
<p class="w-[80%] truncate text-xs">{$profile.email}</p>
|
||||
</div>
|
||||
</span>
|
||||
<div>
|
||||
<SettingsIcon size={16} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{#if !$globalStore.isOrgSite}
|
||||
<div class="space-y-4 border-b py-3">
|
||||
<p class="text-xs font-semibold text-gray-500">{$t('profileMenu.current_org')}</p>
|
||||
<a
|
||||
href={`${$currentOrgPath}/settings?tab=org`}
|
||||
class="flex items-center justify-between hover:no-underline"
|
||||
onclick={closeMenu}
|
||||
>
|
||||
<span class="flex max-w-[70%] items-center gap-2">
|
||||
{#if $currentOrg.avatar_url && $currentOrg.name}
|
||||
<Avatar src={$currentOrg.avatar_url} name={$currentOrg.name} shape="rounded-md" width="w-7" height="h-7" />
|
||||
{:else if $currentOrg.shortName}
|
||||
<TextChip size="sm" value={$currentOrg.shortName} className="bg-primary-200 dark:text-black font-medium" />
|
||||
{/if}
|
||||
<div>
|
||||
<p class="w-[80%] truncate text-sm font-semibold">{$currentOrg.name}</p>
|
||||
<p class="w-[80%] truncate text-xs">
|
||||
{$currentOrgDomain}
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<div>
|
||||
<SettingsIcon size={16} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4 border-b py-3">
|
||||
<p class="text-xs font-semibold text-gray-500">{$t('profileMenu.free_tools')}</p>
|
||||
<a
|
||||
href="https://classroomio.com/tools/progress"
|
||||
target="_blank"
|
||||
class="flex items-center gap-2"
|
||||
onclick={closeMenu}
|
||||
>
|
||||
<img src="/progress.svg" alt="progress" class="h-6 w-6 rounded-full" />
|
||||
<p class="text-sm font-semibold">{$t('profileMenu.progress')}</p>
|
||||
</a>
|
||||
<a
|
||||
href="https://classroomio.com/tools/activity-stopwatch"
|
||||
target="_blank"
|
||||
class=" flex items-center gap-2"
|
||||
onclick={closeMenu}
|
||||
>
|
||||
<img src="/timer.svg" alt="timer" class="h-6 w-6 rounded-full" />
|
||||
<p class="text-sm font-semibold">{$t('profileMenu.timer')}</p>
|
||||
</a>
|
||||
<a
|
||||
href="https://classroomio.com/tools/tic-tac-toe"
|
||||
target="_blank"
|
||||
class=" flex items-center gap-2"
|
||||
onclick={closeMenu}
|
||||
>
|
||||
<img src="/tictac.svg" alt="tic_tac_toe" class="h-6 w-6 rounded-full" />
|
||||
<p class="text-sm font-semibold">{$t('profileMenu.tic_tac')}</p>
|
||||
</a>
|
||||
<a
|
||||
href="https://classroomio.com/tools"
|
||||
onclick={closeMenu}
|
||||
target="_blank"
|
||||
class="ml-auto flex w-fit items-center justify-end"
|
||||
>
|
||||
<div class="flex items-center gap-1 text-xs font-semibold text-blue-900">
|
||||
{$t('profileMenu.see_more')}
|
||||
<ChevronDownIcon size={16} class="text-blue-900" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{#if !$globalStore.isOrgSite}
|
||||
<div class="space-y-4 border-b py-3">
|
||||
<a
|
||||
href="https://classroomio.com/roadmap"
|
||||
target="_blank"
|
||||
onclick={closeMenu}
|
||||
class="flex items-center gap-2 hover:no-underline"
|
||||
>
|
||||
<BellPlusIcon size={16} />
|
||||
<p class="text-sm font-semibold">{$t('profileMenu.whats_new')}</p>
|
||||
</a>
|
||||
<a
|
||||
href="https://classroomio.com/blog/launch-week"
|
||||
target="_blank"
|
||||
onclick={closeMenu}
|
||||
class="flex items-center gap-2 hover:no-underline"
|
||||
>
|
||||
<RocketIcon size={16} />
|
||||
<p class="text-sm font-semibold">{$t('profileMenu.launch_week')}</p>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button onclick={handleLogout} class="w-full space-y-4 pt-3">
|
||||
<span class="flex items-center gap-2">
|
||||
<LogOutIcon size={16} />
|
||||
<p class="text-sm font-semibold">{$t('settings.profile.logout')}</p>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script>
|
||||
import Menu from './Menu.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { profileMenu } from '../store';
|
||||
import { clickOutside } from '$lib/utils/functions/routes/dashboard';
|
||||
</script>
|
||||
|
||||
{#if $isMobile}
|
||||
<Modal
|
||||
bind:open={$profileMenu.open}
|
||||
onClose={() => ($profileMenu.open = false)}
|
||||
width="w-4/5"
|
||||
containerClass="h-full !max-h-[70vh] pt-0 pb-2"
|
||||
headerClass="py-1"
|
||||
>
|
||||
<Menu />
|
||||
</Modal>
|
||||
{:else if $profileMenu.open}
|
||||
<div
|
||||
use:clickOutside
|
||||
on:click:outside={() => ($profileMenu.open = false)}
|
||||
class="absolute bottom-28 left-0 z-[300] w-full border bg-black p-2 dark:bg-white"
|
||||
>
|
||||
<Menu />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="root relative mb-4 mr-4 flex w-full rounded-md border border-gray-100 p-3 transition ease-in-out dark:border-neutral-600 dark:bg-neutral-800"
|
||||
>
|
||||
<SkeletonPlaceholder style="width: 20%; height: 100%;" />
|
||||
<Skeleton class="h-full w-[20%]" />
|
||||
<div class="mx-5 flex w-full flex-col justify-evenly">
|
||||
<!-- Title -->
|
||||
<SkeletonText style="width: 50%;" />
|
||||
<Skeleton class="h-4 w-2/4" />
|
||||
<!-- Description -->
|
||||
<SkeletonText style="width: 50%; height: 4rem;" />
|
||||
<Skeleton class="h-16 w-2/4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
{#snippet footer()}
|
||||
<div class="flex items-center justify-center">
|
||||
<p class="mr-3 font-bold">Let's go</p>
|
||||
<p class="mr-3">Let's go</p>
|
||||
<PrimaryButton label="View Players" variant={VARIANTS.OUTLINED} onClick={goToPlayersStep} />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script lang="ts">
|
||||
import pluralize from 'pluralize';
|
||||
import { ImageLoader } from 'carbon-components-svelte';
|
||||
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
||||
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import Dropdown from '$lib/components/Dropdown/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { onRename, onDelete } from '$lib/utils/services/org/quiz';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { themeImages } from '$lib/utils/constants/quiz';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { onRename, onDelete } from '$lib/utils/services/org/quiz';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
|
||||
import ImageRenderer from '../ImageRenderer.svelte';
|
||||
import Dropdown from '$lib/components/Dropdown/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
let { quiz, totalQuestions } = $props();
|
||||
|
||||
@@ -55,15 +57,15 @@
|
||||
<div
|
||||
class="root relative mb-5 flex w-full flex-col rounded-lg border bg-gray-100 p-3 transition ease-in-out hover:shadow-2xl lg:flex-row dark:bg-black"
|
||||
>
|
||||
<ImageLoader
|
||||
<ImageRenderer
|
||||
src={themeImages[quiz.theme]?.card || themeImages.standard.card}
|
||||
alt="quiz-card"
|
||||
style="max-width:300px, min-width: 200px;"
|
||||
className="max-w-[300px] min-w-[200px]"
|
||||
/>
|
||||
|
||||
<div class="flex w-full flex-col justify-between p-2 md:p-5">
|
||||
<div class="flex justify-between">
|
||||
<h4 class="title text-md font-bold capitalize dark:text-white">
|
||||
<h4 class="title text-md capitalize dark:text-white">
|
||||
<a href="{$currentOrgPath}/quiz/{quiz.id}">{quiz.title}</a>
|
||||
</h4>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
{#if isPreview}
|
||||
<h1 class="mb-5 font-bold text-white">{currentQuestion.label}</h1>
|
||||
<h1 class="mb-5 text-white">{currentQuestion.label}</h1>
|
||||
{:else}
|
||||
<TextField
|
||||
placeholder={$t('components.quiz.start_typing')}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script>
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { currentOrgPlan, isFreePlan } from '$lib/utils/store/org';
|
||||
import { Column, Grid, Row } from 'carbon-components-svelte';
|
||||
|
||||
import Row from './Layout/Row.svelte';
|
||||
import Grid from './Layout/Grid.svelte';
|
||||
import Column from './Layout/Column.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
let isRedirecting = $state(false);
|
||||
|
||||
@@ -43,7 +46,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c mt-5 w-full rounded border-gray-200 dark:border-neutral-600">
|
||||
<Grid class="mt-5 w-full">
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}><SectionTitle>{$t('settings.billing.sub_title')}</SectionTitle></Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import SaveIcon from '@lucide/svelte/icons/save';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import { Toggle } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
|
||||
import Row from './Layout/Row.svelte';
|
||||
import Grid from './Layout/Grid.svelte';
|
||||
import Column from './Layout/Column.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
const supabase = getSupabase();
|
||||
|
||||
@@ -44,37 +49,40 @@
|
||||
</script>
|
||||
|
||||
<Grid class="border-c relative mt-5 w-full rounded border-gray-200 dark:border-neutral-600">
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Row class="border-bottom-c flex flex-col items-start py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('components.settings.customize_lms.dashboard.title')}</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<div>
|
||||
<p>{$t('components.settings.customize_lms.dashboard.community')}</p>
|
||||
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.dashboard.community}>
|
||||
<span slot="labelA" style="color: gray">
|
||||
{$t('components.settings.customize_lms.disabled')}
|
||||
</span>
|
||||
<span slot="labelB" style="color: gray">
|
||||
{$t('components.settings.customize_lms.enabled')}
|
||||
</span>
|
||||
</Toggle>
|
||||
|
||||
<Column sm={8} md={8} lg={8} class="space-y-2">
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.dashboard.community')}</p>
|
||||
<div class="mb-3 flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.dashboard.community} />
|
||||
|
||||
<Label class="text-sm">
|
||||
{$currentOrg.customization.dashboard.community
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>{$t('components.settings.customize_lms.dashboard.exercises')}</p>
|
||||
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.dashboard.exercise}>
|
||||
<span slot="labelA" style="color: gray">
|
||||
{$t('components.settings.customize_lms.disabled')}
|
||||
</span>
|
||||
<span slot="labelB" style="color: gray">
|
||||
{$t('components.settings.customize_lms.enabled')}
|
||||
</span>
|
||||
</Toggle>
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.dashboard.exercises')}</p>
|
||||
<div class="mb-3 flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.dashboard.exercise} />
|
||||
|
||||
<Label class="text-sm">
|
||||
{$currentOrg.customization.dashboard.exercise
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<p>{$t('components.settings.customize_lms.dashboard.banner_image')}</p>
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.dashboard.banner_image')}</p>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('components.settings.customize_lms.dashboard.banner_image_btn')}
|
||||
@@ -94,7 +102,6 @@
|
||||
<TextField
|
||||
label={$t('components.settings.customize_lms.dashboard.banner_text_label')}
|
||||
placeholder={$t('components.settings.customize_lms.dashboard.banner_text_placeholder')}
|
||||
className="mb-5"
|
||||
bind:value={$currentOrg.customization.dashboard.bannerText}
|
||||
/>
|
||||
</div>
|
||||
@@ -105,25 +112,30 @@
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('components.settings.customize_lms.course.title')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<div>
|
||||
<p>{$t('components.settings.customize_lms.course.newsfeed')}</p>
|
||||
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.course.newsfeed}>
|
||||
<span slot="labelA" style="color: gray">
|
||||
{$t('components.settings.customize_lms.disabled')}
|
||||
</span>
|
||||
<span slot="labelB" style="color: gray">
|
||||
{$t('components.settings.customize_lms.enabled')}
|
||||
</span>
|
||||
</Toggle>
|
||||
<Column sm={8} md={8} lg={8} class="space-y-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.course.newsfeed')}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.course.newsfeed} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$currentOrg.customization.course.newsfeed
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>{$t('components.settings.customize_lms.course.grading')}</p>
|
||||
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.course.grading}>
|
||||
<span slot="labelA" style="color: gray">{$t('components.settings.customize_lms.disabled')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('components.settings.customize_lms.enabled')}</span>
|
||||
</Toggle>
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.course.grading')}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.course.grading} />
|
||||
<Label class="text-gray-600">
|
||||
{$currentOrg.customization.course.grading
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
@@ -132,20 +144,30 @@
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('components.settings.customize_lms.apps.title')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<div>
|
||||
<p>{$t('components.settings.customize_lms.apps.poll')}</p>
|
||||
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.apps.poll}>
|
||||
<span slot="labelA" style="color: gray">{$t('components.settings.customize_lms.disabled')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('components.settings.customize_lms.enabled')}</span>
|
||||
</Toggle>
|
||||
<Column sm={8} md={8} lg={8} class="space-y-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.apps.poll')}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.apps.poll} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$currentOrg.customization.apps.poll
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.apps.live_comment')}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.apps.comments} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$currentOrg.customization.apps.comments
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
<p>{$t('components.settings.customize_lms.apps.live_comment')}</p>
|
||||
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.apps.comments}>
|
||||
<span slot="labelA" style="color: gray">{$t('components.settings.customize_lms.disabled')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('components.settings.customize_lms.enabled')}</span>
|
||||
</Toggle>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Column, CopyButton, Grid, Row } from 'carbon-components-svelte';
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import Copy from '@lucide/svelte/icons/copy';
|
||||
import isValidDomain from 'is-valid-domain';
|
||||
import { goto } from '$app/navigation';
|
||||
import { untrack } from 'svelte';
|
||||
import { parse } from 'tldts';
|
||||
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { blockedSubdomain } from '$lib/utils/constants/app';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { copyToClipboard } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { updateOrgSiteNameValidation } from '$lib/utils/functions/validator';
|
||||
import { sanitizeDomain, sendDomainRequest } from '$lib/utils/functions/domain';
|
||||
|
||||
import Row from './Layout/Row.svelte';
|
||||
import Grid from './Layout/Grid.svelte';
|
||||
import Column from './Layout/Column.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import { blockedSubdomain } from '$lib/utils/constants/app';
|
||||
import { sanitizeDomain, sendDomainRequest } from '$lib/utils/functions/domain';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { updateOrgSiteNameValidation } from '$lib/utils/functions/validator';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
|
||||
let siteName = $derived($currentOrg.siteName);
|
||||
let customDomain = $state('');
|
||||
@@ -202,10 +207,11 @@
|
||||
|
||||
<Grid class="mt-5 w-full rounded border border-gray-200 dark:border-neutral-600">
|
||||
<Row class="border-bottom-c py-7">
|
||||
<Column sm={2} md={2} lg={4} class="text-lg">
|
||||
<Column sm={4} md={4} lg={4} class="text-lg">
|
||||
<SectionTitle>{$t('components.settings.domains.add')}</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={2} md={6} lg={8}>
|
||||
|
||||
<Column sm={8} md={8} lg={8} class="space-y-2">
|
||||
<p class="text-md mb-5 text-gray-500 dark:text-white">
|
||||
{$t('settings.organization.organization_profile.custom_domain.body')}
|
||||
</p>
|
||||
@@ -225,7 +231,7 @@
|
||||
<div class="mb-6 flex items-center">
|
||||
<PrimaryButton
|
||||
label={$t('components.settings.domains.update')}
|
||||
className="py-4"
|
||||
className="py-2"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleSaveSiteName}
|
||||
isDisabled={isLoading}
|
||||
@@ -239,12 +245,13 @@
|
||||
|
||||
<Row class="py-7">
|
||||
<UpgradeBanner className="mb-5">{$t('upgrade.domain')}</UpgradeBanner>
|
||||
<Column sm={2} md={2} lg={4} class="text-lg">
|
||||
<Column sm={4} md={4} lg={4} class="text-lg">
|
||||
<SectionTitle>
|
||||
{$t('components.settings.domains.custom')}
|
||||
</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={2} md={6} lg={10}>
|
||||
|
||||
<Column sm={8} md={8} lg={8} class="space-y-2">
|
||||
<div class="border-bottom-c pb-10">
|
||||
<!-- DNS Configuration -->
|
||||
{#if $currentOrg.customDomain}
|
||||
@@ -290,14 +297,18 @@
|
||||
<p class="text-sm font-light">{$t('components.settings.domains.dns_value')}</p>
|
||||
<p class=" flex items-center gap-1">
|
||||
cname.vercel-dns.com
|
||||
<CopyButton text="cname.vercel-dns.com" />
|
||||
<IconButton onClick={() => copyToClipboard('cname.vercel-dns.com')}>
|
||||
<p>cname.vercel-dns.com</p>
|
||||
|
||||
<Copy />
|
||||
</IconButton>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="py-4 flex items-center gap-2"
|
||||
className="py-2 flex items-center gap-2"
|
||||
onClick={handleRefreshCustomDomain}
|
||||
isLoading={isRefreshing}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
@@ -309,7 +320,7 @@
|
||||
</PrimaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
className="py-4 flex items-center gap-2"
|
||||
className="py-2 flex items-center gap-2"
|
||||
onClick={handleRemoveCustomDomain}
|
||||
isLoading={isCustomDomainLoading}
|
||||
variant={VARIANTS.CONTAINED_DANGER}
|
||||
@@ -338,7 +349,7 @@
|
||||
<div class="mt-5 flex items-center">
|
||||
<PrimaryButton
|
||||
label={$t('components.settings.domains.save')}
|
||||
className="py-4"
|
||||
className="py-2"
|
||||
onClick={$isFreePlan ? () => {} : handleSaveCustomDomain}
|
||||
isLoading={isCustomDomainLoading}
|
||||
isDisabled={isLoading || !isDomainValid}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Grid, Column, Row } from 'carbon-components-svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
|
||||
import Row from './Layout/Row.svelte';
|
||||
import Grid from './Layout/Grid.svelte';
|
||||
import Column from './Layout/Column.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
|
||||
let chatId: number | null = $state(null);
|
||||
|
||||
@@ -46,7 +49,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c mt-5 w-full rounded border-gray-200 dark:border-neutral-600">
|
||||
<Grid class="mt-5 w-full">
|
||||
<Row class="border-bottom-c flex flex-col justify-center py-7 lg:flex-row lg:justify-start">
|
||||
<Column sm={4} md={4} lg={4} class="flex items-center justify-center">
|
||||
<img src="/telegram-svg.svg" alt="" class="mr-2 w-10" />
|
||||
@@ -75,7 +78,7 @@
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => window.open('https://t.me/classroomio_bot', '_blank')}
|
||||
><span class="mr-2">{$t('settings.integrations.open_bot_button')}</span>
|
||||
<ArrowUpRightIcon size={16} /></PrimaryButton
|
||||
<ArrowUpRightIcon /></PrimaryButton
|
||||
>
|
||||
<h4 class="font-normal">{$t('settings.integrations.step_chatId')}</h4>
|
||||
<TextField
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { Column, Grid, RadioButton, RadioButtonGroup, Row, Toggle } from 'carbon-components-svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import SaveIcon from '@lucide/svelte/icons/save';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import * as RadioGroup from '@cio/ui/base/radio-group';
|
||||
|
||||
import { landingPageSettings } from './store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
|
||||
import Row from './Layout/Row.svelte';
|
||||
import Grid from './Layout/Grid.svelte';
|
||||
import Column from './Layout/Column.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
|
||||
let isSaving = $state(false);
|
||||
let creatingNewQuestion = $state(false);
|
||||
@@ -170,14 +177,19 @@
|
||||
|
||||
<UnsavedChanges bind:hasUnsavedChanges />
|
||||
|
||||
<Grid class="border-c relative mt-5 w-full rounded border-gray-200 dark:border-neutral-600">
|
||||
<Grid class="relative mt-5 w-full">
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.heading')}</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.header.show} size="sm" on:change={() => (hasUnsavedChanges = true)}>
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
|
||||
</Toggle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.header.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.header.show
|
||||
? $t('settings.landing_page.show_section')
|
||||
: $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
@@ -220,29 +232,37 @@
|
||||
bind:value={$landingPageSettings.header.action.link}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<Toggle
|
||||
bind:toggled={$landingPageSettings.header.action.redirect}
|
||||
size="sm"
|
||||
on:change={() => (hasUnsavedChanges = true)}
|
||||
>
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.actions.no_redirect')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.actions.redirect')}</span>
|
||||
</Toggle>
|
||||
<div class="mt-3 flex items-center space-x-2">
|
||||
<Switch
|
||||
bind:checked={$landingPageSettings.header.action.redirect}
|
||||
onCheckedChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.header.action.redirect
|
||||
? $t('settings.landing_page.actions.redirect')
|
||||
: $t('settings.landing_page.actions.no_redirect')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadioButtonGroup
|
||||
legendText={$t('settings.landing_page.actions.banner_type.heading')}
|
||||
bind:selected={$landingPageSettings.header.banner.type}
|
||||
on:click={() => {
|
||||
<div class="mb-5 mt-10">
|
||||
<Label class="mb-3 block font-medium">{$t('settings.landing_page.actions.banner_type.heading')}</Label>
|
||||
<RadioGroup.Root
|
||||
bind:value={$landingPageSettings.header.banner.type}
|
||||
onValueChange={() => {
|
||||
if (hasUnsavedChanges) return;
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
class="mb-5 mt-10"
|
||||
>
|
||||
{#each banner as item}
|
||||
<RadioButton value={item.value} labelText={item.label} />
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value={item.value} id={item.value} />
|
||||
<Label for={item.value} class="cursor-pointer">{item.label}</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</RadioButtonGroup>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
{#if $landingPageSettings.header.banner.type === 'video'}
|
||||
<div class="mb-5 mt-3 gap-2">
|
||||
<TextField
|
||||
@@ -264,14 +284,17 @@
|
||||
<img alt="bannerImage" src={$landingPageSettings.header.banner.image} class="mt-2 w-full rounded-md" />
|
||||
{/if}
|
||||
|
||||
<Toggle
|
||||
bind:toggled={$landingPageSettings.header.banner.show}
|
||||
size="sm"
|
||||
on:change={() => (hasUnsavedChanges = true)}
|
||||
>
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.actions.hide_banner')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.actions.show_banner')}</span>
|
||||
</Toggle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
bind:checked={$landingPageSettings.header.banner.show}
|
||||
onCheckedChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.header.banner.show
|
||||
? $t('settings.landing_page.actions.show_banner')
|
||||
: $t('settings.landing_page.actions.hide_banner')}
|
||||
</Label>
|
||||
</div>
|
||||
{#if $handleOpenWidget.open && widgetKey === 'banner'}
|
||||
<UploadWidget
|
||||
bind:imageURL={$landingPageSettings.header.banner.image}
|
||||
@@ -282,7 +305,7 @@
|
||||
<!-- background -->
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="mb-4 font-bold">{$t('settings.landing_page.background.title')}</p>
|
||||
<p class="mb-4">{$t('settings.landing_page.background.title')}</p>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.about.select_image')}
|
||||
@@ -298,24 +321,27 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Toggle
|
||||
toggled={$landingPageSettings.header.background?.show}
|
||||
on:change={() => {
|
||||
<div class="mt-2 flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={$landingPageSettings.header.background?.show}
|
||||
onCheckedChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
if (!$landingPageSettings.header.background) {
|
||||
$landingPageSettings.header.background = {
|
||||
image: '',
|
||||
show: false
|
||||
show: true
|
||||
};
|
||||
} else {
|
||||
$landingPageSettings.header.background.show = !$landingPageSettings.header.background.show;
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.background.hide_background')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.background.show_background')}</span>
|
||||
</Toggle>
|
||||
/>
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.header.background?.show
|
||||
? $t('settings.landing_page.background.show_background')
|
||||
: $t('settings.landing_page.background.hide_background')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{#if $handleOpenWidget.open && widgetKey === 'background'}
|
||||
<UploadWidget
|
||||
@@ -339,14 +365,17 @@
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.landing_page.about.heading')}</SectionTitle>
|
||||
|
||||
<Toggle bind:toggled={$landingPageSettings.aboutUs.show} on:change={() => (hasUnsavedChanges = true)} size="sm">
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
|
||||
</Toggle></Column
|
||||
>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.about.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.aboutUs.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.aboutUs.show
|
||||
? $t('settings.landing_page.show_section')
|
||||
: $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.about.title')}
|
||||
@@ -385,12 +414,16 @@
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.landing_page.courses.heading')}</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.courses.show} size="sm" on:change={() => (hasUnsavedChanges = true)}>
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.courses.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.courses.show_section')}</span>
|
||||
</Toggle>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.courses.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.courses.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.courses.show
|
||||
? $t('settings.landing_page.courses.show_section')
|
||||
: $t('settings.landing_page.courses.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
@@ -419,11 +452,15 @@
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.faq.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.faq.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
|
||||
<Toggle bind:toggled={$landingPageSettings.faq.show} size="sm" on:change={() => (hasUnsavedChanges = true)}>
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.faq.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.faq.show_section')}</span>
|
||||
</Toggle>
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.faq.show
|
||||
? $t('settings.landing_page.faq.show_section')
|
||||
: $t('settings.landing_page.faq.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<TextField
|
||||
@@ -496,12 +533,16 @@
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.landing_page.contact_us.heading')}</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.contact.show} size="sm" on:change={() => (hasUnsavedChanges = true)}>
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.contact_us.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.contact_us.show_section')}</span>
|
||||
</Toggle>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.contact_us.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.contact.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.contact.show
|
||||
? $t('settings.landing_page.contact_us.show_section')
|
||||
: $t('settings.landing_page.contact_us.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<TextField
|
||||
@@ -553,16 +594,19 @@
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.landing_page.mailing_list.heading')}</SectionTitle>
|
||||
<Toggle
|
||||
bind:toggled={$landingPageSettings.mailinglist.show}
|
||||
size="sm"
|
||||
on:change={() => (hasUnsavedChanges = true)}
|
||||
>
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.mailing_list.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.mailing_list.show_section')}</span>
|
||||
</Toggle>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.mailing_list.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
bind:checked={$landingPageSettings.mailinglist.show}
|
||||
onCheckedChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.mailinglist.show
|
||||
? $t('settings.landing_page.mailing_list.show_section')
|
||||
: $t('settings.landing_page.mailing_list.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
@@ -593,12 +637,17 @@
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.landing_page.custom_links.heading')}</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.customLinks.show} size="sm">
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.custom_links.hide_links')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.custom_links.show_links')}</span>
|
||||
</Toggle>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.custom_links.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.customLinks.show} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.customLinks.show
|
||||
? $t('settings.landing_page.custom_links.show_links')
|
||||
: $t('settings.landing_page.custom_links.hide_links')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<p class="mb-4 text-sm text-gray-600">
|
||||
@@ -682,12 +731,17 @@
|
||||
</Row>
|
||||
|
||||
<Row class="flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.landing_page.footer.heading')}</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.footer.show} size="sm" on:change={() => (hasUnsavedChanges = true)}>
|
||||
<span slot="labelA" style="color: gray">{$t('settings.landing_page.footer.hide_section')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('settings.landing_page.footer.show_section')}</span>
|
||||
</Toggle>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.footer.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.footer.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.footer.show
|
||||
? $t('settings.landing_page.footer.show_section')
|
||||
: $t('settings.landing_page.footer.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Dropdown } from 'carbon-components-svelte';
|
||||
import { t, initialized } from '$lib/utils/functions/translations';
|
||||
import { LANGUAGES } from '$lib/utils/constants/translation';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
|
||||
import { LOCALE } from '$lib/utils/types';
|
||||
import { LANGUAGES } from '$lib/utils/constants/translation';
|
||||
import { t, initialized } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -13,23 +15,25 @@
|
||||
|
||||
let { className = '', value = $bindable(LOCALE.EN), hasLangChanged = $bindable(false), change }: Props = $props();
|
||||
|
||||
const dropdownItems = [{ id: '', text: 'Pick a Language' }, ...LANGUAGES];
|
||||
|
||||
function handleSelect(event) {
|
||||
value = event.detail.selectedId;
|
||||
|
||||
function handleSelect(selectedValue: string) {
|
||||
value = selectedValue as LOCALE;
|
||||
hasLangChanged = true;
|
||||
|
||||
change?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $initialized}
|
||||
<Dropdown
|
||||
titleText={$t('content.toggle_label')}
|
||||
items={dropdownItems}
|
||||
selectedId={value}
|
||||
on:select={handleSelect}
|
||||
class="h-full {className}"
|
||||
/>
|
||||
<div class={className}>
|
||||
<Label class="mb-2 block">{$t('content.toggle_label')}</Label>
|
||||
<Select.Root type="single" bind:value onValueChange={handleSelect}>
|
||||
<Select.Trigger class="w-full">
|
||||
<p>{value ? LANGUAGES.find((lang) => lang.id === value)?.text : 'Pick a Language'}</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each LANGUAGES as language}
|
||||
<Select.Item value={language.id}>{language.text}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
sm?: number;
|
||||
md?: number;
|
||||
lg?: number;
|
||||
class?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { sm = 8, md = 8, lg = 8, class: className = '', children }: Props = $props();
|
||||
|
||||
const getColSpan = (cols: number) => {
|
||||
const spanMap = {
|
||||
1: 'col-span-1',
|
||||
2: 'col-span-2',
|
||||
3: 'col-span-3',
|
||||
4: 'col-span-4',
|
||||
5: 'col-span-5',
|
||||
6: 'col-span-6',
|
||||
7: 'col-span-7',
|
||||
8: 'col-span-8',
|
||||
9: 'col-span-9',
|
||||
10: 'col-span-10',
|
||||
11: 'col-span-11',
|
||||
12: 'col-span-12',
|
||||
13: 'col-span-13',
|
||||
14: 'col-span-14',
|
||||
15: 'col-span-15',
|
||||
16: 'col-span-16'
|
||||
};
|
||||
return spanMap[cols] || 'col-span-full';
|
||||
};
|
||||
|
||||
const smClass = getColSpan(sm);
|
||||
const mdClass = `md:${getColSpan(md)}`;
|
||||
const lgClass = `lg:${getColSpan(lg)}`;
|
||||
</script>
|
||||
|
||||
<div class="{smClass} {mdClass} {lgClass} {className} grid">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
class?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { class: className = '', children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="w-full {className}">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
id?: string;
|
||||
class?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { id, class: className = '', children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div {id} class="lg:grid-cols-16 grid grid-cols-1 gap-5 px-3 {className}">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,24 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Column, Grid, Row } from 'carbon-components-svelte';
|
||||
import ZapIcon from '@lucide/svelte/icons/zap';
|
||||
import debounce from 'lodash/debounce';
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { injectCustomTheme, setCustomTheme, setTheme } from '$lib/utils/functions/theme';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { updateOrgNameValidation } from '$lib/utils/functions/validator';
|
||||
import { currentOrg, currentOrgPath, isFreePlan } from '$lib/utils/store/org';
|
||||
import { injectCustomTheme, setCustomTheme, setTheme } from '$lib/utils/functions/theme';
|
||||
|
||||
import Row from './Layout/Row.svelte';
|
||||
import Grid from './Layout/Grid.svelte';
|
||||
import Column from './Layout/Column.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
|
||||
let avatar = $state<string | undefined>();
|
||||
let hasUnsavedChanges = $state(false);
|
||||
@@ -155,7 +157,7 @@
|
||||
|
||||
<UnsavedChanges bind:hasUnsavedChanges />
|
||||
|
||||
<Grid class="border-c mt-5 w-full rounded border-gray-200 dark:border-neutral-600">
|
||||
<Grid class="mt-5 w-full">
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import generateUUID from '$lib/utils/functions/generateUUID';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { handleLocaleChange, t } from '$lib/utils/functions/translations';
|
||||
import { updateProfileValidation } from '$lib/utils/functions/validator';
|
||||
import { LOCALE } from '$lib/utils/types';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { Column, Grid, Row } from 'carbon-components-svelte';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import type { ProfileStore } from '$lib/utils/store/user';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { updateProfileValidation } from '$lib/utils/functions/validator';
|
||||
import { handleLocaleChange, t } from '$lib/utils/functions/translations';
|
||||
|
||||
import Grid from './Layout/Grid.svelte';
|
||||
import Row from './Layout/Row.svelte';
|
||||
import Column from './Layout/Column.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import LanguagePicker from './LanguagePicker.svelte';
|
||||
import type { ProfileStore } from '$lib/utils/store/user';
|
||||
import { LOCALE } from '$lib/utils/types';
|
||||
import generateUUID from '$lib/utils/functions/generateUUID';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
|
||||
let avatar = $state<string | undefined>();
|
||||
let loading = $state(false);
|
||||
@@ -92,12 +95,12 @@
|
||||
|
||||
<UnsavedChanges bind:hasUnsavedChanges />
|
||||
|
||||
<Grid class="border-c mt-5 w-full rounded border-gray-200 dark:border-neutral-600">
|
||||
<Row class="border-bottom-c flex flex-col items-center py-7 lg:flex-row lg:items-start ">
|
||||
<Grid class="mt-0 w-full md:mt-5">
|
||||
<Row class="border-bottom-c flex flex-col items-center lg:flex-row lg:items-start lg:py-7 ">
|
||||
<Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
|
||||
<SectionTitle>{$t('settings.profile.profile_picture.heading')}</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={2} md={4} lg={8} class="mt-2 lg:mt-0">
|
||||
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0">
|
||||
<UploadImage
|
||||
bind:avatar
|
||||
src={$profile.avatar_url}
|
||||
@@ -141,14 +144,14 @@
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="m-5 flex w-full items-center gap-2 lg:justify-center">
|
||||
<div class="m-5 flex w-full items-center gap-2 lg:justify-center">
|
||||
<PrimaryButton
|
||||
label={$t('settings.profile.update_profile')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
className="mr-5"
|
||||
className="mr-5 w-fit"
|
||||
isLoading={loading}
|
||||
isDisabled={loading}
|
||||
onClick={handleUpdate}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { Grid, Row, Column, Select, SelectItem } from 'carbon-components-svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import { Moon } from 'svelte-loading-spinners';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { getOrgTeam } from '$lib/utils/services/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||
import { currentOrg, orgTeam } from '$lib/utils/store/org';
|
||||
import { ROLE_LABEL, ROLE } from '$lib/utils/constants/roles';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { validateEmailInString } from '$lib/utils/functions/validator';
|
||||
import { ROLE_LABEL, ROLE } from '$lib/utils/constants/roles';
|
||||
import { currentOrg, orgTeam } from '$lib/utils/store/org';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { getOrgTeam } from '$lib/utils/services/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import { triggerSendEmail, NOTIFICATION_NAME } from '$lib/utils/services/notification/notification';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import Row from './Layout/Row.svelte';
|
||||
import Grid from './Layout/Grid.svelte';
|
||||
import Column from './Layout/Column.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
let emailsStr = $state('');
|
||||
let errorMessage = $state('');
|
||||
let role = $state(ROLE.TUTOR);
|
||||
let role = $state(ROLE.TUTOR.toString());
|
||||
let isFetching = $state(false);
|
||||
let isLoading = $state(false);
|
||||
let isRemoving: number | null = $state(null);
|
||||
@@ -137,10 +143,10 @@
|
||||
|
||||
<Grid class="relative mt-5 w-full rounded border border-gray-200 dark:border-neutral-600">
|
||||
<Row class="border-bottom-c py-7">
|
||||
<Column sm={2} md={2} lg={4} class="text-lg"
|
||||
<Column sm={4} md={4} lg={4} class="text-lg"
|
||||
><SectionTitle>{$t('course.navItem.people.teams.add')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={2} md={6} lg={8}>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<p class="text-md mb-5 text-gray-500 dark:text-white">
|
||||
{$t('course.navItem.people.teams.add_team')}
|
||||
</p>
|
||||
@@ -155,15 +161,18 @@
|
||||
{errorMessage}
|
||||
/>
|
||||
|
||||
<Select
|
||||
labelText={$t('course.navItem.people.teams.role')}
|
||||
bind:selected={role}
|
||||
class="mb-5 w-40"
|
||||
disabled={$isFreePlan}
|
||||
>
|
||||
<SelectItem value={ROLE.ADMIN} text={$t(ROLE_LABEL[ROLE.ADMIN])} />
|
||||
<SelectItem value={ROLE.TUTOR} text={$t(ROLE_LABEL[ROLE.TUTOR])} />
|
||||
</Select>
|
||||
<div class="mb-5">
|
||||
<Label class="mb-2 block">{$t('course.navItem.people.teams.role')}</Label>
|
||||
<Select.Root type="single" bind:value={role} disabled={$isFreePlan}>
|
||||
<Select.Trigger class="w-40">
|
||||
<p>{role ? $t(ROLE_LABEL[role]) : $t('course.navItem.people.teams.select_role')}</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value={ROLE.ADMIN.toString()}>{$t(ROLE_LABEL[ROLE.ADMIN])}</Select.Item>
|
||||
<Select.Item value={ROLE.TUTOR.toString()}>{$t(ROLE_LABEL[ROLE.TUTOR])}</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.people.teams.send_invite')}
|
||||
@@ -176,10 +185,10 @@
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c py-7">
|
||||
<Column sm={2} md={2} lg={4} class="text-lg"
|
||||
<Column sm={4} md={4} lg={4} class="text-lg"
|
||||
><SectionTitle>{$t('course.navItem.people.teams.members')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={2} md={6} lg={8}>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
{#if isFetching}
|
||||
<Moon />
|
||||
{:else}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Tabs, Tab, TabContent } from 'carbon-components-svelte';
|
||||
import * as Tabs from '@cio/ui/base/tabs';
|
||||
|
||||
import Profile from './Profile.svelte';
|
||||
import LandingpageSettings from './LandingpageSettings.svelte';
|
||||
// import Account from './Account.svelte';
|
||||
import OrgSettings from './OrgSettings.svelte';
|
||||
import Billing from './Billing.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import OrgSettings from './OrgSettings.svelte';
|
||||
import Integrations from './Integrations.svelte';
|
||||
import LandingpageSettings from './LandingpageSettings.svelte';
|
||||
// import Account from './Account.svelte';
|
||||
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface TabItem {
|
||||
@@ -59,45 +61,41 @@
|
||||
disabled: false
|
||||
}
|
||||
]);
|
||||
let selected = $derived(getSelectedByTab(tabs, tabKey));
|
||||
|
||||
function getSelectedByTab(_tabs: TabItem[], tabKey = '') {
|
||||
const tab = _tabs.find((t) => t.tabKey === tabKey);
|
||||
return tab ? tab.key : 0;
|
||||
}
|
||||
|
||||
function onTabChange(e: CustomEvent<number>) {
|
||||
const key = e.detail;
|
||||
const tab = tabs.find((t) => t.key === key);
|
||||
function onTabChange(value: string) {
|
||||
const tab = tabs.find((t) => t.tabKey === value);
|
||||
if (tab) {
|
||||
goto(tab.href);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tabs autoWidth bind:selected on:change={onTabChange}>
|
||||
<Tabs.Root value={tabKey} onValueChange={onTabChange} class="w-full p-0">
|
||||
<Tabs.List class="w-full">
|
||||
{#each tabs as tab}
|
||||
<Tab label={tab.label} href={tab.href} disabled={tab.disabled} />
|
||||
<Tabs.Trigger value={tab.tabKey} disabled={tab.disabled}>
|
||||
{tab.label}
|
||||
</Tabs.Trigger>
|
||||
{/each}
|
||||
<!-- <Tab label="Account" /> -->
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent class="w-full p-0">
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="" class="w-full p-0">
|
||||
<Profile />
|
||||
</TabContent>
|
||||
<!-- <TabContent>
|
||||
<Account />
|
||||
</TabContent> -->
|
||||
<TabContent class="w-full p-0">
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="org" class="w-full p-0">
|
||||
<OrgSettings />
|
||||
</TabContent>
|
||||
<TabContent class="w-full p-0">
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="landingpage" class="w-full p-0">
|
||||
<LandingpageSettings />
|
||||
</TabContent>
|
||||
<TabContent class="w-full p-0">
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="billing" class="w-full p-0">
|
||||
<Billing />
|
||||
</TabContent>
|
||||
<TabContent class="w-full p-0">
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="integrations" class="w-full p-0">
|
||||
<Integrations />
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user