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:
Adeyemi Adetunji
2025-11-21 12:18:04 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 2145782385
commit cbf6ae7e63
499 changed files with 19708 additions and 15603 deletions
-1
View File
@@ -92,7 +92,6 @@
"@types/node": "^20", "@types/node": "^20",
"all-object-keys": "^2.2.0", "all-object-keys": "^2.2.0",
"babel-jest": "^29.7.0", "babel-jest": "^29.7.0",
"carbon-components-svelte": "^0.79.0",
"diff": "^5.2.0", "diff": "^5.2.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^9.36.0", "eslint": "^9.36.0",
+178 -22
View File
@@ -39,9 +39,11 @@ html .dark {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG); scrollbar-color: var(--thumbBG) var(--scrollbarBG);
} }
html .dark { html .dark {
--scrollbarBG: #000; --scrollbarBG: #000;
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 10px; width: 10px;
height: 10px; height: 10px;
@@ -106,6 +108,7 @@ iframe.iframe {
border: 1px solid gray; border: 1px solid gray;
border-radius: 5px; border-radius: 5px;
} }
@media (max-width: 760px) { @media (max-width: 760px) {
iframe.iframe { iframe.iframe {
height: 209px; height: 209px;
@@ -116,11 +119,6 @@ code {
white-space: pre-wrap; white-space: pre-wrap;
} }
/* [aria-current] :global(svg.carbon-icon),
.active :global(svg.carbon-icon) {
fill: var(--main-primary-color);
} */
.prose a { .prose a {
color: var(--main-primary-color); color: var(--main-primary-color);
} }
@@ -128,20 +126,15 @@ code {
svg.lucide-icon:not(.custom) { svg.lucide-icon:not(.custom) {
color: #666666; color: #666666;
} }
.dark svg.lucide-icon:not(.custom) { .dark svg.lucide-icon:not(.custom) {
color: #fff; color: #fff;
} }
/* .success :global(svg.carbon-icon) {
fill: green;
}
:global(svg.carbon-icon.error) {
fill: red;
} */
.org-root { .org-root {
height: calc(100vh - 48px); height: calc(100vh - 48px);
} }
.org-slot { .org-slot {
min-width: calc(100vw - 250px); min-width: calc(100vw - 250px);
height: calc(100vh - 48px); height: calc(100vh - 48px);
@@ -151,20 +144,25 @@ svg.lucide-icon:not(.custom) {
.shake { .shake {
animation: shake 0.85s; animation: shake 0.85s;
} }
@keyframes shake { @keyframes shake {
10%, 10%,
90% { 90% {
transform: translate3d(-15px, 0, 0); transform: translate3d(-15px, 0, 0);
} }
20%, 20%,
80% { 80% {
transform: translate3d(15px, 0, 0); transform: translate3d(15px, 0, 0);
} }
30%, 30%,
50%, 50%,
70% { 70% {
transform: translate3d(-15px, 0, 0); transform: translate3d(-15px, 0, 0);
} }
40%, 40%,
60% { 60% {
transform: translate3d(15px, 0, 0); transform: translate3d(15px, 0, 0);
@@ -174,11 +172,13 @@ svg.lucide-icon:not(.custom) {
.bx--list-box__menu-item__option { .bx--list-box__menu-item__option {
white-space: normal !important; white-space: normal !important;
} }
.bx--list-box--xl .bx--list-box__menu-item, .bx--list-box--xl .bx--list-box__menu-item,
.bx--list-box--lg .bx--list-box__menu-item, .bx--list-box--lg .bx--list-box__menu-item,
.bx--list-box__menu-item__option { .bx--list-box__menu-item__option {
height: fit-content !important; height: fit-content !important;
} }
.cio-bg-blue { .cio-bg-blue {
background: rgb(2, 51, 189); background: rgb(2, 51, 189);
background-size: cover; background-size: cover;
@@ -199,9 +199,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-50 { .theme-rose .bg-primary-50 {
background-color: #fff1f2 !important; background-color: #fff1f2 !important;
} }
.theme-rose .text-primary-50 { .theme-rose .text-primary-50 {
color: #fff1f2 !important; color: #fff1f2 !important;
} }
.theme-rose .border-primary-50 { .theme-rose .border-primary-50 {
border-color: #fff1f2 !important; border-color: #fff1f2 !important;
} }
@@ -209,9 +211,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-100 { .theme-rose .bg-primary-100 {
background-color: #ffe4e7 !important; background-color: #ffe4e7 !important;
} }
.theme-rose .text-primary-100 { .theme-rose .text-primary-100 {
color: #ffe4e7 !important; color: #ffe4e7 !important;
} }
.theme-rose .border-primary-100 { .theme-rose .border-primary-100 {
border-color: #ffe4e7 !important; border-color: #ffe4e7 !important;
} }
@@ -219,9 +223,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-200 { .theme-rose .bg-primary-200 {
background-color: #fecdd4 !important; background-color: #fecdd4 !important;
} }
.theme-rose .text-primary-200 { .theme-rose .text-primary-200 {
color: #fecdd4 !important; color: #fecdd4 !important;
} }
.theme-rose .border-primary-200 { .theme-rose .border-primary-200 {
border-color: #fecdd4 !important; border-color: #fecdd4 !important;
} }
@@ -229,9 +235,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-300 { .theme-rose .bg-primary-300 {
background-color: #fda4b2 !important; background-color: #fda4b2 !important;
} }
.theme-rose .text-primary-300 { .theme-rose .text-primary-300 {
color: #fda4b2 !important; color: #fda4b2 !important;
} }
.theme-rose .border-primary-300 { .theme-rose .border-primary-300 {
border-color: #fda4b2 !important; border-color: #fda4b2 !important;
} }
@@ -239,9 +247,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-400 { .theme-rose .bg-primary-400 {
background-color: #fb7189 !important; background-color: #fb7189 !important;
} }
.theme-rose .text-primary-400 { .theme-rose .text-primary-400 {
color: #fb7189 !important; color: #fb7189 !important;
} }
.theme-rose .border-primary-400 { .theme-rose .border-primary-400 {
border-color: #fb7189 !important; border-color: #fb7189 !important;
} }
@@ -249,9 +259,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-500 { .theme-rose .bg-primary-500 {
background-color: #f43f63 !important; background-color: #f43f63 !important;
} }
.theme-rose .text-primary-500 { .theme-rose .text-primary-500 {
color: #f43f63 !important; color: #f43f63 !important;
} }
.theme-rose .border-primary-500 { .theme-rose .border-primary-500 {
border-color: #f43f63 !important; border-color: #f43f63 !important;
} }
@@ -259,9 +271,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-600 { .theme-rose .bg-primary-600 {
background-color: #e21d4e !important; background-color: #e21d4e !important;
} }
.theme-rose .text-primary-600 { .theme-rose .text-primary-600 {
color: #e21d4e !important; color: #e21d4e !important;
} }
.theme-rose .border-primary-600 { .theme-rose .border-primary-600 {
border-color: #e21d4e !important; border-color: #e21d4e !important;
} }
@@ -269,9 +283,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-700 { .theme-rose .bg-primary-700 {
background-color: #be1241 !important; background-color: #be1241 !important;
} }
.theme-rose .text-primary-700 { .theme-rose .text-primary-700 {
color: #be1241 !important; color: #be1241 !important;
} }
.theme-rose .border-primary-700 { .theme-rose .border-primary-700 {
border-color: #be1241 !important; border-color: #be1241 !important;
} }
@@ -279,9 +295,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-800 { .theme-rose .bg-primary-800 {
background-color: #9f123d !important; background-color: #9f123d !important;
} }
.theme-rose .text-primary-800 { .theme-rose .text-primary-800 {
color: #9f123d !important; color: #9f123d !important;
} }
.theme-rose .border-primary-800 { .theme-rose .border-primary-800 {
border-color: #9f123d !important; border-color: #9f123d !important;
} }
@@ -289,9 +307,11 @@ svg.lucide-icon:not(.custom) {
.theme-rose .bg-primary-900 { .theme-rose .bg-primary-900 {
background-color: #88133a !important; background-color: #88133a !important;
} }
.theme-rose .text-primary-900 { .theme-rose .text-primary-900 {
color: #88133a !important; color: #88133a !important;
} }
.theme-rose .border-primary-900 { .theme-rose .border-primary-900 {
border-color: #88133a !important; border-color: #88133a !important;
} }
@@ -302,9 +322,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-50 { .bg-primary-50 {
background-color: #eff6ff !important; background-color: #eff6ff !important;
} }
.text-primary-50 { .text-primary-50 {
color: #eff6ff !important; color: #eff6ff !important;
} }
.border-primary-50 { .border-primary-50 {
border-color: #eff6ff !important; border-color: #eff6ff !important;
} }
@@ -312,9 +334,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-100 { .bg-primary-100 {
background-color: #dbeafe !important; background-color: #dbeafe !important;
} }
.text-primary-100 { .text-primary-100 {
color: #dbeafe !important; color: #dbeafe !important;
} }
.border-primary-100 { .border-primary-100 {
border-color: #dbeafe !important; border-color: #dbeafe !important;
} }
@@ -322,9 +346,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-200 { .bg-primary-200 {
background-color: #bfdafe !important; background-color: #bfdafe !important;
} }
.text-primary-200 { .text-primary-200 {
color: #bfdafe !important; color: #bfdafe !important;
} }
.border-primary-200 { .border-primary-200 {
border-color: #bfdafe !important; border-color: #bfdafe !important;
} }
@@ -332,9 +358,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-300 { .bg-primary-300 {
background-color: #92c4fe !important; background-color: #92c4fe !important;
} }
.text-primary-300 { .text-primary-300 {
color: #92c4fe !important; color: #92c4fe !important;
} }
.border-primary-300 { .border-primary-300 {
border-color: #92c4fe !important; border-color: #92c4fe !important;
} }
@@ -342,9 +370,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-400 { .bg-primary-400 {
background-color: #5fa3fb !important; background-color: #5fa3fb !important;
} }
.text-primary-400 { .text-primary-400 {
color: #5fa3fb !important; color: #5fa3fb !important;
} }
.border-primary-400 { .border-primary-400 {
border-color: #5fa3fb !important; border-color: #5fa3fb !important;
} }
@@ -352,9 +382,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-500 { .bg-primary-500 {
background-color: #3a7ff7 !important; background-color: #3a7ff7 !important;
} }
.text-primary-500 { .text-primary-500 {
color: #3a7ff7 !important; color: #3a7ff7 !important;
} }
.border-primary-500 { .border-primary-500 {
border-color: #3a7ff7 !important; border-color: #3a7ff7 !important;
} }
@@ -362,9 +394,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-600 { .bg-primary-600 {
background-color: #2460ec !important; background-color: #2460ec !important;
} }
.text-primary-600 { .text-primary-600 {
color: #2460ec !important; color: #2460ec !important;
} }
.border-primary-600 { .border-primary-600 {
border-color: #2460ec !important; border-color: #2460ec !important;
} }
@@ -372,9 +406,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-700 { .bg-primary-700 {
background-color: #1d4ee2 !important; background-color: #1d4ee2 !important;
} }
.text-primary-700 { .text-primary-700 {
color: #1d4ee2 !important; color: #1d4ee2 !important;
} }
.border-primary-700 { .border-primary-700 {
border-color: #1d4ee2 !important; border-color: #1d4ee2 !important;
} }
@@ -382,9 +418,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-800 { .bg-primary-800 {
background-color: #1d3eb0 !important; background-color: #1d3eb0 !important;
} }
.text-primary-800 { .text-primary-800 {
color: #1d3eb0 !important; color: #1d3eb0 !important;
} }
.border-primary-800 { .border-primary-800 {
border-color: #1d3eb0 !important; border-color: #1d3eb0 !important;
} }
@@ -392,9 +430,11 @@ svg.lucide-icon:not(.custom) {
.bg-primary-900 { .bg-primary-900 {
background-color: #1d398b !important; background-color: #1d398b !important;
} }
.text-primary-900 { .text-primary-900 {
color: #1d398b !important; color: #1d398b !important;
} }
.border-primary-900 { .border-primary-900 {
border-color: #1d398b !important; border-color: #1d398b !important;
} }
@@ -405,9 +445,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-50 { .theme-green .bg-primary-50 {
background-color: #effef0 !important; background-color: #effef0 !important;
} }
.theme-green .text-primary-50 { .theme-green .text-primary-50 {
color: #effef0 !important; color: #effef0 !important;
} }
.theme-green .border-primary-50 { .theme-green .border-primary-50 {
border-color: #effef0 !important; border-color: #effef0 !important;
} }
@@ -415,9 +457,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-100 { .theme-green .bg-primary-100 {
background-color: #d9ffdc !important; background-color: #d9ffdc !important;
} }
.theme-green .text-primary-100 { .theme-green .text-primary-100 {
color: #d9ffdc !important; color: #d9ffdc !important;
} }
.theme-green .border-primary-100 { .theme-green .border-primary-100 {
border-color: #d9ffdc !important; border-color: #d9ffdc !important;
} }
@@ -425,9 +469,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-200 { .theme-green .bg-primary-200 {
background-color: #b6fcbc !important; background-color: #b6fcbc !important;
} }
.theme-green .text-primary-200 { .theme-green .text-primary-200 {
color: #b6fcbc !important; color: #b6fcbc !important;
} }
.theme-green .border-primary-200 { .theme-green .border-primary-200 {
border-color: #b6fcbc !important; border-color: #b6fcbc !important;
} }
@@ -435,9 +481,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-300 { .theme-green .bg-primary-300 {
background-color: #7df88a !important; background-color: #7df88a !important;
} }
.theme-green .text-primary-300 { .theme-green .text-primary-300 {
color: #7df88a !important; color: #7df88a !important;
} }
.theme-green .border-primary-300 { .theme-green .border-primary-300 {
border-color: #7df88a !important; border-color: #7df88a !important;
} }
@@ -445,9 +493,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-400 { .theme-green .bg-primary-400 {
background-color: #3deb51 !important; background-color: #3deb51 !important;
} }
.theme-green .text-primary-400 { .theme-green .text-primary-400 {
color: #3deb51 !important; color: #3deb51 !important;
} }
.theme-green .border-primary-400 { .theme-green .border-primary-400 {
border-color: #3deb51 !important; border-color: #3deb51 !important;
} }
@@ -455,9 +505,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-500 { .theme-green .bg-primary-500 {
background-color: #12bf26 !important; background-color: #12bf26 !important;
} }
.theme-green .text-primary-500 { .theme-green .text-primary-500 {
color: #12bf26 !important; color: #12bf26 !important;
} }
.theme-green .border-primary-500 { .theme-green .border-primary-500 {
border-color: #12bf26 !important; border-color: #12bf26 !important;
} }
@@ -465,9 +517,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-600 { .theme-green .bg-primary-600 {
background-color: #0aaf1d !important; background-color: #0aaf1d !important;
} }
.theme-green .text-primary-600 { .theme-green .text-primary-600 {
color: #0aaf1d !important; color: #0aaf1d !important;
} }
.theme-green .border-primary-600 { .theme-green .border-primary-600 {
border-color: #0aaf1d !important; border-color: #0aaf1d !important;
} }
@@ -475,9 +529,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-700 { .theme-green .bg-primary-700 {
background-color: #0c891b !important; background-color: #0c891b !important;
} }
.theme-green .text-primary-700 { .theme-green .text-primary-700 {
color: #0c891b !important; color: #0c891b !important;
} }
.theme-green .border-primary-700 { .theme-green .border-primary-700 {
border-color: #0c891b !important; border-color: #0c891b !important;
} }
@@ -485,9 +541,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-800 { .theme-green .bg-primary-800 {
background-color: #0f6c1b !important; background-color: #0f6c1b !important;
} }
.theme-green .text-primary-800 { .theme-green .text-primary-800 {
color: #0f6c1b !important; color: #0f6c1b !important;
} }
.theme-green .border-primary-800 { .theme-green .border-primary-800 {
border-color: #0f6c1b !important; border-color: #0f6c1b !important;
} }
@@ -495,9 +553,11 @@ svg.lucide-icon:not(.custom) {
.theme-green .bg-primary-900 { .theme-green .bg-primary-900 {
background-color: #0f581a !important; background-color: #0f581a !important;
} }
.theme-green .text-primary-900 { .theme-green .text-primary-900 {
color: #0f581a !important; color: #0f581a !important;
} }
.theme-green .border-primary-900 { .theme-green .border-primary-900 {
border-color: #0f581a !important; border-color: #0f581a !important;
} }
@@ -508,9 +568,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-50 { .theme-orange .bg-primary-50 {
background-color: #fff8ec !important; background-color: #fff8ec !important;
} }
.theme-orange .text-primary-50 { .theme-orange .text-primary-50 {
color: #fff8ec !important; color: #fff8ec !important;
} }
.theme-orange .border-primary-50 { .theme-orange .border-primary-50 {
border-color: #fff8ec !important; border-color: #fff8ec !important;
} }
@@ -518,9 +580,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-100 { .theme-orange .bg-primary-100 {
background-color: #fff0d3 !important; background-color: #fff0d3 !important;
} }
.theme-orange .text-primary-100 { .theme-orange .text-primary-100 {
color: #fff0d3 !important; color: #fff0d3 !important;
} }
.theme-orange .border-primary-100 { .theme-orange .border-primary-100 {
border-color: #fff0d3 !important; border-color: #fff0d3 !important;
} }
@@ -528,9 +592,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-200 { .theme-orange .bg-primary-200 {
background-color: #ffdca5 !important; background-color: #ffdca5 !important;
} }
.theme-orange .text-primary-200 { .theme-orange .text-primary-200 {
color: #ffdca5 !important; color: #ffdca5 !important;
} }
.theme-orange .border-primary-200 { .theme-orange .border-primary-200 {
border-color: #ffdca5 !important; border-color: #ffdca5 !important;
} }
@@ -538,9 +604,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-300 { .theme-orange .bg-primary-300 {
background-color: #ffc26d !important; background-color: #ffc26d !important;
} }
.theme-orange .text-primary-300 { .theme-orange .text-primary-300 {
color: #ffc26d !important; color: #ffc26d !important;
} }
.theme-orange .border-primary-300 { .theme-orange .border-primary-300 {
border-color: #ffc26d !important; border-color: #ffc26d !important;
} }
@@ -548,9 +616,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-400 { .theme-orange .bg-primary-400 {
background-color: #ff9d32 !important; background-color: #ff9d32 !important;
} }
.theme-orange .text-primary-400 { .theme-orange .text-primary-400 {
color: #ff9d32 !important; color: #ff9d32 !important;
} }
.theme-orange .border-primary-400 { .theme-orange .border-primary-400 {
border-color: #ff9d32 !important; border-color: #ff9d32 !important;
} }
@@ -558,9 +628,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-500 { .theme-orange .bg-primary-500 {
background-color: #ff7f0a !important; background-color: #ff7f0a !important;
} }
.theme-orange .text-primary-500 { .theme-orange .text-primary-500 {
color: #ff7f0a !important; color: #ff7f0a !important;
} }
.theme-orange .border-primary-500 { .theme-orange .border-primary-500 {
border-color: #ff7f0a !important; border-color: #ff7f0a !important;
} }
@@ -568,9 +640,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-600 { .theme-orange .bg-primary-600 {
background-color: #ff6600 !important; background-color: #ff6600 !important;
} }
.theme-orange .text-primary-600 { .theme-orange .text-primary-600 {
color: #ff6600 !important; color: #ff6600 !important;
} }
.theme-orange .border-primary-600 { .theme-orange .border-primary-600 {
border-color: #ff6600 !important; border-color: #ff6600 !important;
} }
@@ -578,9 +652,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-700 { .theme-orange .bg-primary-700 {
background-color: #cc4902 !important; background-color: #cc4902 !important;
} }
.theme-orange .text-primary-700 { .theme-orange .text-primary-700 {
color: #cc4902 !important; color: #cc4902 !important;
} }
.theme-orange .border-primary-700 { .theme-orange .border-primary-700 {
border-color: #cc4902 !important; border-color: #cc4902 !important;
} }
@@ -588,9 +664,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-800 { .theme-orange .bg-primary-800 {
background-color: #a1390b !important; background-color: #a1390b !important;
} }
.theme-orange .text-primary-800 { .theme-orange .text-primary-800 {
color: #a1390b !important; color: #a1390b !important;
} }
.theme-orange .border-primary-800 { .theme-orange .border-primary-800 {
border-color: #a1390b !important; border-color: #a1390b !important;
} }
@@ -598,9 +676,11 @@ svg.lucide-icon:not(.custom) {
.theme-orange .bg-primary-900 { .theme-orange .bg-primary-900 {
background-color: #82310c !important; background-color: #82310c !important;
} }
.theme-orange .text-primary-900 { .theme-orange .text-primary-900 {
color: #82310c !important; color: #82310c !important;
} }
.theme-orange .border-primary-900 { .theme-orange .border-primary-900 {
border-color: #82310c !important; border-color: #82310c !important;
} }
@@ -611,9 +691,11 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-50 { .theme-violet .bg-primary-50 {
background-color: #fff2fe !important; background-color: #fff2fe !important;
} }
.theme-violet .text-primary-50 { .theme-violet .text-primary-50 {
color: #fff2fe !important; color: #fff2fe !important;
} }
.theme-violet .border-primary-50 { .theme-violet .border-primary-50 {
border-color: #fff2fe !important; border-color: #fff2fe !important;
} }
@@ -621,9 +703,11 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-100 { .theme-violet .bg-primary-100 {
background-color: #ffe3fe !important; background-color: #ffe3fe !important;
} }
.theme-violet .text-primary-100 { .theme-violet .text-primary-100 {
color: #ffe3fe !important; color: #ffe3fe !important;
} }
.theme-violet .border-primary-100 { .theme-violet .border-primary-100 {
border-color: #ffe3fe !important; border-color: #ffe3fe !important;
} }
@@ -631,9 +715,11 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-200 { .theme-violet .bg-primary-200 {
background-color: #ffc6fe !important; background-color: #ffc6fe !important;
} }
.theme-violet .text-primary-200 { .theme-violet .text-primary-200 {
color: #ffc6fe !important; color: #ffc6fe !important;
} }
.theme-violet .border-primary-200 { .theme-violet .border-primary-200 {
border-color: #ffc6fe !important; border-color: #ffc6fe !important;
} }
@@ -641,9 +727,11 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-300 { .theme-violet .bg-primary-300 {
background-color: #ff99f7 !important; background-color: #ff99f7 !important;
} }
.theme-violet .text-primary-300 { .theme-violet .text-primary-300 {
color: #ff99f7 !important; color: #ff99f7 !important;
} }
.theme-violet .border-primary-300 { .theme-violet .border-primary-300 {
border-color: #ff99f7 !important; border-color: #ff99f7 !important;
} }
@@ -651,9 +739,11 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-400 { .theme-violet .bg-primary-400 {
background-color: #ff5df2 !important; background-color: #ff5df2 !important;
} }
.theme-violet .text-primary-400 { .theme-violet .text-primary-400 {
color: #ff5df2 !important; color: #ff5df2 !important;
} }
.theme-violet .border-primary-400 { .theme-violet .border-primary-400 {
border-color: #ff5df2 !important; border-color: #ff5df2 !important;
} }
@@ -661,9 +751,11 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-500 { .theme-violet .bg-primary-500 {
background-color: #ff21f5 !important; background-color: #ff21f5 !important;
} }
.theme-violet .text-primary-500 { .theme-violet .text-primary-500 {
color: #ff21f5 !important; color: #ff21f5 !important;
} }
.theme-violet .border-primary-500 { .theme-violet .border-primary-500 {
border-color: #ff21f5 !important; border-color: #ff21f5 !important;
} }
@@ -671,9 +763,11 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-600 { .theme-violet .bg-primary-600 {
background-color: #fb00ff !important; background-color: #fb00ff !important;
} }
.theme-violet .text-primary-600 { .theme-violet .text-primary-600 {
color: #fb00ff !important; color: #fb00ff !important;
} }
.theme-violet .border-primary-600 { .theme-violet .border-primary-600 {
border-color: #fb00ff !important; border-color: #fb00ff !important;
} }
@@ -681,9 +775,11 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-700 { .theme-violet .bg-primary-700 {
background-color: #cf00ce !important; background-color: #cf00ce !important;
} }
.theme-violet .text-primary-700 { .theme-violet .text-primary-700 {
color: #cf00ce !important; color: #cf00ce !important;
} }
.theme-violet .border-primary-700 { .theme-violet .border-primary-700 {
border-color: #cf00ce !important; border-color: #cf00ce !important;
} }
@@ -691,9 +787,11 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-800 { .theme-violet .bg-primary-800 {
background-color: #a900a6 !important; background-color: #a900a6 !important;
} }
.theme-violet .text-primary-800 { .theme-violet .text-primary-800 {
color: #a900a6 !important; color: #a900a6 !important;
} }
.theme-violet .border-primary-800 { .theme-violet .border-primary-800 {
border-color: #a900a6 !important; border-color: #a900a6 !important;
} }
@@ -701,24 +799,26 @@ svg.lucide-icon:not(.custom) {
.theme-violet .bg-primary-900 { .theme-violet .bg-primary-900 {
background-color: #890684 !important; background-color: #890684 !important;
} }
.theme-violet .text-primary-900 { .theme-violet .text-primary-900 {
color: #890684 !important; color: #890684 !important;
} }
.theme-violet .border-primary-900 { .theme-violet .border-primary-900 {
border-color: #890684 !important; border-color: #890684 !important;
} }
/* Tailwind v4 Custom Animations and Keyframes */ /* Tailwind v4 Custom Animations and Keyframes */
@theme { @theme {
--animate-shake-announcement: shakeAnnouncement 0.5s ease-in-out; --animate-NEWS_FEED: shakeAnnouncement 0.5s ease-in-out;
--animate-flip-book: flipBook 0.6s ease-in-out; --animate-SECTION: flipBook 0.6s ease-in-out;
--animate-scroll-unroll: scrollUnroll 0.8s ease-out; --animate-LESSONS: scrollUnroll 0.8s ease-out;
--animate-check-items: attendanceCount 0.8s ease-in-out; --animate-ATTENDANCE: attendanceCount 0.8s ease-in-out;
--animate-task-complete: taskComplete 0.6s ease-in-out; --animate-SUBMISSIONS: taskComplete 0.6s ease-in-out;
--animate-grade-glow: gradeGlow 1s ease-in-out; --animate-MARKS: gradeGlow 1s ease-in-out;
--animate-people-wave: peopleWave 0.8s ease-in-out; --animate-PEOPLE: peopleWave 0.8s ease-in-out;
--animate-chart-grow: dataScan 1s ease-in-out; --animate-ANALYTICS: dataScan 1s ease-in-out;
--animate-screen-glow: screenGlow 1s ease-in-out; --animate-LANDING_PAGE: screenGlow 1s ease-in-out;
--animate-library-expand: libraryExpand 0.7s ease-out; --animate-library-expand: libraryExpand 0.7s ease-out;
--animate-template-morph: templateMorph 0.6s ease-in-out; --animate-template-morph: templateMorph 0.6s ease-in-out;
--animate-ripple: communityRipple 0.9s ease-out; --animate-ripple: communityRipple 0.9s ease-out;
@@ -728,10 +828,12 @@ svg.lucide-icon:not(.custom) {
} }
@keyframes shakeAnnouncement { @keyframes shakeAnnouncement {
0%, 0%,
100% { 100% {
transform: translateX(0); transform: translateX(0);
} }
10%, 10%,
30%, 30%,
50%, 50%,
@@ -739,6 +841,7 @@ svg.lucide-icon:not(.custom) {
90% { 90% {
transform: translateX(-2px); transform: translateX(-2px);
} }
20%, 20%,
40%, 40%,
60%, 60%,
@@ -751,9 +854,11 @@ svg.lucide-icon:not(.custom) {
0% { 0% {
transform: rotateY(0deg); transform: rotateY(0deg);
} }
50% { 50% {
transform: rotateY(90deg); transform: rotateY(90deg);
} }
100% { 100% {
transform: rotateY(0deg); transform: rotateY(0deg);
} }
@@ -764,10 +869,12 @@ svg.lucide-icon:not(.custom) {
transform: scaleY(0) translateY(-50%); transform: scaleY(0) translateY(-50%);
opacity: 0.5; opacity: 0.5;
} }
50% { 50% {
transform: scaleY(0.8) translateY(-25%); transform: scaleY(0.8) translateY(-25%);
opacity: 0.8; opacity: 0.8;
} }
100% { 100% {
transform: scaleY(1) translateY(0); transform: scaleY(1) translateY(0);
opacity: 1; opacity: 1;
@@ -779,22 +886,27 @@ svg.lucide-icon:not(.custom) {
transform: scale(1); transform: scale(1);
opacity: 0.8; opacity: 0.8;
} }
20% { 20% {
transform: scale(1.1); transform: scale(1.1);
opacity: 1; opacity: 1;
} }
40% { 40% {
transform: scale(1.05); transform: scale(1.05);
opacity: 0.9; opacity: 0.9;
} }
60% { 60% {
transform: scale(1.15); transform: scale(1.15);
opacity: 1; opacity: 1;
} }
80% { 80% {
transform: scale(1.02); transform: scale(1.02);
opacity: 0.95; opacity: 0.95;
} }
100% { 100% {
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
@@ -805,26 +917,32 @@ svg.lucide-icon:not(.custom) {
0% { 0% {
transform: scale(1) rotate(0deg); transform: scale(1) rotate(0deg);
} }
25% { 25% {
transform: scale(1.1) rotate(-5deg); transform: scale(1.1) rotate(-5deg);
} }
50% { 50% {
transform: scale(1.2) rotate(5deg); transform: scale(1.2) rotate(5deg);
} }
75% { 75% {
transform: scale(1.1) rotate(-2deg); transform: scale(1.1) rotate(-2deg);
} }
100% { 100% {
transform: scale(1) rotate(0deg); transform: scale(1) rotate(0deg);
} }
} }
@keyframes gradeGlow { @keyframes gradeGlow {
0%, 0%,
100% { 100% {
transform: scale(1); transform: scale(1);
filter: brightness(1) drop-shadow(0 0 0 rgba(34, 197, 94, 0)); filter: brightness(1) drop-shadow(0 0 0 rgba(34, 197, 94, 0));
} }
50% { 50% {
transform: scale(1.15); transform: scale(1.15);
filter: brightness(1.3) drop-shadow(0 0 8px rgba(34, 197, 94, 0.6)); 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 { @keyframes peopleWave {
0%, 0%,
100% { 100% {
transform: rotate(0deg) scale(1); transform: rotate(0deg) scale(1);
} }
25% { 25% {
transform: rotate(-5deg) scale(1.05); transform: rotate(-5deg) scale(1.05);
} }
50% { 50% {
transform: rotate(5deg) scale(1.1); transform: rotate(5deg) scale(1.1);
} }
75% { 75% {
transform: rotate(-3deg) scale(1.05); transform: rotate(-3deg) scale(1.05);
} }
@@ -852,18 +974,22 @@ svg.lucide-icon:not(.custom) {
transform: scale(1); transform: scale(1);
filter: brightness(1) drop-shadow(0 0 0 rgba(99, 102, 241, 0)); filter: brightness(1) drop-shadow(0 0 0 rgba(99, 102, 241, 0));
} }
25% { 25% {
transform: scale(1.05); transform: scale(1.05);
filter: brightness(1.1) drop-shadow(0 0 4px rgba(99, 102, 241, 0.4)); filter: brightness(1.1) drop-shadow(0 0 4px rgba(99, 102, 241, 0.4));
} }
50% { 50% {
transform: scale(1.1); transform: scale(1.1);
filter: brightness(1.2) drop-shadow(0 0 8px rgba(99, 102, 241, 0.6)); filter: brightness(1.2) drop-shadow(0 0 8px rgba(99, 102, 241, 0.6));
} }
75% { 75% {
transform: scale(1.15) rotate(2deg); transform: scale(1.15) rotate(2deg);
filter: brightness(1.3) drop-shadow(0 0 12px rgba(99, 102, 241, 0.8)); filter: brightness(1.3) drop-shadow(0 0 12px rgba(99, 102, 241, 0.8));
} }
100% { 100% {
transform: scale(1); transform: scale(1);
filter: brightness(1) drop-shadow(0 0 0 rgba(99, 102, 241, 0)); filter: brightness(1) drop-shadow(0 0 0 rgba(99, 102, 241, 0));
@@ -871,11 +997,13 @@ svg.lucide-icon:not(.custom) {
} }
@keyframes screenGlow { @keyframes screenGlow {
0%, 0%,
100% { 100% {
transform: scale(1); transform: scale(1);
filter: brightness(1) drop-shadow(0 0 0 rgba(59, 130, 246, 0)); filter: brightness(1) drop-shadow(0 0 0 rgba(59, 130, 246, 0));
} }
50% { 50% {
transform: scale(1.1); transform: scale(1.1);
filter: brightness(1.2) drop-shadow(0 0 6px rgba(59, 130, 246, 0.5)); 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); transform: translateY(-100%) scale(0.8);
opacity: 0; opacity: 0;
} }
30% { 30% {
transform: translateY(-20%) scale(0.9); transform: translateY(-20%) scale(0.9);
opacity: 0.7; opacity: 0.7;
} }
60% { 60% {
transform: translateY(5%) scale(1.05); transform: translateY(5%) scale(1.05);
opacity: 1; opacity: 1;
} }
80% { 80% {
transform: translateY(-2%) scale(1.02); transform: translateY(-2%) scale(1.02);
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: translateY(0%) scale(1); transform: translateY(0%) scale(1);
opacity: 1; opacity: 1;
@@ -909,15 +1041,19 @@ svg.lucide-icon:not(.custom) {
0% { 0% {
transform: scale(1) rotate(0deg); transform: scale(1) rotate(0deg);
} }
25% { 25% {
transform: scale(1.05) rotate(-2deg) skewX(2deg); transform: scale(1.05) rotate(-2deg) skewX(2deg);
} }
50% { 50% {
transform: scale(1.1) rotate(2deg) skewX(-2deg); transform: scale(1.1) rotate(2deg) skewX(-2deg);
} }
75% { 75% {
transform: scale(1.05) rotate(-1deg) skewX(1deg); transform: scale(1.05) rotate(-1deg) skewX(1deg);
} }
100% { 100% {
transform: scale(1) rotate(0deg) skewX(0deg); transform: scale(1) rotate(0deg) skewX(0deg);
} }
@@ -928,18 +1064,22 @@ svg.lucide-icon:not(.custom) {
transform: translateX(-100%) scale(0.8); transform: translateX(-100%) scale(0.8);
opacity: 0; opacity: 0;
} }
30% { 30% {
transform: translateX(-20%) scale(0.9); transform: translateX(-20%) scale(0.9);
opacity: 0.7; opacity: 0.7;
} }
60% { 60% {
transform: translateX(5%) scale(1.05); transform: translateX(5%) scale(1.05);
opacity: 1; opacity: 1;
} }
80% { 80% {
transform: translateX(-2%) scale(1.02); transform: translateX(-2%) scale(1.02);
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: translateX(0%) scale(1); transform: translateX(0%) scale(1);
opacity: 1; opacity: 1;
@@ -950,18 +1090,23 @@ svg.lucide-icon:not(.custom) {
0% { 0% {
transform: scale(1) rotate(0deg); transform: scale(1) rotate(0deg);
} }
20% { 20% {
transform: scale(1.05) rotate(72deg); transform: scale(1.05) rotate(72deg);
} }
40% { 40% {
transform: scale(1.1) rotate(144deg); transform: scale(1.1) rotate(144deg);
} }
60% { 60% {
transform: scale(1.15) rotate(216deg); transform: scale(1.15) rotate(216deg);
} }
80% { 80% {
transform: scale(1.1) rotate(288deg); transform: scale(1.1) rotate(288deg);
} }
100% { 100% {
transform: scale(1) rotate(360deg); transform: scale(1) rotate(360deg);
} }
@@ -972,18 +1117,22 @@ svg.lucide-icon:not(.custom) {
transform: scale(1); transform: scale(1);
filter: brightness(1) drop-shadow(0 0 0 rgba(147, 51, 234, 0)); filter: brightness(1) drop-shadow(0 0 0 rgba(147, 51, 234, 0));
} }
25% { 25% {
transform: scale(0.95) scaleX(1.1); transform: scale(0.95) scaleX(1.1);
filter: brightness(1.05) drop-shadow(0 0 2px rgba(147, 51, 234, 0.3)); filter: brightness(1.05) drop-shadow(0 0 2px rgba(147, 51, 234, 0.3));
} }
50% { 50% {
transform: scale(1.1) scaleX(0.9); transform: scale(1.1) scaleX(0.9);
filter: brightness(1.1) drop-shadow(0 0 4px rgba(147, 51, 234, 0.5)); filter: brightness(1.1) drop-shadow(0 0 4px rgba(147, 51, 234, 0.5));
} }
75% { 75% {
transform: scale(1.05) scaleX(1.05); transform: scale(1.05) scaleX(1.05);
filter: brightness(1.05) drop-shadow(0 0 2px rgba(147, 51, 234, 0.3)); filter: brightness(1.05) drop-shadow(0 0 2px rgba(147, 51, 234, 0.3));
} }
100% { 100% {
transform: scale(1) scaleX(1); transform: scale(1) scaleX(1);
filter: brightness(1) drop-shadow(0 0 0 rgba(147, 51, 234, 0)); filter: brightness(1) drop-shadow(0 0 0 rgba(147, 51, 234, 0));
@@ -994,25 +1143,32 @@ svg.lucide-icon:not(.custom) {
0% { 0% {
transform: scale(1) rotate(0deg); transform: scale(1) rotate(0deg);
} }
15% { 15% {
transform: scale(1.02) rotate(15deg); transform: scale(1.02) rotate(15deg);
} }
30% { 30% {
transform: scale(1.04) rotate(-10deg); transform: scale(1.04) rotate(-10deg);
} }
45% { 45% {
transform: scale(1.06) rotate(20deg); transform: scale(1.06) rotate(20deg);
} }
60% { 60% {
transform: scale(1.08) rotate(-15deg); transform: scale(1.08) rotate(-15deg);
} }
75% { 75% {
transform: scale(1.06) rotate(10deg); transform: scale(1.06) rotate(10deg);
} }
90% { 90% {
transform: scale(1.02) rotate(-5deg); transform: scale(1.02) rotate(-5deg);
} }
100% { 100% {
transform: scale(1) rotate(0deg); transform: scale(1) rotate(0deg);
} }
} }
@@ -2,11 +2,8 @@
// import { preventDefault } from '$lib/utils/functions/svelte'; // import { preventDefault } from '$lib/utils/functions/svelte';
// import { writable } from 'svelte/store'; // import { writable } from 'svelte/store';
// // import { useCompletion } from 'ai/svelte'; // // 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 WandSparklesIcon from '@lucide/svelte/icons/wand-sparkles';
// import { IconButton } from '$lib/components/IconButton'; // import { IconButton } from '$lib/components/IconButton';
// import { Loading } from 'carbon-components-svelte';
// import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte'; // import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
// import { t } from '$lib/utils/functions/translations'; // 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"> <p class="text-center text-sm font-medium text-gray-600 md:text-left dark:text-gray-200">
{activity.title} {activity.title}
</p> </p>
<p class="text-center text-2xl font-bold md:text-left"> <p class="text-center text-2xl md:text-left">
{activity.percentage} {activity.percentage}
{#if !activity.hidePercentage} {#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"> <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" /> <Avatar src={user.avatarUrl} name={user.fullName} width="w-16" height="h-16" />
<div class="flex flex-col space-y-2"> <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} {user.fullName}
</p> </p>
@@ -1,12 +1,12 @@
<script> <script>
import { SkeletonPlaceholder } from 'carbon-components-svelte'; import { Skeleton } from '@cio/ui/base/skeleton';
</script> </script>
<div class="mx-auto max-w-4xl pt-14"> <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"> <div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
<SkeletonPlaceholder style="width: 270px; height: 10rem;" /> <Skeleton class="h-40 w-[270px]" />
<SkeletonPlaceholder style="width: 270px; height: 10rem;" /> <Skeleton class="h-40 w-[270px]" />
<SkeletonPlaceholder style="width: 270px; height: 10rem;" /> <Skeleton class="h-40 w-[270px]" />
</div> </div>
</div> </div>
@@ -19,5 +19,5 @@
</script> </script>
<IconButton {onClick} {color} {contained} {size}> <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> </IconButton>
@@ -6,5 +6,5 @@
</script> </script>
<IconButton {onClick}> <IconButton {onClick}>
<XIcon size={16} class="carbon-icon dark:text-white" /> <XIcon size={16} class="dark:text-white" />
</IconButton> </IconButton>
@@ -12,7 +12,7 @@
<p <p
class="{className} {size === 'sm' ? 'px-1 py-1' : 'px-2 py-2'} org-avatar {className.includes('bg-') 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} {value}
</p> </p>
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { SkeletonPlaceholder } from 'carbon-components-svelte'; import { Skeleton } from '@cio/ui/base/skeleton';
</script> </script>
<div class="min-h-screen"> <div class="min-h-screen">
@@ -10,11 +10,12 @@
{#each Array(4) as _} {#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="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"> <div class="flex items-center space-x-4">
<SkeletonPlaceholder style="width: 48px; height: 48px;" /> <Skeleton class="h-[48px] w-[48px]" />
<div class="flex-1"> <div class="flex-1">
<SkeletonPlaceholder style="width: 80px; height: 16px; margin-bottom: 8px;" /> <Skeleton class="h-[48px] w-[48px]" />
<SkeletonPlaceholder style="width: 40px; height: 24px; margin-bottom: 4px;" /> <Skeleton class="h-[16px] w-[80px] mb-2" />
<SkeletonPlaceholder style="width: 60px; height: 12px;" /> <Skeleton class="h-[24px] w-[40px] mb-1" />
<Skeleton class="h-[12px] w-[60px]" />
</div> </div>
</div> </div>
</div> </div>
@@ -25,10 +26,10 @@
<!-- Charts Section Skeleton --> <!-- Charts Section Skeleton -->
<div class="mb-12"> <div class="mb-12">
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800"> <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"> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<SkeletonPlaceholder style="width: 100%; height: 300px;" /> <Skeleton class="w-full h-[300px]" />
<SkeletonPlaceholder style="width: 100%; height: 300px;" /> <Skeleton class="w-full h-[300px]" />
</div> </div>
</div> </div>
</div> </div>
@@ -36,22 +37,22 @@
<!-- Students Table Skeleton --> <!-- Students Table Skeleton -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-neutral-700 dark:bg-neutral-800"> <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"> <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>
<div class="p-6"> <div class="p-6">
<div class="space-y-4"> <div class="space-y-4">
{#each Array(5) as _} {#each Array(5) as _}
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<SkeletonPlaceholder style="width: 32px; height: 32px;" /> <Skeleton class="h-[32px] w-[32px]" />
<div class="flex-1"> <div class="flex-1">
<SkeletonPlaceholder style="width: 150px; height: 16px; margin-bottom: 4px;" /> <Skeleton class="h-[16px] w-[150px] mb-1" />
<SkeletonPlaceholder style="width: 200px; height: 12px;" /> <Skeleton class="h-[12px] w-[200px]" />
</div> </div>
<SkeletonPlaceholder style="width: 80px; height: 16px;" /> <Skeleton class="h-[16px] w-[80px]" />
<SkeletonPlaceholder style="width: 60px; height: 16px;" /> <Skeleton class="h-[16px] w-[60px]" />
<SkeletonPlaceholder style="width: 40px; height: 20px;" /> <Skeleton class="h-[20px] w-[40px]" />
<SkeletonPlaceholder style="width: 80px; height: 12px;" /> <Skeleton class="h-[12px] w-[80px]" />
<SkeletonPlaceholder style="width: 100px; height: 32px;" /> <Skeleton class="h-[32px] w-[100px]" />
</div> </div>
{/each} {/each}
</div> </div>
@@ -1,13 +1,16 @@
<script lang="ts"> <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 { 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 { 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 { interface Props {
students?: StudentOverview[]; students?: StudentOverview[];
} }
@@ -32,10 +35,6 @@
goto(`/courses/${$course.id}/people/${student.id}?back=/courses/${$course.id}/analytics`); 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 // Reset to first page when students array changes
$effect(() => { $effect(() => {
if (students.length > 0 && currentPage > totalPages) { if (students.length > 0 && currentPage > totalPages) {
@@ -128,12 +127,15 @@
</span> </span>
</td> </td>
<td class="min-w-[120px] whitespace-nowrap px-4 py-3"> <td class="min-w-[120px] whitespace-nowrap px-4 py-3">
<Tag <Badge
type={student.averageGrade >= 80 ? 'green' : student.averageGrade >= 60 ? 'blue' : 'red'} class={student.averageGrade >= 80
size="sm" ? 'bg-green-600'
: student.averageGrade >= 60
? 'bg-blue-600'
: 'bg-red-600'}
> >
{student.averageGrade}% {student.averageGrade}%
</Tag> </Badge>
</td> </td>
<td class="min-w-[120px] whitespace-nowrap px-4 py-3"> <td class="min-w-[120px] whitespace-nowrap px-4 py-3">
<span class="text-sm text-gray-500 dark:text-gray-400"> <span class="text-sm text-gray-500 dark:text-gray-400">
@@ -165,7 +167,37 @@
total: students.length total: students.length
})} })}
</div> </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> </div>
{/if} {/if}
{/if} {/if}
@@ -1,25 +1,28 @@
<script> <script>
import { RadioButtonGroup, RadioButton, Toggle } from 'carbon-components-svelte';
import ZapIcon from '@lucide/svelte/icons/zap';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { updateCourse } from '$lib/utils/services/courses'; import { Label } from '@cio/ui/base/label';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import { Switch } from '@cio/ui/base/switch';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import ZapIcon from '@lucide/svelte/icons/zap';
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte'; import * as RadioGroup from '@cio/ui/base/radio-group';
import TextArea from '$lib/components/Form/TextArea.svelte';
import Professional from './templates/Professional.svelte';
import Plain from './templates/Plain.svelte';
import { course } from '$lib/components/Course/store'; 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 { 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 { 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 { 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 studentNamePlaceholder = 'Name of student';
const themes = [ const themes = [
'professional', 'professional',
@@ -94,16 +97,20 @@
<p class="my-4 text-xs font-normal dark:text-gray-100"> <p class="my-4 text-xs font-normal dark:text-gray-100">
{$t('course.navItem.certificates.theme')} {$t('course.navItem.certificates.theme')}
</p> </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"> <div class="flex flex-wrap justify-between gap-y-5">
{#each themes as theme} {#each themes as theme}
<div class="mr-3 flex"> <div class="mr-3 flex items-start space-x-2">
<RadioButton value={theme} /> <RadioGroup.Item value={theme} id={theme} />
<img src={`/images/certificate_theme_${theme}.png`} alt="themes" class="h-[82px] w-[110px]" /> <Label for={theme} class="cursor-pointer">
<img src={`/images/certificate_theme_${theme}.png`} alt="themes" class="h-[82px] w-[110px]" />
</Label>
</div> </div>
{/each} {/each}
</div> </div>
</RadioButtonGroup> </RadioGroup.Root>
<div> <div>
<p class="my-2 text-xs font-normal text-black dark:text-gray-100"> <p class="my-2 text-xs font-normal text-black dark:text-gray-100">
{$t('course.navItem.certificates.logo')} {$t('course.navItem.certificates.logo')}
@@ -135,18 +142,16 @@
helperMessage={helperText} helperMessage={helperText}
/> />
</span> </span>
<Toggle <div class="my-4 flex items-center space-x-2">
labelText={$t('course.navItem.certificates.allow')} <Switch
bind:toggled={$course.is_certificate_downloadable} id="certificate-downloadable"
class="my-4" bind:checked={$course.is_certificate_downloadable}
size="sm" disabled={$isFreePlan}
disabled={$isFreePlan} />
> <Label for="certificate-downloadable" class="text-sm font-medium dark:text-gray-100">
<span slot="labelA" style={$globalStore.isDark ? 'color: white' : 'color: #161616'} {$t('course.navItem.certificates.allow')}
>{$t('generic.locked')}</span </Label>
> </div>
<span slot="labelB" style="color: green">{$t('generic.unlocked')}</span>
</Toggle>
</div> </div>
</section> </section>
<section class="flex w-full items-center justify-center rounded-md bg-gray-100 lg:w-3/5 dark:bg-neutral-800"> <section class="flex w-full items-center justify-center rounded-md bg-gray-100 lg:w-3/5 dark:bg-neutral-800">
@@ -1,16 +1,16 @@
<script> <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 { preventDefault } from '$lib/utils/functions/svelte';
import { Toggle } from 'carbon-components-svelte';
import Modal from '$lib/components/Modal/index.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 TextArea from '$lib/components/Form/TextArea.svelte';
import TextField from '$lib/components/Form/TextField.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) => { let isAutomatic = $state(false);
console.log(e);
};
const issueCertificate = () => { const issueCertificate = () => {
resetForm(); resetForm();
@@ -26,15 +26,10 @@
> >
<main> <main>
<div> <div>
<Toggle <div class="mb-4 flex items-center space-x-2">
labelText="Automatically send certificates on completion" <Switch id="auto-certificate" bind:checked={isAutomatic} />
on:toggle={(e) => onToggle(e)} <Label for="auto-certificate" class="text-sm font-medium">Automatic</Label>
size="sm" </div>
class="mb-4"
>
<span slot="labelA" style="color: #161616">Automatic</span>
<span slot="labelB" style="color: green">Automatic</span>
</Toggle>
<p class="my-4 text-sm font-medium"> <p class="my-4 text-sm font-medium">
If you set this as automatic, certificates will be issued after the learner completes the course. If you set this as automatic, certificates will be issued after the learner completes the course.
</p> </p>
@@ -1,23 +1,51 @@
<script> <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 { CertificateReportData, students } from './mockData';
import {
Search,
Checkbox,
StructuredList,
StructuredListHead,
StructuredListRow,
StructuredListCell,
StructuredListBody,
Pagination
} from 'carbon-components-svelte';
let searchValue = $state(''); 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> </script>
<section class="-ml-4 -mr-4 mb-10 flex flex-wrap items-start gap-2"> <section class="-ml-4 -mr-4 mb-10 flex flex-wrap items-start gap-2">
{#each CertificateReportData as data (data.id)} {#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> <p class="text-xs font-normal">{data.title}</p>
<bold class="text-2xl font-medium">{data.value}</bold> <bold class="text-2xl font-medium">{data.value}</bold>
</div> </div>
@@ -25,61 +53,80 @@
</section> </section>
<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> <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> </div>
</section> </section>
<section class="mt-14 border-t border-gray-300"> <section class="mt-14">
<StructuredList> <div class="rounded-md border dark:border-neutral-600">
<StructuredListHead class="bg-primary-50 py-0"> <Table.Root>
<StructuredListRow head class="py-0"> <Table.Header>
<StructuredListCell head class="py-0"> <Table.Row>
<div class="flex-3 text-primary-600 flex w-full flex-row items-center justify-start text-sm font-medium"> <Table.Head class="w-[50px]">
<Checkbox bind:checked /> <Checkbox checked={selectAll} onCheckedChange={toggleSelectAll} />
<p class="text-primary-900 w-full text-start text-sm font-medium">Name of student</p> </Table.Head>
</div> <Table.Head>Name of student</Table.Head>
</StructuredListCell> <Table.Head>Date issued</Table.Head>
<StructuredListCell head class="py-0"> </Table.Row>
<div class="flex-2 text-primary-900 w-full items-center text-sm font-medium"> </Table.Header>
<p class="pt-1 align-middle text-sm font-medium">Date issued</p> <Table.Body>
<p class="opacity-0">personalized</p> {#each paginatedStudents as student (student.id)}
</div> <Table.Row>
</StructuredListCell> <Table.Cell>
</StructuredListRow> <Checkbox checked={selectedStudents.has(student.id)} onCheckedChange={() => toggleStudent(student.id)} />
</StructuredListHead> </Table.Cell>
<StructuredListBody> <Table.Cell class="font-medium">{student.name}</Table.Cell>
{#each students as item (item.id)} <Table.Cell>
<StructuredListRow label for="row-{item}"> <div class="flex items-center gap-4">
<StructuredListCell> <span class="text-sm">{student.issued}</span>
<div class="flex-3 flex w-full flex-row items-center justify-start pt-4 text-sm font-medium"> {#if student.peronalized}
<Checkbox /> <Badge variant="secondary" class="bg-primary-100 text-primary-600">personalized</Badge>
<p class="w-full text-start text-sm font-normal">{item.name}</p> {/if}
</div> </div>
</StructuredListCell> </Table.Cell>
<StructuredListCell> </Table.Row>
<div class="flex-2 flex w-full flex-row items-center gap-10"> {/each}
<p class="align-middle text-sm font-normal">{item.issued}</p> </Table.Body>
{#if item.peronalized == true} </Table.Root>
<span class="bg-primary-100 rounded-lg px-3 py-2 text-center"> </div>
<p class="text-primary-600 text-xs opacity-100">personalized</p>
</span>
{:else}
<p class="opacity-0">personalized</p>
{/if}
</div>
</StructuredListCell>
</StructuredListRow>
{/each}
</StructuredListBody>
</StructuredList>
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
</section>
<style> {#if students.length > pageSize}
.box { <div class="mt-4 flex items-center justify-end">
width: 246px; <Pagination.Root
height: 165px; count={students.length}
} perPage={pageSize}
</style> 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>
@@ -16,17 +16,17 @@
<img src="/images/blue-badge-pattern-background.svg" alt="" class="absolute z-0 h-full w-full object-cover" /> <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="relative z-20 m-2 flex h-full justify-between bg-white md:h-[17rem]">
<div class="w-[70%] pl-3"> <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" /> <img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
<h1 class="text-sm">{$currentOrg.name}</h1> <h1 class="text-sm">{$currentOrg.name}</h1>
</header> </header>
<div class="mt-16 font-bold"> <div class="mt-16">
<h2 class="m-0 text-[7px]">This certificate is awarded to</h2> <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> <h1 class="m-0 mt-0.5 text-lg leading-3 text-[#2D499F]">{studentName}</h1>
</div> </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> <h1 class="text-[7px]">has successfully completed training on</h1>
<h2 class="m-0 text-lg"> <h2 class="m-0 text-lg">
{$course.title} {$course.title}
@@ -36,15 +36,15 @@
<div class="my-8 flex items-center justify-between text-center"> <div class="my-8 flex items-center justify-between text-center">
<div> <div>
<h1 class="m-0 h-2/4 text-[7px]">DATE ISSUED:</h1> <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>
<div class="w-[20%] text-[7px]"> <div class="w-[20%] text-[7px]">
<div></div> <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>
</div> </div>
<footer class="desc w-[90%] text-[8px] font-bold"> <footer class="desc w-[90%] text-[8px]">
{$course.description} {$course.description}
</footer> </footer>
</div> </div>
@@ -15,17 +15,17 @@
<div class="w-full bg-[#2D499F] p-3 dark:text-black"> <div class="w-full bg-[#2D499F] p-3 dark:text-black">
<div class="flex w-full bg-white pb-5"> <div class="flex w-full bg-white pb-5">
<div class="w-[80%] pl-3"> <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" /> <img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
<h1 class="text-sm">{$currentOrg.name}</h1> <h1 class="text-sm">{$currentOrg.name}</h1>
</header> </header>
<div class="mt-7 font-bold"> <div class="mt-7">
<h2 class="m-0 text-[7px]">This certificate is awarded to</h2> <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> <h1 class="m-0 mt-0.5 text-lg leading-3 text-[#2D499F]">{studentName}</h1>
</div> </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> <h1 class="text-[7px]">has succesfully completed training on</h1>
<h2 class="m-0 text-lg"> <h2 class="m-0 text-lg">
{$course.title} {$course.title}
@@ -35,15 +35,15 @@
<div class="mb-5 mt-8 flex items-center justify-between text-center"> <div class="mb-5 mt-8 flex items-center justify-between text-center">
<div class=""> <div class="">
<h1 class="m-0 h-2/4 text-[7px]">DATE ISSUED:</h1> <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>
<div class="w-[20%] text-[7px]"> <div class="w-[20%] text-[7px]">
<div class=""></div> <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>
</div> </div>
<footer class="desc w-[90%] text-[8px] font-bold"> <footer class="desc w-[90%] text-[8px]">
{$course.description} {$course.description}
</footer> </footer>
</div> </div>
@@ -16,17 +16,17 @@
<img src="/images/purple-badge-pattern-background.svg" alt="" class="absolute z-0 h-full w-full object-cover" /> <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="relative z-20 m-2 flex h-full justify-between bg-white md:h-[17rem]">
<div class="w-[70%] pl-3"> <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" /> <img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
<h1 class="text-sm">{$currentOrg.name}</h1> <h1 class="text-sm">{$currentOrg.name}</h1>
</header> </header>
<div class="mt-16 font-bold"> <div class="mt-16">
<h2 class="m-0 text-[7px]">This certificate is awarded to</h2> <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> <h1 class="m-0 mt-0.5 text-lg leading-3 text-[#914397]">{studentName}</h1>
</div> </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> <h1 class="text-[7px]">has successfully completed training on</h1>
<h2 class="m-0 text-lg"> <h2 class="m-0 text-lg">
{$course.title} {$course.title}
@@ -36,15 +36,15 @@
<div class="my-8 flex items-center justify-between text-center"> <div class="my-8 flex items-center justify-between text-center">
<div> <div>
<h1 class="m-0 h-2/4 text-[7px]">DATE ISSUED:</h1> <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>
<div class="w-[20%] text-[7px]"> <div class="w-[20%] text-[7px]">
<div></div> <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>
</div> </div>
<footer class="desc w-[90%] text-[8px] font-bold"> <footer class="desc w-[90%] text-[8px]">
{$course.description} {$course.description}
</footer> </footer>
</div> </div>
@@ -15,17 +15,17 @@
<div class="w-full bg-[#95449A] p-3 dark:text-black"> <div class="w-full bg-[#95449A] p-3 dark:text-black">
<div class="flex w-full bg-white pb-5"> <div class="flex w-full bg-white pb-5">
<div class="w-[80%] pl-3"> <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" /> <img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
<h1 class="text-sm">{$currentOrg.name}</h1> <h1 class="text-sm">{$currentOrg.name}</h1>
</header> </header>
<div class="mt-7 font-bold"> <div class="mt-7">
<h2 class="m-0 text-[7px]">This certificate is awarded to</h2> <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> <h1 class="m-0 mt-0.5 text-lg leading-3 text-[#95449A]">{studentName}</h1>
</div> </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> <h1 class="text-[7px]">has succesfully completed training on</h1>
<h2 class="m-0 text-lg"> <h2 class="m-0 text-lg">
{$course.title} {$course.title}
@@ -35,15 +35,15 @@
<div class="mb-5 mt-8 flex items-center justify-between text-center"> <div class="mb-5 mt-8 flex items-center justify-between text-center">
<div class=""> <div class="">
<h1 class="m-0 h-2/4 text-[7px]">DATE ISSUED:</h1> <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>
<div class="w-[20%] text-[7px]"> <div class="w-[20%] text-[7px]">
<div class=""></div> <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>
</div> </div>
<footer class="desc w-[90%] text-[8px] font-bold"> <footer class="desc w-[90%] text-[8px]">
{$course.description} {$course.description}
</footer> </footer>
</div> </div>
@@ -1,5 +1,7 @@
<script lang="ts"> <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 { import {
questionnaire, questionnaire,
@@ -12,28 +14,25 @@
handleAnswerSelect, handleAnswerSelect,
addDynamicValue addDynamicValue
} from '../store/exercise'; } 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 { lesson } from '../store/lessons';
import CirclePlusIcon from '@lucide/svelte/icons/circle-plus'; import OrderModal from './OrderModal.svelte';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte'; import { t } from '$lib/utils/functions/translations';
import TextField from '$lib/components/Form/TextField.svelte';
import Modal from '$lib/components/Modal/index.svelte'; import Modal from '$lib/components/Modal/index.svelte';
import { IconButton } from '$lib/components/IconButton';
import TextArea from '$lib/components/Form/TextArea.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte';
import Checkbox from '$lib/components/Form/Checkbox.svelte'; import Checkbox from '$lib/components/Form/Checkbox.svelte';
import RadioItem from '$lib/components/Form/RadioItem.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 ErrorMessage from '$lib/components/ErrorMessage/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/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 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; const initialQuestionsLength = $questionnaire.questions.length;
@@ -171,23 +170,26 @@
/> />
</div> </div>
<Select <Select.Root
size="xl" type="single"
class="w-[50px]" onValueChange={(value) => {
selected={question?.question_type?.id} if (!value) return;
on:change={(e) => { const id = parseInt(value);
const id = (e.target as HTMLSelectElement)?.value;
if (!id) return;
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.question_type_id = question.question_type.id;
question.is_dirty = true; question.is_dirty = true;
}} }}
> >
{#each QUESTION_TYPES as type} <Select.Trigger class="w-[180px]">
<SelectItem value={type.id} text={$t(type.label)} /> {question?.question_type?.label ? $t(question.question_type.label) : 'Select type'}
{/each} </Select.Trigger>
</Select> <Select.Content>
{#each QUESTION_TYPES as type}
<Select.Item value={type.id.toString()} label={$t(type.label)} />
{/each}
</Select.Content>
</Select.Root>
</div> </div>
{#if typeof question.code === 'string'} {#if typeof question.code === 'string'}
@@ -262,8 +264,4 @@
:global(#dnd-action-dragged-el div) { :global(#dnd-action-dragged-el div) {
z-index: 100; z-index: 100;
} }
:global(select) {
background-image: none;
}
</style> </style>
@@ -1,13 +1,13 @@
<script> <script>
import { SkeletonText } from 'carbon-components-svelte'; import { Skeleton } from '@cio/ui/base/skeleton';
</script> </script>
<div class="space-y-6"> <div class="space-y-6">
<SkeletonText width="100%" heading /> <Skeleton class="h-4 w-full" />
<div class="space-y-3"> <div class="space-y-3">
<SkeletonText width="100%" heading /> <Skeleton class="h-4 w-full" />
<SkeletonText width="100%" heading /> <Skeleton class="h-4 w-full" />
<SkeletonText width="100%" heading /> <Skeleton class="h-4 w-full" />
<SkeletonText width="100%" heading /> <Skeleton class="h-4 w-full" />
</div> </div>
</div> </div>
@@ -1,18 +1,23 @@
<script lang="ts"> <script lang="ts">
import Modal from '$lib/components/Modal/index.svelte'; import { untrack } from 'svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import { Badge } from '@cio/ui/base/badge';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import * as Select from '@cio/ui/base/select';
import Preview from './Preview.svelte'; import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
import { STATUS } from './constants'; import { STATUS } from './constants';
import { t } from '$lib/utils/functions/translations';
import { snackbar } from '$lib/components/Snackbar/store'; 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 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 { useCompletion } from 'ai/svelte';
// import { QUESTION_TYPE } from '$lib/components/Question/constants'; // 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 { interface Props {
open?: boolean; open?: boolean;
@@ -51,16 +56,17 @@
} }
]; ];
let status = SELECTABLE_STATUS[0]; let status = $state(SELECTABLE_STATUS[0]);
let selectedId = $state(status.id);
let reasons = $state({}); let reasons = $state({});
let isLoading = $state(false); let isLoading = $state(false);
let openMenu = $state(false);
let openDeletePrompt = $state(false); let openDeletePrompt = $state(false);
let isDeleting = $state(false); let isDeleting = $state(false);
const total = calculateTotal(data.questionAnswerByPoint); const total = calculateTotal(data.questionAnswerByPoint);
const maxPoints = $derived(getMaxPoints(data.questions)); 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) { function getMaxPoints(questions) {
return (questions || []).reduce((acc, question) => acc + question.points, 0); 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); return Object.values(grades).reduce((acc, grade) => acc + parseInt(grade || '0'), 0);
} }
function handleStatusChange(event) { function handleStatusChange(value: string) {
const newSelectedId = event.detail.selectedId; const newSelectedId = parseInt(value);
const prevStatusId = data.statusId;
setStatus({ // Find the new status
...data, const newStatus = SELECTABLE_STATUS.find((s) => s.id === newSelectedId);
statusId: newSelectedId
});
updateStatus({ if (newStatus) {
submissionId: data.id, // Update state
prevStatusId: data.statusId, status = newStatus;
nextStatusId: status.id, data.statusId = newSelectedId;
total
});
snackbar.success(`${$t('snackbar.exercise.submission_updated')} '${status.text}'`); updateStatus({
submissionId: data.id,
prevStatusId: prevStatusId,
nextStatusId: newSelectedId,
total
});
snackbar.success(`${$t('snackbar.exercise.submission_updated')} '${newStatus.text}'`);
}
} }
function setStatus(data: SubmissionIdData) { function setStatus(data: SubmissionIdData) {
untrack(() => { untrack(() => {
const statusBySelectedId = SELECTABLE_STATUS.find((status) => status.id === data.statusId); const statusBySelectedId = SELECTABLE_STATUS.find((s) => s.id === data.statusId);
if (!statusBySelectedId) { if (statusBySelectedId) {
return; status = statusBySelectedId;
}
status = statusBySelectedId;
if (data.statusId !== selectedId) {
selectedId = data.statusId;
} }
}); });
} }
@@ -261,7 +266,7 @@
</div> </div>
{:else} {:else}
<Preview <Preview
questions={Array.isArray(data.questions) ? data.questions.sort((a, b) => a.order - b.order) : []} questions={sortedQuestions}
questionnaireMetaData={{ questionnaireMetaData={{
answers: data.answers || {}, answers: data.answers || {},
isFinished: true isFinished: true
@@ -279,7 +284,7 @@
<div <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" 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')} {$t('course.navItem.submissions.grading_modal.details')}
{#if data.isEarly} {#if data.isEarly}
<span class="badge ml-2 rounded-sm bg-green-500 px-2 text-sm text-white"> <span class="badge ml-2 rounded-sm bg-green-500 px-2 text-sm text-white">
@@ -292,16 +297,23 @@
{/if} {/if}
</p> </p>
<OverflowMenu open={openMenu} flipped> <DropdownMenu.Root>
<OverflowMenuItem <DropdownMenu.Trigger
text={$t('delete_modal.label')} class="flex h-8 w-8 items-center justify-center rounded-md hover:bg-gray-100 dark:hover:bg-neutral-700"
on:click={() => { >
openDeletePrompt = true; <EllipsisVerticalIcon class="h-5 w-5" />
openMenu = false; </DropdownMenu.Trigger>
}} <DropdownMenu.Content align="end">
danger <DropdownMenu.Item
/> class="text-red-600"
</OverflowMenu> onclick={() => {
openDeletePrompt = true;
}}
>
{$t('delete_modal.label')}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div> </div>
<div class="flex items-center space-x-4 px-3 py-2 text-sm"> <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')}: {$t('course.navItem.submissions.grading_modal.total_grade')}:
</p> </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} {total}/{maxPoints}
</Tag> </Badge>
</div> </div>
<!-- <div class="flex items-center text-sm p-3"> <!-- <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"> <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 rounded-full w-5 h-5 bg-yellow-300 mr-2" />
<p class="dark:text-white">Grading</p> <p class="dark:text-white">Grading</p>
@@ -345,7 +357,26 @@
<p class="font-semibold text-gray-500 dark:text-white"> <p class="font-semibold text-gray-500 dark:text-white">
{$t('course.navItem.submissions.grading_modal.status')}: {$t('course.navItem.submissions.grading_modal.status')}:
</p> </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>
<div class="flex flex-col items-start px-3 py-2 text-sm"> <div class="flex flex-col items-start px-3 py-2 text-sm">
@@ -379,7 +410,7 @@
/> />
</div> </div>
<!-- <div class="flex items-center text-sm p-3"> <!-- <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> <p class="dark:text-white">rotimi-best</p>
</div> --> </div> -->
</div> </div>
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Loading } from 'carbon-components-svelte'; import { Spinner } from '@cio/ui/base/spinner';
import type { ExerciseSubmissions } from '$lib/utils/types';
import { questionnaire } from '../../store/exercise';
import { submissions } from './store'; import { submissions } from './store';
import { questionnaire } from '../../store/exercise';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import type { ExerciseSubmissions } from '$lib/utils/types';
interface Props { interface Props {
isLoading?: boolean; isLoading?: boolean;
@@ -53,7 +54,7 @@
</script> </script>
{#if isLoading} {#if isLoading}
<Loading withOverlay={true} /> <Spinner />
{:else if $submissions?.length} {:else if $submissions?.length}
<div class="mb-5 mt-2 flex w-full gap-1 overflow-auto"> <div class="mb-5 mt-2 flex w-full gap-1 overflow-auto">
{#each $submissions as student, i} {#each $submissions as student, i}
@@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import { onMount, untrack } from 'svelte'; import { untrack } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import type { ExerciseSubmissions } from '$lib/utils/types'; import { Spinner } from '@cio/ui/base/spinner';
import { questionnaire } from '../../store/exercise'; // import * as Chart from '@cio/ui/base/chart';
import { replaceHTMLTag } from '$lib/utils/functions/course'; // import { scaleBand } from '@cio/ui/base/chart';
import type { BarChartOptions, PieChartOptions, PieChart, BarChartSimple } from '@carbon/charts-svelte';
import { getChartOptions } from './functions';
import { submissions } from './store'; import { submissions } from './store';
import '@carbon/charts-svelte/styles.css'; import { questionnaire } from '../../store/exercise';
import { Loading } from 'carbon-components-svelte';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import type { ExerciseSubmissions } from '$lib/utils/types';
import { replaceHTMLTag } from '$lib/utils/functions/course';
interface Props { interface Props {
isLoading?: boolean; isLoading?: boolean;
@@ -17,22 +17,31 @@
let { isLoading = $bindable(true) }: Props = $props(); let { isLoading = $bindable(true) }: Props = $props();
type TranformedQuestionChartData = { type TransformedQuestionChartData = {
group: string; option: string;
value: number; responses: number;
isCorrect?: boolean; isCorrect?: boolean;
}; };
interface TranformedQuestion {
interface TransformedQuestion {
title: string; title: string;
type: number; type: number;
chartData: TranformedQuestionChartData[]; chartData: TransformedQuestionChartData[];
} }
let transformedQuestions: TranformedQuestion[] = $state([]); let transformedQuestions: TransformedQuestion[] = $state([]);
let barChart: typeof BarChartSimple | undefined = $state();
let pieChart: typeof PieChart | undefined = $state(); // Create dynamic chart config based on question data
let pieOptions: PieChartOptions | undefined = $derived(getChartOptions(isLoading).pieOptions); function getChartConfig(chartData: TransformedQuestionChartData[]): any {
let barOptions: BarChartOptions | undefined = $derived(getChartOptions(isLoading).barOptions); 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( function getAnswerToQuestionOfStudent(
questionId: number, questionId: number,
@@ -57,7 +66,7 @@
} }
const getTransformedData = ($submissions: ExerciseSubmissions[]): void => { const getTransformedData = ($submissions: ExerciseSubmissions[]): void => {
const _transformedQuestions: TranformedQuestion[] = []; const _transformedQuestions: TransformedQuestion[] = [];
$questionnaire.questions.forEach( $questionnaire.questions.forEach(
(question: { (question: {
@@ -69,7 +78,7 @@
const transformedQuestion: { const transformedQuestion: {
title: string; title: string;
type: number; type: number;
chartData: TranformedQuestionChartData[]; chartData: TransformedQuestionChartData[];
} = { } = {
title: question.title, title: question.title,
type: question.question_type_id, type: question.question_type_id,
@@ -79,9 +88,9 @@
// If textarea don't calculate the value just get the student's answer // If textarea don't calculate the value just get the student's answer
if (transformedQuestion.type === 3) { if (transformedQuestion.type === 3) {
$submissions.forEach((submission) => { $submissions.forEach((submission) => {
const chartData: TranformedQuestionChartData = { const chartData: TransformedQuestionChartData = {
group: getAnswerToQuestionOfStudent(question.id, true, submission)[0], option: getAnswerToQuestionOfStudent(question.id, true, submission)[0],
value: 0 responses: 0
}; };
// Update the transformed question chartData // Update the transformed question chartData
@@ -91,9 +100,9 @@
// radio or checkbox // radio or checkbox
question.options.forEach((option) => { question.options.forEach((option) => {
const { value, is_correct, label } = option; const { value, is_correct, label } = option;
const chartData: TranformedQuestionChartData = { const chartData: TransformedQuestionChartData = {
group: replaceHTMLTag(label), option: replaceHTMLTag(label),
value: 0, responses: 0,
isCorrect: is_correct isCorrect: is_correct
}; };
@@ -101,7 +110,7 @@
const studentAnswer = getAnswerToQuestionOfStudent(question.id, false, studentSubmission); const studentAnswer = getAnswerToQuestionOfStudent(question.id, false, studentSubmission);
if (studentAnswer.includes(value)) { if (studentAnswer.includes(value)) {
chartData.value += 1; chartData.responses += 1;
} }
}); });
transformedQuestion.chartData.push(chartData); transformedQuestion.chartData.push(chartData);
@@ -119,12 +128,6 @@
console.log({ transformedQuestions }); console.log({ transformedQuestions });
}; };
onMount(async () => {
const charts = await import('@carbon/charts-svelte');
barChart = charts.BarChartSimple;
pieChart = charts.PieChart;
});
$effect(() => { $effect(() => {
if ($submissions?.length) { if ($submissions?.length) {
getTransformedData($submissions); getTransformedData($submissions);
@@ -133,28 +136,49 @@
</script> </script>
{#if isLoading} {#if isLoading}
<Loading withOverlay={true} /> <Spinner />
{:else if browser} {:else if browser}
<div> <div>
<p class="mb-3 text-2xl"> <p class="mb-3 text-2xl">
{$t('course.navItem.lessons.exercises.all_exercises.analytics.summary.question_chart')} {$t('course.navItem.lessons.exercises.all_exercises.analytics.summary.question_chart')}
</p> </p>
{#each transformedQuestions as q} {#each transformedQuestions as q}
<div class="mb-4"> <div class="mb-6">
<p>{q.title}</p> <p class="mb-3 font-medium">{q.title}</p>
{#if q.type === 1 && pieOptions} {#if q.type === 1}
{@const SvelteComponent = pieChart} <!-- Pie Chart for Radio Questions (single choice) -->
<SvelteComponent data={q.chartData} options={pieOptions} /> {@const chartConfig = getChartConfig(q.chartData)}
{:else if q.type === 2 && barOptions} {console.log('chartConfig', chartConfig)}
{@const SvelteComponent_1 = barChart} <!-- <Chart.Container config={chartConfig} class="min-h-[300px] w-full">
<SvelteComponent_1 data={q.chartData} options={barOptions} /> <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} {:else}
<!-- Text answers -->
<div class="max-h-[250px] overflow-auto"> <div class="max-h-[250px] overflow-auto">
<ul> <ul>
{#each q.chartData as answer (answer)} {#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"> <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> </div>
{/if} {/if}
{/each} {/each}
@@ -1,10 +1,11 @@
<script lang="ts"> <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 DateTime from '$lib/components/Form/DateTime.svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { questionnaire } from '../store/exercise';
import TextEditor from '$lib/components/TextEditor/index.svelte'; 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 { interface Props {
preview: boolean; preview: boolean;
@@ -43,14 +44,15 @@
<p class="mb-1"> <p class="mb-1">
{$t('course.navItem.lessons.exercises.all_exercises.description.heading')} {$t('course.navItem.lessons.exercises.all_exercises.description.heading')}
</p> </p>
<TextEditor <TextEditor
value={$questionnaire.description} content={$questionnaire.description}
onChange={(html) => { onChange={(html) => {
$questionnaire.is_description_dirty = true; $questionnaire.is_description_dirty = true;
$questionnaire.description = html; $questionnaire.description = html;
}} }}
editorClass="max-h-[300px]"
placeholder={$t('course.navItem.lessons.exercises.all_exercises.description.describe')} placeholder={$t('course.navItem.lessons.exercises.all_exercises.description.describe')}
maxHeight={300}
/> />
</div> </div>
{:else if preview} {:else if preview}
@@ -78,7 +80,7 @@
{/if} {/if}
</div> </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')} {@html $questionnaire.description || $t('course.navItem.lessons.exercises.all_exercises.description.no')}
</article> </article>
{/if} {/if}
@@ -1,26 +1,15 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { IconButton } from '$lib/components/IconButton'; import { goto } from '$app/navigation';
import { PageBody } from '$lib/components/Page'; import { Button } from '@cio/ui/base/button';
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 EyeIcon from '@lucide/svelte/icons/eye'; import EyeIcon from '@lucide/svelte/icons/eye';
import { onDestroy, onMount, untrack } from 'svelte'; 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 { import {
handleAddQuestion, handleAddQuestion,
questionnaire, questionnaire,
@@ -28,10 +17,20 @@
reset, reset,
validateQuestionnaire validateQuestionnaire
} from '../store/exercise'; } 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 EditMode from './EditMode.svelte';
import Analytics from './Submissions/index.svelte';
import UpdateDescription from './UpdateDescription.svelte';
import ViewMode from './ViewMode.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 { interface Props {
exerciseId?: string; exerciseId?: string;
@@ -45,7 +44,7 @@
let preview: boolean = $state(false); let preview: boolean = $state(false);
let shouldDeleteExercise = $state(false); let shouldDeleteExercise = $state(false);
let isSaving = $state(false); let isSaving = $state(false);
let selectedIndex = $state(0); let selectedTab = $state('questions');
async function handleSave() { async function handleSave() {
if ($globalStore.isStudent) return; if ($globalStore.isStudent) return;
@@ -84,21 +83,17 @@
reset(); reset();
}); });
function onSelectedIndexChange(index: number) {
untrack(() => {
goto($page.url.pathname + '?tabIndex=' + index);
});
}
onMount(() => { onMount(() => {
const tabIndex = $page.url.searchParams.get('tabIndex'); const tabParam = $page.url.searchParams.get('tab');
if (tabIndex) { if (tabParam) {
selectedIndex = parseInt(tabIndex); selectedTab = tabParam;
} }
}); });
$effect(() => { $effect(() => {
onSelectedIndexChange(selectedIndex); untrack(() => {
goto($page.url.pathname + '?tab=' + selectedTab);
});
}); });
// $effect(() => { // $effect(() => {
// const addNewQ = $questionnaire?.questions?.length < 1; // const addNewQ = $questionnaire?.questions?.length < 1;
@@ -114,29 +109,35 @@
<PageBody isPageNavHidden={$globalStore.isStudent} padding="px-4 overflow-x-hidden"> <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"> <div class="sticky top-0 z-10 mb-3 bg-gray-100 p-2 dark:bg-neutral-800">
<Breadcrumb noTrailingSlash> <Breadcrumb.Root>
<BreadcrumbItem href={path}> <Breadcrumb.List>
{$t('course.navItem.lessons.exercises.all_exercises.heading')} <Breadcrumb.Item>
</BreadcrumbItem> <Breadcrumb.Link href={path}>
<BreadcrumbItem href={`${path}/${exerciseId}`} isCurrentPage> {$t('course.navItem.lessons.exercises.all_exercises.heading')}
{$questionnaire.title} </Breadcrumb.Link>
</BreadcrumbItem> </Breadcrumb.Item>
</Breadcrumb> <Breadcrumb.Separator />
<Breadcrumb.Item>
<Breadcrumb.Page>
{$questionnaire.title}
</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
<RoleBasedSecurity allowedRoles={[1, 2]}> <RoleBasedSecurity allowedRoles={[1, 2]}>
<!-- <ContentSwitcher bind:selectedIndex class="mb-2"> <!-- <Tabs.Root bind:value={selectedTab} class="w-full">
<Switch <Tabs.List class="mb-2">
text="{$t('course.navItem.lessons.exercises.all_exercises.questions')} ({$questionnaire <Tabs.Trigger value="questions">
.questions.length})" {$t('course.navItem.lessons.exercises.all_exercises.questions')} ({$questionnaire.questions.length})
/> </Tabs.Trigger>
<Switch <Tabs.Trigger value="submissions">
text="{$t( {$t('course.navItem.lessons.exercises.all_exercises.submissions')} ({$questionnaire.totalSubmissions})
'course.navItem.lessons.exercises.all_exercises.submissions' </Tabs.Trigger>
)} ({$questionnaire.totalSubmissions})" </Tabs.List>
/> </Tabs.Root> -->
</ContentSwitcher> -->
{#if selectedIndex === 0} {#if selectedTab === 'questions'}
<div class="right-0 flex w-full items-center justify-end"> <div class="right-0 flex w-full items-center justify-end">
<div class="flex items-center"> <div class="flex items-center">
<PrimaryButton <PrimaryButton
@@ -167,24 +168,33 @@
> >
<CirclePlusIcon size={20} /> <CirclePlusIcon size={20} />
</IconButton> </IconButton>
<OverflowMenu flipped> <DropdownMenu.Root>
<OverflowMenuItem <DropdownMenu.Trigger>
text={$t('course.navItem.lessons.exercises.all_exercises.reorder')} <Button variant="ghost" size="icon" class="h-8 w-8">
on:click={() => ($questionnaireOrder.open = true)} <EllipsisVerticalIcon class="h-5 w-5" />
/> <span class="sr-only">Open menu</span>
<OverflowMenuItem </Button>
danger </DropdownMenu.Trigger>
text={$t('course.navItem.lessons.exercises.all_exercises.delete_exercise')} <DropdownMenu.Content align="end">
on:click={() => (shouldDeleteExercise = true)} <DropdownMenu.Item onclick={() => ($questionnaireOrder.open = true)}>
/> {$t('course.navItem.lessons.exercises.all_exercises.reorder')}
</OverflowMenu> </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>
</div> </div>
{/if} {/if}
</RoleBasedSecurity> </RoleBasedSecurity>
</div> </div>
{#if selectedIndex === 0} {#if selectedTab === 'questions'}
<RoleBasedSecurity allowedRoles={[1, 2]}> <RoleBasedSecurity allowedRoles={[1, 2]}>
<UpdateDescription {preview} /> <UpdateDescription {preview} />
</RoleBasedSecurity> </RoleBasedSecurity>
@@ -193,7 +203,7 @@
{:else} {:else}
<ViewMode {preview} {exerciseId} isFetchingExercise={isFetching} /> <ViewMode {preview} {exerciseId} isFetchingExercise={isFetching} />
{/if} {/if}
{:else if selectedIndex === 1} {:else if selectedTab === 'submissions'}
<Analytics bind:exerciseId /> <Analytics bind:exerciseId />
{/if} {/if}
</PageBody> </PageBody>
@@ -1,24 +1,18 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; 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 Modal from '$lib/components/Modal/index.svelte';
import Confetti from '$lib/components/Confetti/index.svelte';
import TextField from '$lib/components/Form/TextField.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 PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.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 { 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 { interface Props {
open: boolean; open: boolean;
@@ -164,7 +158,7 @@
</div> </div>
<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> <span class="mr-2 text-sm">{option.title}</span>
{#if option.isDisabled} {#if option.isDisabled}
<ComingSoon /> <ComingSoon />
@@ -222,14 +216,12 @@
<div> <div>
<div class="mb-5 flex items-center gap-2"> <div class="mb-5 flex items-center gap-2">
{#each tags as tag} {#each tags as tag}
<Tag <Badge
type={selectedTag === tag ? 'warm-gray' : 'outline'}
class={selectedTag === tag ? 'bg-primary-400' : ''} class={selectedTag === tag ? 'bg-primary-400' : ''}
interactive onclick={() => {
on:click={() => {
selectedTag = tag; selectedTag = tag;
selectedTemplateId = ''; selectedTemplateId = '';
}}>{tag}</Tag }}>{tag}</Badge
> >
{/each} {/each}
</div> </div>
@@ -246,7 +238,7 @@
onclick={() => (selectedTemplateId = template.id)} onclick={() => (selectedTemplateId = template.id)}
> >
<div class="flex h-full flex-col justify-evenly"> <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} {template.title}
</p> </p>
<div class="flex flex-col items-start justify-between gap-1"> <div class="flex flex-col items-start justify-between gap-1">
@@ -1,25 +1,27 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; 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 { onMount, untrack } from 'svelte';
import { Moon } from 'svelte-loading-spinners'; import { Moon } from 'svelte-loading-spinners';
import Exercise from '../Exercise/index.svelte';
import { isQuestionnaireFetching, questionnaire } from '../store/exercise';
import { lesson } from '../store/lessons'; 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 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 { interface Props {
path?: string; path?: string;
@@ -177,9 +179,15 @@
<PageBody isPageNavHidden={$globalStore.isStudent}> <PageBody isPageNavHidden={$globalStore.isStudent}>
{#snippet header()} {#snippet header()}
<slot:fragment> <slot:fragment>
<Breadcrumb class="my-2"> <Breadcrumb.Root class="my-2">
<BreadcrumbItem href={path}>{$t('course.navItem.lessons.exercises.heading')}</BreadcrumbItem> <Breadcrumb.List>
</Breadcrumb> <Breadcrumb.Item>
<Breadcrumb.Link href={path}>
{$t('course.navItem.lessons.exercises.heading')}
</Breadcrumb.Link>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
<RoleBasedSecurity allowedRoles={[1, 2]}> <RoleBasedSecurity allowedRoles={[1, 2]}>
<PrimaryButton <PrimaryButton
className="mr-2 my-2" className="mr-2 my-2"
@@ -1,18 +1,22 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { dndzone } from 'svelte-dnd-action'; import { dndzone } from 'svelte-dnd-action';
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte'; import { Button } from '@cio/ui/base/button';
import TextField from '$lib/components/Form/TextField.svelte'; import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import TextChip from '$lib/components/Chip/Text.svelte';
import { course } from '$lib/components/Course/store';
import { globalStore } from '$lib/utils/store/app';
import ListChecksIcon from '@lucide/svelte/icons/list-checks'; import ListChecksIcon from '@lucide/svelte/icons/list-checks';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte'; import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
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 Box from '$lib/components/Box/index.svelte'; 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; const flipDurationMs = 300;
@@ -138,37 +142,48 @@
<!-- 3 dot edit --> <!-- 3 dot edit -->
<div class="flex flex-row"> <div class="flex flex-row">
<RoleBasedSecurity allowedRoles={[1, 2]}> <RoleBasedSecurity allowedRoles={[1, 2]}>
<OverflowMenu size="xl"> <DropdownMenu.Root>
<OverflowMenuItem <DropdownMenu.Trigger>
text={lesson.is_unlocked <Button variant="ghost" size="icon" class="h-8 w-8">
? $t('course.navItem.lessons.add_lesson.lock') <EllipsisVerticalIcon class="h-5 w-5" />
: $t('course.navItem.lessons.add_lesson.unlock')} <span class="sr-only">Open menu</span>
on:click={() => { </Button>
lesson.is_unlocked = !lesson.is_unlocked; </DropdownMenu.Trigger>
handleSaveLesson(lesson, $course.id); <DropdownMenu.Content align="end">
}} <DropdownMenu.Item
/> onclick={() => {
<OverflowMenuItem lesson.is_unlocked = !lesson.is_unlocked;
text={lessonEditing === lesson.id handleSaveLesson(lesson, $course.id);
? $t('course.navItem.lessons.add_lesson.save') }}
: $t('course.navItem.lessons.add_lesson.edit')} >
on:click={() => { {lesson.is_unlocked
if (lessonEditing === lesson.id) { ? $t('course.navItem.lessons.add_lesson.lock')
saveLesson(lesson, $course.id); : $t('course.navItem.lessons.add_lesson.unlock')}
} else { </DropdownMenu.Item>
lessonEditing = lesson.id; <DropdownMenu.Item
} onclick={() => {
}} if (lessonEditing === lesson.id) {
/> saveLesson(lesson, $course.id);
<OverflowMenuItem } else {
danger lessonEditing = lesson.id;
text={$t('course.navItem.lessons.add_lesson.delete')} }
on:click={() => { }}
lessonToDelete = lesson; >
openDeleteModal = true; {lessonEditing === lesson.id
}} ? $t('course.navItem.lessons.add_lesson.save')
/> : $t('course.navItem.lessons.add_lesson.edit')}
</OverflowMenu> </DropdownMenu.Item>
<DropdownMenu.Item
class="text-red-600 focus:text-red-600 dark:text-red-400"
onclick={() => {
lessonToDelete = lesson;
openDeleteModal = true;
}}
>
{$t('course.navItem.lessons.add_lesson.delete')}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</RoleBasedSecurity> </RoleBasedSecurity>
</div> </div>
</div> </div>
@@ -4,7 +4,9 @@
import { dndzone } from 'svelte-dnd-action'; import { dndzone } from 'svelte-dnd-action';
import PlusIcon from '@lucide/svelte/icons/plus'; import PlusIcon from '@lucide/svelte/icons/plus';
import ListChecksIcon from '@lucide/svelte/icons/list-checks'; 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 TextField from '$lib/components/Form/TextField.svelte';
import TextChip from '$lib/components/Chip/Text.svelte'; import TextChip from '$lib/components/Chip/Text.svelte';
import { IconButton } from '$lib/components/IconButton'; import { IconButton } from '$lib/components/IconButton';
@@ -222,18 +224,29 @@
<IconButton contained size="small" onClick={() => handleAddLesson(section.id)} disabled={!!lessonEditing}> <IconButton contained size="small" onClick={() => handleAddLesson(section.id)} disabled={!!lessonEditing}>
<PlusIcon size={16} /> <PlusIcon size={16} />
</IconButton> </IconButton>
<OverflowMenu size="xl" flipped> <DropdownMenu.Root>
<OverflowMenuItem <DropdownMenu.Trigger>
text={$t('course.navItem.lessons.add_lesson.edit')} <Button variant="ghost" size="icon" class="h-8 w-8">
disabled={!!lessonEditing} <EllipsisVerticalIcon class="h-5 w-5" />
on:click={() => onEdit({ sectionId: section.id, sectionTitle: section.title })} <span class="sr-only">Open menu</span>
/> </Button>
<OverflowMenuItem </DropdownMenu.Trigger>
danger <DropdownMenu.Content align="end">
text={$t('course.navItem.lessons.add_lesson.delete')} <DropdownMenu.Item
on:click={() => onDelete({ sectionId: section.id })} disabled={!!lessonEditing}
/> onclick={() => onEdit({ sectionId: section.id, sectionTitle: section.title })}
</OverflowMenu> >
{$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} {/if}
</div> </div>
</RoleBasedSecurity> </RoleBasedSecurity>
@@ -309,26 +322,36 @@
onClick={() => onSave({ lessonId: lesson.id }, lesson)} onClick={() => onSave({ lessonId: lesson.id }, lesson)}
/> />
{:else} {:else}
<OverflowMenu size="sm" flipped> <DropdownMenu.Root>
<OverflowMenuItem <DropdownMenu.Trigger>
text={lesson.is_unlocked <Button variant="ghost" size="icon" class="h-8 w-8">
? $t('course.navItem.lessons.add_lesson.lock') <EllipsisVerticalIcon class="h-5 w-5" />
: $t('course.navItem.lessons.add_lesson.unlock')} <span class="sr-only">Open menu</span>
on:click={() => { </Button>
lesson.is_unlocked = !lesson.is_unlocked; </DropdownMenu.Trigger>
handleSaveLesson(lesson, $course.id); <DropdownMenu.Content align="end">
}} <DropdownMenu.Item
/> onclick={() => {
<OverflowMenuItem lesson.is_unlocked = !lesson.is_unlocked;
text={$t('course.navItem.lessons.add_lesson.edit')} handleSaveLesson(lesson, $course.id);
on:click={() => onEdit({ lessonId: lesson.id, lessonTitle: lesson.title })} }}
/> >
<OverflowMenuItem {lesson.is_unlocked
danger ? $t('course.navItem.lessons.add_lesson.lock')
text={$t('course.navItem.lessons.add_lesson.delete')} : $t('course.navItem.lessons.add_lesson.unlock')}
on:click={() => onDelete({ lessonId: lesson.id })} </DropdownMenu.Item>
/> <DropdownMenu.Item onclick={() => onEdit({ lessonId: lesson.id, lessonTitle: lesson.title })}>
</OverflowMenu> {$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} {/if}
</div> </div>
</RoleBasedSecurity> </RoleBasedSecurity>
@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte'; import { Skeleton } from '@cio/ui/base/skeleton';
</script> </script>
<div class="w-full"> <div class="w-full">
<SkeletonPlaceholder style="width: 100%; height: 10rem;" /> <Skeleton class="h-40 w-full" />
<div class="my-3"> <div class="my-3">
<SkeletonText style="width: 100%;" /> <Skeleton class="h-4 w-full" />
<SkeletonText style="width: 100%;" /> <Skeleton class="h-4 w-full" />
<SkeletonText style="width: 100%;" /> <Skeleton class="h-4 w-full" />
</div> </div>
</div> </div>
@@ -1,20 +1,20 @@
<script lang="ts"> <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 type { LessonVideoType } from '$lib/utils/types';
import { isLessonDirty, lesson } from '$lib/components/Course/components/Lesson/store/lessons'; 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 { IconButton } from '$lib/components/IconButton';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.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 genericLinks = $state('');
let error = $state(''); let error = $state('');
function getVideoUrls(urls = '') {
return (urls || '').split(',').filter((url) => !!url.trim());
}
function addVideo() { function addVideo() {
const links = getVideoUrls(genericLinks); const links = getVideoUrls(genericLinks);
const validLinks = links.filter(isValidLink); 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> </script>
<div class="flex w-full items-{error ? 'center' : 'end'} justify-between gap-5"> <div class="flex w-full items-{error ? 'center' : 'end'} justify-between gap-5">
@@ -85,10 +81,12 @@
{#each $lesson?.materials?.videos as video, index} {#each $lesson?.materials?.videos as video, index}
{#if video.type === 'generic'} {#if video.type === 'generic'}
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Tag type="blue"> <Badge class="max-w-md truncate" variant="secondary">
{video.link} {video.link}
</Tag> </Badge>
<CopyButton text={video.link} feedback="Copied to clipboard" /> <IconButton value="copy-video" onClick={() => copyToClipboard(video.link)}>
<CopyIcon size={16} />
</IconButton>
<IconButton value="delete-video" onClick={() => removeVideo(index)}> <IconButton value="delete-video" onClick={() => removeVideo(index)}>
<TrashIcon size={16} /> <TrashIcon size={16} />
</IconButton> </IconButton>
@@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import { Progress } from '@cio/ui/base/progress';
import { preventDefault } from '$lib/utils/functions/svelte'; 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 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 { VideoUploader } from '$lib/utils/services/courses/presign';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
interface Props { interface Props {
lessonId?: string; lessonId?: string;
@@ -142,12 +141,7 @@
<p class="mt-5 text-center"> <p class="mt-5 text-center">
{$t('course.navItem.lessons.materials.tabs.video.add_video.uploading')} {$t('course.navItem.lessons.materials.tabs.video.add_video.uploading')}
</p> </p>
<ProgressBar <Progress class="w-full" value={$lessonVideoUpload.uploadProgress} max={100} />
class="w-full"
value={$lessonVideoUpload.uploadProgress}
max={100}
status={$lessonVideoUpload.uploadProgress === 100 ? 'finished' : 'active'}
/>
<p class="text-sm">{helperText}</p> <p class="text-sm">{helperText}</p>
<div class="mt-3"> <div class="mt-3">
<PrimaryButton <PrimaryButton
@@ -1,20 +1,20 @@
<script lang="ts"> <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 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 { t } from '$lib/utils/functions/translations';
import { IconButton } from '$lib/components/IconButton';
import type { LessonVideoType } from '$lib/utils/types'; 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 youtubeLinks = $state('');
let error = $state(''); let error = $state('');
function getVideoUrls(urls = '') {
return (urls || '').split(',').filter((url) => !!url.trim());
}
function addVideo() { function addVideo() {
const links = getVideoUrls(youtubeLinks); const links = getVideoUrls(youtubeLinks);
const validLinks = links.filter(isValidYouTubeLink); const validLinks = links.filter(isValidYouTubeLink);
@@ -42,10 +42,6 @@
return youtubeRegex.test(link.trim()); return youtubeRegex.test(link.trim());
} }
function removeVideo(index = 0) {
$lesson.materials.videos = $lesson.materials.videos.filter((_v, i) => i !== index);
}
</script> </script>
<div class="flex w-full items-{error ? 'center' : 'end'} justify-between gap-5"> <div class="flex w-full items-{error ? 'center' : 'end'} justify-between gap-5">
@@ -74,10 +70,12 @@
{#each $lesson?.materials?.videos as video, index} {#each $lesson?.materials?.videos as video, index}
{#if video.type === 'youtube'} {#if video.type === 'youtube'}
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Tag type="blue"> <Badge class="max-w-md truncate" variant="secondary">
{video.link} {video.link}
</Tag> </Badge>
<CopyButton text={video.link} feedback="Copied to clipboard" /> <IconButton value="copy-video" onClick={() => copyToClipboard(video.link)}>
<CopyIcon size={16} />
</IconButton>
<IconButton value="delete-video" onClick={() => removeVideo(index)}> <IconButton value="delete-video" onClick={() => removeVideo(index)}>
<TrashIcon size={16} /> <TrashIcon size={16} />
</IconButton> </IconButton>
@@ -1,20 +1,23 @@
<script lang="ts"> <script lang="ts">
import Avatar from '$lib/components/Avatar/index.svelte'; import { onDestroy, onMount } from 'svelte';
import { lesson, lessonCommentsChannel } from '$lib/components/Course/components/Lesson/store/lessons'; 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 { group } from '$lib/components/Course/store';
import Avatar from '$lib/components/Avatar/index.svelte';
import TextArea from '$lib/components/Form/TextArea.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte';
import DeleteModal from '$lib/components/Modal/DeleteModal.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 PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store'; import { lesson, lessonCommentsChannel } from '$lib/components/Course/components/Lesson/store/lessons';
import { calDateDiff } from '$lib/utils/functions/date';
import { getSupabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
import { profile } from '$lib/utils/store/user'; 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 { 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 supabase = getSupabase();
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
@@ -261,7 +264,7 @@
<div class="mx-auto w-full max-w-[65ch]"> <div class="mx-auto w-full max-w-[65ch]">
<!-- <hr class="my-5" /> --> <!-- <hr class="my-5" /> -->
<div class="mb-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}) {$t('course.navItem.lessons.comments.title')} ({pagination.count})
</p> </p>
</div> </div>
@@ -294,7 +297,7 @@
<div class="w-full rounded-md border px-4 pb-4 pt-2 dark:border-neutral-700"> <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"> <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} {commentItem.name}
<span <span
class="ml-1 text-xs font-normal text-gray-800 dark:text-white" class="ml-1 text-xs font-normal text-gray-800 dark:text-white"
@@ -305,17 +308,25 @@
</p> </p>
{#if groupmember?.id === commentItem.groupmember_id} {#if groupmember?.id === commentItem.groupmember_id}
<OverflowMenu flipped> <DropdownMenu.Root>
<OverflowMenuItem text="Edit" on:click={() => (editCommentId = commentItem.id)} /> <DropdownMenu.Trigger
<OverflowMenuItem class="flex h-8 w-8 items-center justify-center rounded-md hover:bg-gray-100 dark:hover:bg-neutral-700"
danger >
text="Delete" <EllipsisVerticalIcon class="h-5 w-5" />
on:click={() => { </DropdownMenu.Trigger>
deleteCommentId = commentItem.id; <DropdownMenu.Content align="end">
openDeleteModal = true; <DropdownMenu.Item onclick={() => (editCommentId = commentItem.id)}>Edit</DropdownMenu.Item>
}} <DropdownMenu.Item
/> class="text-red-600"
</OverflowMenu> onclick={() => {
deleteCommentId = commentItem.id;
openDeleteModal = true;
}}
>
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if} {/if}
</div> </div>
@@ -1,9 +1,26 @@
<script lang="ts"> <script lang="ts">
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import isEmpty from 'lodash/isEmpty';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import Box from '$lib/components/Box/index.svelte'; import { fade } from 'svelte/transition';
import AddVideoToLesson from '$lib/components/Course/components/Lesson/Materials/Video/AddVideoToLesson.svelte'; import * as Popover from '@cio/ui/base/popover';
import AddDocumentToLesson from '$lib/components/Course/components/Lesson/Materials/Document/AddDocumentToLesson.svelte';
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 { import {
deleteLessonVideo, deleteLessonVideo,
handleUpdateLessonMaterials, handleUpdateLessonMaterials,
@@ -13,41 +30,26 @@
lessonVideoUpload, lessonVideoUpload,
lessonDocUpload lessonDocUpload
} from '$lib/components/Course/components/Lesson/store/lessons'; } 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 { 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 { lessonFallbackNote, t } from '$lib/utils/functions/translations';
import { currentOrg } from '$lib/utils/store/org'; import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import type { LessonPage, LOCALE } from '$lib/utils/types'; import Loader from './Loader.svelte';
// import { useCompletion } from 'ai/svelte'; import Box from '$lib/components/Box/index.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 Comments from './components/Comments.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 ComponentNote from './components/ComponentNote.svelte';
import ComponentSlide from './components/ComponentSlide.svelte'; import ComponentSlide from './components/ComponentSlide.svelte';
import ComponentVideo from './components/ComponentVideo.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 ComponentDocument from './components/ComponentDocument.svelte';
import * as CONSTANTS from './constants'; import AddVideoToLesson from '$lib/components/Course/components/Lesson/Materials/Video/AddVideoToLesson.svelte';
import { orderedTabs } from './constants'; import AddDocumentToLesson from '$lib/components/Course/components/Lesson/Materials/Document/AddDocumentToLesson.svelte';
import Loader from './Loader.svelte';
interface Props { interface Props {
mode?: any; mode?: any;
@@ -67,30 +69,20 @@
toggleMode = () => {} toggleMode = () => {}
}: Props = $props(); }: Props = $props();
// let content = $state<EdraEditorProps['Content']>(); let openPopover = $state<boolean>(false);
// let editor = $state<EdraEditorProps['Editor']>(); let aiButtonClass =
// function onUpdate() { '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';
// content = editor?.getHTML(); let player = $state<HTMLVideoElement>();
// 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 localeExists: Record<string, boolean> = {}; let localeExists: Record<string, boolean> = {};
// let prevContent = ''; // let prevContent = '';
let timeoutId: NodeJS.Timeout; let timeoutId: NodeJS.Timeout;
let errors: Record<string, string> = {}; let errors: Record<string, string> = {};
// let editorWindowRef: Window | undefined = $state(); // 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'); ('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 lessonTitle = $derived($lesson.title);
const tabs = $derived.by(() => { const tabs = $derived.by(() => {
const ordered = orderedTabs(CONSTANTS.tabs, $course.metadata?.lessonTabsOrder); const ordered = orderedTabs(CONSTANTS.tabs, $course.metadata?.lessonTabsOrder);
const content = $lessonByTranslation[lessonId]?.[$lesson.locale] || ''; const content = $lessonByTranslation[lessonId]?.[$lesson.locale] || '';
@@ -114,9 +106,24 @@
}); });
}); });
const componentsToRender = $derived(getComponentOrder(tabs)); const componentsToRender = $derived(getComponentOrder(tabs));
let currentTab = $derived(tabs[0].value); 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) => { const onChange = (tab) => {
return () => { return () => {
currentTab = tab; currentTab = tab;
@@ -211,33 +218,38 @@
$lessonVideoUpload.isModalOpen = true; $lessonVideoUpload.isModalOpen = true;
}; };
let isLoading = writable(false);
// const { input, handleSubmit, completion, isLoading } = useCompletion({ // const { input, handleSubmit, completion, isLoading } = useCompletion({
// api: '/api/completion' // api: '/api/completion'
// }); // });
// function updateNoteByCompletion(completion: string) { // function updateNoteByCompletion(completion: string) {
// if (!completion) return; // if (!completion) return;
// if ($lessonByTranslation[lessonId]) {
// if ($lessonByTranslation[lessonId]) { // $lessonByTranslation[lessonId][$lesson.locale] = `${prevContent}${completion}`;
// $lessonByTranslation[lessonId][$lesson.locale] = `${prevContent}${completion}`; // }
// } // autoSave($lesson.materials, $lessonByTranslation[lessonId], false, lessonId);
// if (editorInstance && editorInstance.view) {
// autoSave($lesson.materials, $lessonByTranslation[lessonId], false, lessonId); // try {
// // For TipTap editor, we can use the editor's commands to scroll
// if (editorWindowRef) { // const { view } = editorInstance;
// const tmceBody = editorWindowRef?.document?.querySelector('body'); // const { state } = view;
// if (typeof tmceBody?.scrollHeight === 'number') { // const endPos = state.doc.content.size;
// editorWindowRef?.scrollTo(0, tmceBody.scrollHeight); // // 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);
// } // }
// } // }
// }
function callAI(_type = '') { function callAI(_type = '') {
// prevContent = $lessonByTranslation[lessonId]?.[$lesson.locale] || ''; // prevContent = $lessonByTranslation[lessonId]?.[$lesson.locale] || '';
// const _lesson = $lessons.find((les) => les.id === $lesson.id); // const _lesson = $lessons.find((les) => les.id === $lesson.id);
// $input = JSON.stringify({ // $input = JSON.stringify({
// type, // _type,
// lessonTitle: _lesson?.title || '', // lessonTitle: _lesson?.title || '',
// courseTitle: $course.title, // courseTitle: $course.title,
// locale: $lesson.locale // locale: $lesson.locale
@@ -373,27 +385,24 @@
<TabContent value={getValue('course.navItem.lessons.materials.tabs.note.title')} index={currentTab}> <TabContent value={getValue('course.navItem.lessons.materials.tabs.note.title')} index={currentTab}>
<div class="flex justify-end gap-1"> <div class="flex justify-end gap-1">
<!-- Update this when ai-sdk is updated --> <!-- Update this when ai-sdk is updated -->
<div bind:this={aiButtonRef} class="hidden flex-row-reverse"> <div class="hidden flex-row-reverse">
<PrimaryButton <Popover.Root>
className="flex items-center relative" <Popover.Trigger>
onClick={() => { <PrimaryButton
openPopover = !openPopover; className="flex items-center relative"
}} onClick={() => {
isLoading={$isLoading} openPopover = !openPopover;
isDisabled={$isLoading} }}
variant={VARIANTS.OUTLINED} isLoading={$isLoading}
disableScale isDisabled={$isLoading}
> variant={VARIANTS.OUTLINED}
<WandSparklesIcon size={16} /> disableScale
AI >
<Popover <WandSparklesIcon size={16} />
caret AI
align="left" </PrimaryButton>
bind:open={openPopover} </Popover.Trigger>
on:click:outside={({ detail }) => { <Popover.Content align="start" class="w-80">
openPopover = aiButtonRef?.contains(detail.target) || false;
}}
>
<div class="p-2"> <div class="p-2">
<button class={aiButtonClass} onclick={() => callAI('outline')}> <button class={aiButtonClass} onclick={() => callAI('outline')}>
<ListChecksIcon size={16} /> <ListChecksIcon size={16} />
@@ -408,48 +417,23 @@
{$t('course.navItem.lessons.materials.tabs.note.ai.activities')} {$t('course.navItem.lessons.materials.tabs.note.ai.activities')}
</button> </button>
</div> </div>
</Popover> </Popover.Content>
</PrimaryButton> </Popover.Root>
</div> </div>
</div> </div>
<div class="mt-5 h-[60vh]"> <div class="mt-5 h-[60vh]">
<!-- <div class="bg-background z-50 mt-12 size-full max-w-5xl rounded-md border border-dashed"> <TextEditor
{#if editor && !editor.isDestroyed} content={_editorValue}
<EdraToolBar onChange={(content) => onEditorChange(content)}
class="bg-secondary/50 flex w-full items-center overflow-x-auto border-b border-dashed p-0.5" onReady={() =>
{editor} // editor
/> {
{:else} // editorInstance = editor;
<div> // editorWindowRef = editor.view.dom.ownerDocument.defaultView;
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;
}}
placeholder={$t('course.navItem.lessons.materials.tabs.note.placeholder')} placeholder={$t('course.navItem.lessons.materials.tabs.note.placeholder')}
/> --> />
</div> </div>
</TabContent> </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"> <script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import { browser } from '$app/environment'; import { goto } from '$app/navigation';
import TextChip from '$lib/components/Chip/Text.svelte'; import Plus from '@lucide/svelte/icons/plus';
import * as Sidebar from '@cio/ui/base/sidebar';
import LockIcon from '@lucide/svelte/icons/lock'; import LockIcon from '@lucide/svelte/icons/lock';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte'; import * as Collapsible from '@cio/ui/base/collapsible';
import NavExpandable from './NavExpandable.svelte'; import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { getNavItemRoute, getLessonsRoute, getLectureNo } from '$lib/components/Course/function';
import { lessons, lessonSections } from '../Lesson/store/lessons'; import UsersIcon from '@lucide/svelte/icons/users';
import { course } from '$lib/components/Course/store'; import AwardIcon from '@lucide/svelte/icons/award';
import { NavClasses } from '$lib/utils/constants/reusableClass'; import PanelTop from '@lucide/svelte/icons/panel-top';
import { isMobile } from '$lib/utils/store/useMobile'; import SettingsIcon from '@lucide/svelte/icons/settings';
import { sideBar } from '$lib/components/Org/store'; 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 { 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 { getIsLessonComplete } from '../Lesson/functions';
import { currentOrg, isFreePlan } from '$lib/utils/store/org'; import { currentOrg, isFreePlan } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations'; import { COURSE_TYPE, COURSE_VERSION } from '$lib/utils/types';
import { COURSE_TYPE, COURSE_VERSION, type Lesson } from '$lib/utils/types'; import { lessons, lessonSections } from '../Lesson/store/lessons';
import { NAV_IDS } from './constants'; 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 { interface Props {
path: string; path: string;
@@ -27,356 +42,366 @@
let { path, isStudent = false }: Props = $props(); let { path, isStudent = false }: Props = $props();
interface Section { const navItems = $derived(
id: string; [
order: number; {
label: string; id: NAV_IDS.NEWS_FEED,
lessons: Lesson[]; title: $t('course.navItems.nav_news_feed'),
isExpanded?: boolean; url: getNavItemRoute($course.id),
} isActive: (path || page.url.pathname) === getNavItemRoute($course.id),
interface NavItem { show() {
id: string; return isStudent ? $currentOrg.customization.course.newsfeed : true;
label: string; },
to: string; icon: getNavIcon(NAV_IDS.NEWS_FEED)
hideSortIcon: boolean; },
isLesson?: boolean; {
isPaidFeature: boolean; id: NAV_IDS.LESSONS,
isExpanded?: boolean; title: $t('course.navItems.nav_content'),
sections?: Section[]; url: getLessonsRoute($course.id),
show?: () => boolean; isActive:
} (path || page.url.pathname).includes('/lessons') ||
(path || page.url.pathname) === getLessonsRoute($course.id),
let resize = $state(false); isLesson: true,
let isDragging = false; icon: getNavIcon(NAV_IDS.LESSONS),
let startX: number; items:
let initialWidth: number; $course.version === COURSE_VERSION.V1
let sidebarRef: HTMLElement | undefined = $state(); ? ($lessons || []).map((lesson, index) => ({
let menuContentRef: HTMLUListElement | undefined = $state(); title: lesson.title,
url: getLessonsRoute($course.id, lesson.id),
const navItems: NavItem[] = $derived([ lesson,
{ index,
id: NAV_IDS.NEWS_FEED, isV1: true
label: $t('course.navItems.nav_news_feed'), }))
to: getNavItemRoute($course.id), : $lessonSections.flatMap((section) =>
hideSortIcon: true, section.lessons.map((lesson) => ({
isPaidFeature: false, title: lesson.title,
show() { url: getLessonsRoute($course.id, lesson.id),
return isStudent ? $currentOrg.customization.course.newsfeed : true; lesson,
} sectionTitle: section.title,
}, isV1: false
{ }))
id: NAV_IDS.LESSONS, )
label: $t('course.navItems.nav_content'), },
to: getLessonsRoute($course.id), {
hideSortIcon: false, id: NAV_IDS.ANALYTICS,
isPaidFeature: false, title: $t('course.navItems.nav_analytics'),
isLesson: true, url: getNavItemRoute($course.id, 'analytics'),
sections: $lessonSections.map((section) => ({ isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'analytics'),
...section, show() {
label: section.title, return !isStudent;
isExpanded: true },
})), icon: getNavIcon(NAV_IDS.ANALYTICS)
isExpanded: isStudent ? true : page.url.pathname.includes('/lessons') },
}, {
{ id: NAV_IDS.ATTENDANCE,
id: NAV_IDS.ANALYTICS, title: $t('course.navItems.nav_attendance'),
label: $t('course.navItems.nav_analytics'), url: getNavItemRoute($course.id, 'attendance'),
to: getNavItemRoute($course.id, 'analytics'), isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'attendance'),
isPaidFeature: false, show() {
hideSortIcon: true, if ($course.type !== COURSE_TYPE.LIVE_CLASS) return false;
show() { return true;
return !isStudent; },
} icon: getNavIcon(NAV_IDS.ATTENDANCE)
}, },
{ {
id: NAV_IDS.ATTENDANCE, id: NAV_IDS.SUBMISSIONS,
label: $t('course.navItems.nav_attendance'), title: $t('course.navItems.nav_submissions'),
to: getNavItemRoute($course.id, 'attendance'), url: getNavItemRoute($course.id, 'submissions'),
isPaidFeature: false, isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'submissions'),
hideSortIcon: true, show() {
show() { if (isStudent) return false;
if ($course.type !== COURSE_TYPE.LIVE_CLASS) return false; return true;
},
return true; icon: getNavIcon(NAV_IDS.SUBMISSIONS)
} },
}, {
{ id: NAV_IDS.MARKS,
id: NAV_IDS.SUBMISSIONS, title: $t('course.navItems.nav_marks'),
label: $t('course.navItems.nav_submissions'), url: getNavItemRoute($course.id, 'marks'),
to: getNavItemRoute($course.id, 'submissions'), isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'marks'),
hideSortIcon: true, show() {
isPaidFeature: false, if ($course.type == COURSE_TYPE.LIVE_CLASS) {
show() { return isStudent ? $currentOrg.customization.course.grading : true;
if (isStudent) return false; }
return true;
}
},
{
id: NAV_IDS.MARKS,
label: $t('course.navItems.nav_marks'),
to: getNavItemRoute($course.id, 'marks'),
isPaidFeature: false,
hideSortIcon: true,
show() {
if ($course.type == COURSE_TYPE.LIVE_CLASS) {
return isStudent ? $currentOrg.customization.course.grading : true;
}
return false;
}
},
{
id: NAV_IDS.CERTIFICATES,
label: $t('course.navItems.nav_certificates'),
to: getNavItemRoute($course.id, 'certificates'),
hideSortIcon: true,
isPaidFeature: true,
show() {
// Dont show students if org on free plan
if (isStudent && $isFreePlan) {
return false; return false;
} },
return true; icon: getNavIcon(NAV_IDS.MARKS)
},
{
id: NAV_IDS.CERTIFICATES,
title: $t('course.navItems.nav_certificates'),
url: getNavItemRoute($course.id, 'certificates'),
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'certificates'),
show() {
if (isStudent && $isFreePlan) {
return false;
}
return true;
},
icon: getNavIcon(NAV_IDS.CERTIFICATES)
},
{
id: NAV_IDS.LANDING_PAGE,
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,
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,
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())
{ );
id: NAV_IDS.LANDING_PAGE,
label: $t('course.navItems.nav_landing_page'),
to: getNavItemRoute($course.id, 'landingpage'),
hideSortIcon: true,
isPaidFeature: false,
show() {
return !isStudent;
}
},
{
id: NAV_IDS.PEOPLE,
label: $t('course.navItems.nav_people'),
to: getNavItemRoute($course.id, 'people'),
isPaidFeature: false,
hideSortIcon: true,
show() {
return !isStudent;
}
},
{
id: NAV_IDS.SETTINGS,
label: $t('course.navItems.nav_settings'),
to: getNavItemRoute($course.id, 'settings'),
hideSortIcon: true,
isPaidFeature: false,
show() {
return !isStudent;
}
}
]);
const toggleSidebar = (defaultValue?: boolean) => { function addLesson(isSection: boolean, id: string) {
$sideBar.hidden = defaultValue ?? !$sideBar.hidden; goto('/courses/' + $course.id + '/lessons');
}; $handleAddLessonWidget.open = true;
const toggleSidebarOnMobile = () => $isMobile && toggleSidebar();
function handleMainGroupClick(href: string) { if ($course.version === COURSE_VERSION.V2) {
return () => { $handleAddLessonWidget.isSection = isSection ? false : true;
goto(href);
toggleSidebarOnMobile();
};
}
function handleCursor(event: MouseEvent) { if (isSection) {
if (!resize && sidebarRef) { $handleAddLessonWidget.id = id;
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';
} }
} }
} }
function startDragging(event: MouseEvent) { const toggleSidebarOnMobile = () => $isMobile && ($sideBar.hidden = !$sideBar.hidden);
if (event.button === 0 && sidebarRef) {
event.preventDefault();
const isNearRightBorder = sidebarRef.getBoundingClientRect().right - event.clientX < 8; function getNavIcon(id: string) {
const isNearLeftBorder = event.clientX - sidebarRef.getBoundingClientRect().left < 8; if (!id) return null;
if ((isNearRightBorder || isNearLeftBorder) && event.clientX >= 0 && event.clientX <= window.innerWidth) { if (id === NAV_IDS.SECTION) {
isDragging = true; return TableOfContentsIcon;
resize = true; } else if (id === NAV_IDS.NEWS_FEED) {
startX = event.clientX; return MegaphoneIcon;
initialWidth = parseInt(getComputedStyle(sidebarRef).width, 10); } else if (id === NAV_IDS.LESSONS) {
} return ScrollTextIcon;
} } else if (id === NAV_IDS.ATTENDANCE) {
} return BookOpenCheck;
} else if (id === NAV_IDS.SUBMISSIONS) {
function stopDragging() { return SquareLibrary;
isDragging = false; } else if (id === NAV_IDS.MARKS) {
resize = false; return BookCheckIcon;
} } else if (id === NAV_IDS.PEOPLE) {
return UsersIcon;
function dragSidebar(event: MouseEvent) { } else if (id === NAV_IDS.ANALYTICS) {
if (!(window.innerWidth >= 1024)) return; return ChartLineIcon;
if (!isDragging || !sidebarRef || !menuContentRef) return; } else if (id === NAV_IDS.LANDING_PAGE) {
return PanelTop;
const deltaX = event.clientX - startX; } else if (id === NAV_IDS.CERTIFICATES) {
let newWidth = initialWidth + deltaX; return AwardIcon;
} else if (id === NAV_IDS.SETTINGS) {
if (newWidth < 150) { return SettingsIcon;
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; return null;
}
sidebarRef.removeEventListener('mousedown', startDragging);
document.removeEventListener('mousemove', dragSidebar);
document.removeEventListener('mouseup', stopDragging);
document.removeEventListener('mousemove', handleCursor);
});
</script> </script>
<aside <Sidebar.Provider class="flex w-fit items-start gap-2">
class={` <Sidebar.Root collapsible="icon" class="inset-y-12 h-[calc(100vh-48px)] {$sideBar.hidden ? 'hidden' : ''}">
${$sideBar.hidden ? 'absolute z-[40] -translate-x-[100%]' : 'absolute z-[40] translate-x-0 md:relative'} <Sidebar.Content>
h-[calc(100vh-48px)] w-[90vw] bg-gray-100 transition md:w-[300px] lg:w-[350px] dark:bg-black <Sidebar.Group>
<Sidebar.GroupLabel>Course Navigation</Sidebar.GroupLabel>
${ <Sidebar.Menu>
resize ? 'border-r-8 border-r-blue-500' : 'dark:border-r-neutral-600' {#each navItems as item (item.id)}
} border-r-1 overflow-y-auto border border-b-0 border-l-0 border-t-0`} <Collapsible.Root open={item.isActive} class="group/collapsible">
style={$sideBar.hidden === true ? 'width:0' : 'width:300px'} {#snippet child({ props })}
bind:this={sidebarRef} <Sidebar.MenuItem {...props}>
> {#if item.isLesson && item.items}
<div class="flex flex-col"> <Collapsible.Trigger>
<ul class="sidebar-content my-5" bind:this={menuContentRef} style={$sideBar.hidden === true ? '' : 'display:block'}> {#snippet child({ props })}
{#each navItems as navItem} <a href={item.url} onclick={toggleSidebarOnMobile}>
{#if !navItem.show || (typeof navItem.show === 'function' && navItem.show())} <Sidebar.MenuButton
<NavExpandable {...props}
id={navItem.id} tooltipContent={item.title}
name={navItem.id} class="flex w-full items-center gap-4 py-2 pl-1.5"
label={navItem.label} >
handleClick={handleMainGroupClick(navItem.to)} {@const Icon = item.icon}
isGroupActive={(path || page.url.pathname) === navItem.to} <Icon size={16} class="nav-icon group-hover:animate-{item.id}" />
total={navItem.isLesson ? ($lessons || []).length : 0} {item.title}
isLoading={!$course.id}
isLesson={navItem.isLesson} <div class="ml-auto flex items-center gap-1">
isPaidFeature={navItem.isPaidFeature} <Plus
isExpanded={navItem.isExpanded} size={20}
{isStudent} class="rounded-full p-1 hover:bg-gray-200"
> onclick={() => addLesson(false, item.id)}
{#if $course.version === COURSE_VERSION.V1} />
{#each $lessons as item, index}
<a <ChevronRightIcon
class="mb-2 w-[95%] pl-7 text-[0.80rem] text-black dark:text-white {isStudent && !item.is_unlocked size={20}
? 'cursor-not-allowed' class="rounded-full p-1 transition-transform duration-200 hover:bg-gray-200 group-data-[state=open]/collapsible:rotate-90"
: ''}" />
href={isStudent && !item.is_unlocked ? page.url.pathname : getLessonsRoute($course.id, item.id)} </div>
onclick={toggleSidebarOnMobile} </Sidebar.MenuButton>
aria-disabled={!item.is_unlocked} </a>
title={item.title} {/snippet}
> </Collapsible.Trigger>
<div <Collapsible.Content>
class="flex items-center px-4 py-2 {NavClasses.item} {(path || page.url.pathname).includes( <Sidebar.MenuSub>
item.id {#if $course.version === COURSE_VERSION.V1}
) && NavClasses.active}" {#each item.items as lessonItem}
> <Sidebar.MenuSubItem>
<TextChip <Sidebar.MenuSubButton
value={getLectureNo(index + 1)} isActive={(path || page.url.pathname).includes(lessonItem.lesson.id)}
className="bg-primary-200 text-primary-600 text-xs mr-2" >
size="sm" {#snippet child({ props })}
shape="rounded-full" <a
/> href={isStudent && !lessonItem.lesson.is_unlocked
<span class="line-clamp-2 w-[85%] text-ellipsis">{item.title}</span> ? page.url.pathname
<span class="grow"></span> : lessonItem.url}
{#if !item.is_unlocked} onclick={toggleSidebarOnMobile}
<span class="text-md ml-2" title="This lesson is locked."> aria-disabled={!lessonItem.lesson.is_unlocked}
<LockIcon size={16} class="carbon-icon dark:text-white" /> title={lessonItem.lesson.title}
</span> class="flex w-full items-center gap-2 {isStudent && !lessonItem.lesson.is_unlocked
{:else if getIsLessonComplete(item.lesson_completion, $profile.id)} ? 'cursor-not-allowed opacity-50'
<span class="ml-2" title="You have completed this lesson"> : ''}"
<CircleCheckIcon size={16} filled /> {...props}
</span> >
{/if} {#if lessonItem.isV1 && 'index' in lessonItem}
</div> <TextChip
</a> value={getLectureNo(lessonItem.index + 1)}
{/each} className="bg-primary-200 text-primary-600 text-xs"
{:else if navItem.sections} size="sm"
{#each navItem.sections as section} shape="rounded-full"
<NavExpandable />
id={section.id} {/if}
name={NAV_IDS.SECTION} <span class="flex-1 truncate">{lessonItem.lesson.title}</span>
label={section.label} {#if !lessonItem.lesson.is_unlocked}
isLoading={!$course.id} <LockIcon size={16} class="shrink-0" />
isSection={true} {:else if getIsLessonComplete(lessonItem.lesson.lesson_completion, $profile.id)}
isExpanded={section.isExpanded} <span class="shrink-0">
className="ml-4" <CircleCheckIcon size={16} filled />
btnPadding="py-2 px-3" </span>
{isStudent} {/if}
> </a>
{#each section.lessons as item} {/snippet}
<a </Sidebar.MenuSubButton>
class="mb-2 w-[95%] pl-7 text-[0.80rem] text-black dark:text-white {isStudent && !item.is_unlocked </Sidebar.MenuSubItem>
? 'cursor-not-allowed' {/each}
: ''}" {:else}
href={isStudent && !item.is_unlocked ? page.url.pathname : getLessonsRoute($course.id, item.id)} {#each $lessonSections as section}
onclick={toggleSidebarOnMobile} <Collapsible.Root open={true} class="group/section">
aria-disabled={!item.is_unlocked} {#snippet child({ props })}
title={item.title} <Sidebar.MenuSubItem {...props}>
> <Collapsible.Trigger>
<div {#snippet child({ props })}
class="flex items-center px-4 py-2 {NavClasses.item} {(path || page.url.pathname).includes( <Sidebar.MenuSubButton
item.id {...props}
) && NavClasses.active}" class="flex w-full items-center gap-2 font-medium"
> >
<span class="line-clamp-2 w-[85%] text-ellipsis">{item.title}</span> {@const Icon = getNavIcon(NAV_IDS.SECTION)}
<span class="grow"></span> <Icon size={14} />
{#if !item.is_unlocked} <span class="flex-1 truncate">{section.title}</span>
<span class="text-md ml-2" title="This lesson is locked."> <div class="ml-auto flex items-center gap-1">
<LockIcon size={16} class="carbon-icon dark:text-white" /> <Plus
</span> size={20}
{:else if getIsLessonComplete(item.lesson_completion, $profile.id)} class="rounded-full p-1 hover:bg-gray-200"
<span class="ml-2" title="You have completed this lesson"> onclick={() => addLesson(true, section.id)}
<CircleCheckIcon size={16} filled /> />
</span>
<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
href={isStudent && !lesson.is_unlocked
? page.url.pathname
: getLessonsRoute($course.id, lesson.id)}
onclick={toggleSidebarOnMobile}
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}
>
<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}
</a>
{/snippet}
</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
{/each}
</Sidebar.MenuSub>
</Collapsible.Content>
</Sidebar.MenuSubItem>
{/snippet}
</Collapsible.Root>
{/each}
{/if} {/if}
</div> </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> </a>
{/each} {/if}
</NavExpandable> </Sidebar.MenuItem>
{/each} {/snippet}
{/if} </Collapsible.Root>
</NavExpandable> {/each}
{/if} </Sidebar.Menu>
{/each} </Sidebar.Group>
</ul> </Sidebar.Content>
</div> <Sidebar.Rail />
</aside> </Sidebar.Root>
<Sidebar.Inset>
<Sidebar.Trigger />
</Sidebar.Inset>
</Sidebar.Provider>
<style>
a {
text-decoration: none;
}
</style>
@@ -1,16 +1,17 @@
<script lang="ts"> <script lang="ts">
import Modal from '$lib/components/Modal/index.svelte'; import { t } from '$lib/utils/functions/translations';
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 { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import type { Feed, Author } from '$lib/utils/types/feed'; import type { Feed, Author } from '$lib/utils/types/feed';
import { NOTIFICATION_NAME, triggerSendEmail } from '$lib/utils/services/notification/notification'; import { createNewFeed } from '$lib/utils/services/newsfeed';
import { t } from '$lib/utils/functions/translations';
import { createNewsfeedValidation } from '$lib/utils/functions/validator';
import { getTextFromHTML } from '$lib/utils/functions/toHtml'; 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 { interface Props {
author?: Author | any; author?: Author | any;
@@ -123,12 +124,12 @@
> >
<section class="w-2/ flex h-full flex-col rounded-xl pb-3"> <section class="w-2/ flex h-full flex-col rounded-xl pb-3">
<TextEditor <TextEditor
value={newPost} content={newPost}
onChange={(text) => { onChange={(text) => {
newPost = text; newPost = text;
}} }}
editorClass="max-h-[400px]"
placeholder={$t('course.navItem.news_feed.heading_button.placeholder')} placeholder={$t('course.navItem.news_feed.heading_button.placeholder')}
maxHeight={400}
/> />
{#if errors.newPost} {#if errors.newPost}
<p class="text-sm text-red-500">{errors.newPost}</p> <p class="text-sm text-red-500">{errors.newPost}</p>
@@ -1,20 +1,23 @@
<script lang="ts"> <script lang="ts">
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte'; import { onMount } from 'svelte';
import SendHorizontalIcon from '@lucide/svelte/icons/send-horizontal'; import pluralize from 'pluralize';
import Chip from '$lib/components/Chip/index.svelte';
import UsersIcon from '@lucide/svelte/icons/users'; 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 { calDateDiff } from '$lib/utils/functions/date';
import type { Author, Feed } from '$lib/utils/types/feed'; 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 DeleteFeedConfirmation from './DeleteFeedConfirmation.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.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 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 { interface Props {
feed: Feed; feed: Feed;
@@ -138,11 +141,22 @@
</span> </span>
</span> </span>
<RoleBasedSecurity allowedRoles={[1, 2]}> <RoleBasedSecurity allowedRoles={[1, 2]}>
<OverflowMenu flipped> <DropdownMenu.Root>
<OverflowMenuItem text={feed.isPinned ? 'Unpin' : 'Pin'} on:click={() => onPin(feed.id, feed.isPinned)} /> <DropdownMenu.Trigger
<OverflowMenuItem text="Edit" on:click={openEditFeed} /> class="flex h-8 w-8 items-center justify-center rounded-md hover:bg-gray-100 dark:hover:bg-neutral-700"
<OverflowMenuItem danger text="Delete" on:click={() => (isDeleteFeedModal = true)} /> >
</OverflowMenu> <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> </RoleBasedSecurity>
</div> </div>
{#if !isHtmlValueEmpty(feed.content)} {#if !isHtmlValueEmpty(feed.content)}
@@ -207,9 +221,18 @@
</span> </span>
{#if comment.author?.profile?.id === $profile.id || $isOrgAdmin} {#if comment.author?.profile?.id === $profile.id || $isOrgAdmin}
<OverflowMenu flipped class="hidden group-hover:flex"> <DropdownMenu.Root>
<OverflowMenuItem danger text="Delete" on:click={() => handleDeleteComment(comment.id)} /> <DropdownMenu.Trigger
</OverflowMenu> 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} {/if}
</div> </div>
{/if} {/if}
@@ -1,11 +1,13 @@
<script> <script>
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte'; import { Skeleton } from '@cio/ui/base/skeleton';
</script> </script>
<div class="mb-7 flex max-w-3xl flex-col gap-5 rounded-md border border-gray-300 p-4"> <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"> <div class="flex items-center gap-2">
<SkeletonPlaceholder style="height: 2rem; width: 2rem;" class="overflow-hidden rounded-full" /> <Skeleton class="h-8 w-8 overflow-hidden rounded-full" />
<SkeletonText width="30%" /> <Skeleton class="h-4 w-[30%]" />
</div> </div>
<SkeletonText paragraph lines={3} /> <Skeleton class="h-4 w-full" />
<Skeleton class="h-4 w-full" />
<Skeleton class="h-4 w-full" />
</div> </div>
@@ -1,28 +1,32 @@
<script lang="ts"> <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 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 { 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 Modal from '$lib/components/Modal/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/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 { interface Tutor {
id: number; id: number;
@@ -32,9 +36,10 @@
} }
let tutors: Tutor[] = $state([]); let tutors: Tutor[] = $state([]);
let selectedIds: Array<number> = $state([]); let selectedIds: string[] = $state([]);
let selectedTutors: Tutor[] = $state([]);
let isLoadingTutors = $state(false); let isLoadingTutors = $state(false);
const selectedTutors = $derived(tutors.filter((tutor) => selectedIds.includes(tutor.id.toString())));
let copied = $state(false); let copied = $state(false);
let qrImage = $state(''); let qrImage = $state('');
let isLoadingQRDownload = $state(false); let isLoadingQRDownload = $state(false);
@@ -192,18 +197,47 @@
<form onsubmit={preventDefault(onSubmit)}> <form onsubmit={preventDefault(onSubmit)}>
<div class="mb-8"> <div class="mb-8">
<p class="mb-1 text-base font-semibold">{$t('course.navItem.people.invite_modal.invite')}</p> <p class="mb-1 text-base font-semibold">{$t('course.navItem.people.invite_modal.invite')}</p>
<MultiSelect <Select.Root type="multiple" bind:value={selectedIds} disabled={isLoadingTutors}>
disabled={isLoadingTutors} <Select.Trigger class="w-full">
label={$t('course.navItem.people.invite_modal.select')} <div>
bind:selectedIds {#if selectedTutors.length > 0}
items={tutors} <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}
{$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} {#if isLoadingTutors}
<span> <div class="mt-2 flex items-center gap-2">
<Loading withOverlay={false} small /> <Spinner class="h-4 w-4" />
</span> <span class="text-sm">Loading...</span>
</div>
{:else} {:else}
<span> <span class="mt-2 block text-sm">
{$t('course.navItem.people.invite_modal.to_add')} {$t('course.navItem.people.invite_modal.to_add')}
<a href={`/org/${$currentOrg.siteName}/settings/teams`} class="text-primary-600 underline"> <a href={`/org/${$currentOrg.siteName}/settings/teams`} class="text-primary-600 underline">
{$t('course.navItem.people.invite_modal.go_to')} {$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> <p class=" text-sm">{$t('course.navItem.people.invite_modal.via_link')}</p>
</div> </div>
<div class="relative"> <Popover.Root>
<button type="button" onclick={copyLink} class="text-primary-800 cursor-pointer font-bold capitalize underline"> <Popover.Trigger>
{$t('course.navItem.people.invite_modal.copy_link')} <button type="button" onclick={copyLink} class="text-primary-800 cursor-pointer capitalize underline">
</button> {$t('course.navItem.people.invite_modal.copy_link')}
</button>
<Popover caret open={copied} align="left"> </Popover.Trigger>
<div style="padding: 5px">{$t('course.navItem.people.invite_modal.success')}</div> <Popover.Content align="start" class="w-auto">
</Popover> <div class="p-1 text-sm">{$t('course.navItem.people.invite_modal.success')}</div>
</div> </Popover.Content>
</Popover.Root>
</div> </div>
<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" 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="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" /> <img src={qrImage} alt="qrcode" />
<div class="pb-4 pt-1"> <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> <p class="mt-1 text-lg font-semibold text-black">{currentOrg.name}</p>
</div> </div>
</div> </div>
{#if $isFreePlan} {#if $isFreePlan}
<div class="relative top-16 flex items-center gap-x-2 rounded-md bg-white px-4 py-2"> <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" /> <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> </div>
{/if} {/if}
</div> </div>
@@ -1,34 +1,41 @@
<script lang="ts"> <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 { 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 TextArea from '$lib/components/Form/TextArea.svelte';
import TextField from '$lib/components/Form/TextField.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 SectionTitle from '$lib/components/Org/SectionTitle.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import Grid from '$lib/components/Org/Settings/Layout/Grid.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 UpgradeBanner from '$lib/components/Upgrade/Banner.svelte'; import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
import UploadWidget from '$lib/components/UploadWidget/index.svelte'; import UploadWidget from '$lib/components/UploadWidget/index.svelte';
import generateSlug from '$lib/utils/functions/generateSlug'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { isObject } from '$lib/utils/functions/isObject'; import Column from '$lib/components/Org/Settings/Layout/Column.svelte';
import { t } from '$lib/utils/functions/translations'; import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
import { deleteCourse, updateCourse } from '$lib/utils/services/courses';
import { currentOrg, currentOrgDomain, currentOrgPath, isFreePlan } from '$lib/utils/store/org'; import { settings } from './store';
import Copy from '@lucide/svelte/icons/copy';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
import { COURSE_TYPE } from '$lib/utils/types'; import { COURSE_TYPE } from '$lib/utils/types';
import { lessons } from '../Lesson/store/lessons'; 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 isSaving = $state(false);
let isLoading = $state(false); let isLoading = $state(false);
@@ -284,11 +291,23 @@
<ArrowUpRightIcon size={16} /> <ArrowUpRightIcon size={16} />
</IconButton> </IconButton>
</p> </p>
{#if $course.slug} <div class="flex items-center justify-between rounded-md border p-3">
<CodeSnippet wrapText type="multi" code={courseLink} /> {#if $course.slug}
{:else} <p class="text-sm">{courseLink}</p>
<CodeSnippet code="Setup landing page to get course link" />
{/if} <IconButton
contained={true}
size="small"
onClick={() => {
copyToClipboard(courseLink);
}}
>
<Copy size={16} />
</IconButton>
{:else}
<p class="text-sm">Setup landing page to get course link</p>
{/if}
</div>
</div> </div>
</Column> </Column>
</Row> </Row>
@@ -327,16 +346,18 @@
{#if $isFreePlan} {#if $isFreePlan}
<UpgradeBanner>{$t('upgrade.download_lessons')}</UpgradeBanner> <UpgradeBanner>{$t('upgrade.download_lessons')}</UpgradeBanner>
{:else} {:else}
<Toggle <div class="flex items-center space-x-2">
size="sm" <Switch
bind:toggled={$settings.lesson_download} id="lesson-download"
on:toggle={() => { bind:checked={$settings.lesson_download}
hasUnsavedChanges = true; 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> <Label for="lesson-download" class="text-sm text-gray-500">
</Toggle> {$settings.lesson_download ? $t('course.navItem.settings.enabled') : $t('course.navItem.settings.disabled')}
</Label>
</div>
{/if} {/if}
</Column> </Column>
</Row> </Row>
@@ -345,7 +366,7 @@
<SectionTitle>{$t('course.navItem.settings.course_download')}</SectionTitle> <SectionTitle>{$t('course.navItem.settings.course_download')}</SectionTitle>
<p>{$t('course.navItem.settings.course_avail')}</p> <p>{$t('course.navItem.settings.course_avail')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={4} md={4} lg={4}>
{#if $isFreePlan} {#if $isFreePlan}
<UpgradeBanner>{$t('upgrade.download_course')}</UpgradeBanner> <UpgradeBanner>{$t('upgrade.download_course')}</UpgradeBanner>
{:else} {:else}
@@ -366,17 +387,23 @@
<p>{$t('course.navItem.settings.course_type_desc')}</p> <p>{$t('course.navItem.settings.course_type_desc')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<RadioButtonGroup <RadioGroup.Root
hideLegend value={$settings.type}
bind:selected={$settings.type} onValueChange={(value) => {
on:click={() => { $settings.type = value as COURSE_TYPE;
if (hasUnsavedChanges) return; if (hasUnsavedChanges) return;
hasUnsavedChanges = true; hasUnsavedChanges = true;
}} }}
> >
<RadioButton labelText={$t('course.navItem.settings.live_class')} value={COURSE_TYPE.LIVE_CLASS} /> <div class="mb-3 flex items-center space-x-2">
<RadioButton labelText={$t('course.navItem.settings.self_paced')} value={COURSE_TYPE.SELF_PACED} /> <RadioGroup.Item value={COURSE_TYPE.LIVE_CLASS} id="live-class" />
</RadioButtonGroup> <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> </Column>
</Row> </Row>
@@ -386,16 +413,20 @@
<p>{$t('course.navItem.settings.access')}</p> <p>{$t('course.navItem.settings.access')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<Toggle <div class="flex items-center space-x-2">
size="sm" <Switch
bind:toggled={$settings.allow_new_students} id="allow-new-students"
on:click={() => { bind:checked={$settings.allow_new_students}
hasUnsavedChanges = true; 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> <Label for="allow-new-students" class="text-sm text-gray-500">
</Toggle> {$settings.allow_new_students
? $t('course.navItem.settings.enabled')
: $t('course.navItem.settings.disabled')}
</Label>
</div>
</Column> </Column>
</Row> </Row>
@@ -406,22 +437,22 @@
<p>{$t('course.navItem.settings.determines')}</p> <p>{$t('course.navItem.settings.determines')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<Toggle <div class="flex items-center space-x-2">
size="sm" <Switch
bind:toggled={$settings.is_published} id="is-published"
on:click={() => { bind:checked={$settings.is_published}
hasUnsavedChanges = true; onCheckedChange={(checked) => {
}} hasUnsavedChanges = true;
on:toggle={(e) => { $settings.allow_new_students = checked;
$settings.allow_new_students = e.detail.toggled; if (!$course.slug) {
if (!$course.slug) { generateNewCourseLink();
generateNewCourseLink(); }
} }}
}} />
> <Label for="is-published" class="text-sm text-gray-500">
<span slot="labelA" style="color: gray">{$t('course.navItem.settings.unpublished')}</span> {$settings.is_published ? $t('course.navItem.settings.published') : $t('course.navItem.settings.unpublished')}
<span slot="labelB" style="color: gray">{$t('course.navItem.settings.published')}</span> </Label>
</Toggle> </div>
</Column> </Column>
</Row> </Row>
@@ -440,12 +471,12 @@
/> />
</Column> </Column>
</Row> </Row>
<Row class="flex w-full items-center justify-end p-5"> <div class="flex w-full items-center justify-end p-5">
<PrimaryButton <PrimaryButton
label={$t('course.navItem.settings.save')} label={$t('course.navItem.settings.save')}
isLoading={isSaving} isLoading={isSaving}
isDisabled={isSaving} isDisabled={isSaving}
onClick={handleSave} onClick={handleSave}
/> />
</Row> </div>
</Grid> </Grid>
@@ -131,8 +131,8 @@ export async function setCourse(data: Course, setLesson = true) {
people: [] people: []
}; };
if (Array.isArray(groupData.members)) { if (Array.isArray(data.group.members)) {
for (const member of groupData.members) { for (const member of data.group.members) {
if (member.role_id === ROLE.STUDENT) { if (member.role_id === ROLE.STUDENT) {
groupData.students.push(member); groupData.students.push(member);
} else if (member.profile) { } 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); group.set(groupData);
} }
@@ -1,12 +1,15 @@
<script lang="ts"> <script lang="ts">
import get from 'lodash/get'; 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 { 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 { interface Props {
course: Course; course: Course;
@@ -45,20 +48,24 @@
}); });
</script> </script>
<div class="border-bottom-c mt-5"> <div class="flex flex-col items-center">
<p class="font-bold"> <div class="border-bottom-c mt-5">
{$t('course.navItem.landing_page.editor.certificate_form.upload_template')} <p class="text-center font-bold">
</p> {$t('course.navItem.landing_page.editor.certificate_form.upload_template')}
<UploadImage bind:avatar src={templateUrl} shape="rounded-none" bind:isUploading /> </p>
</div> <UploadImage bind:avatar src={templateUrl} shape="rounded-none" bind:isUploading />
</div>
<div class="mt-5"> <div class="mt-5 flex flex-col gap-2">
<p class="font-bold"> <p class="font-semibold">
{$t('course.navItem.landing_page.editor.display_section')} {$t('course.navItem.landing_page.editor.display_section')}
</p> </p>
<Toggle bind:toggled={show} size="sm"> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span> <Switch bind:checked={show} />
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span> <Label class="text-gray-600">
</Toggle> {show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
</Label>
</div>
</div>
</div> </div>
@@ -1,10 +1,13 @@
<script lang="ts"> <script lang="ts">
import TextEditor from '$lib/components/TextEditor/index.svelte';
import get from 'lodash/get'; 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 type { Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
import { NAV_ITEM_KEY } from '../../constants'; import { NAV_ITEM_KEY } from '../../constants';
import { t } from '$lib/utils/functions/translations';
import TextEditor from '$lib/components/TextEditor/index.svelte';
interface Props { interface Props {
course: Course; course: Course;
@@ -21,16 +24,19 @@
</script> </script>
<div> <div>
<TextEditor bind:value={course.metadata.description} /> <TextEditor content={course.metadata.description} onChange={(content) => setter(content, `metadata.description`)} />
</div> </div>
<div class="mt-5"> <div class="mt-5 flex flex-col gap-2">
<p class="font-bold"> <p class="font-semibold">
{$t('course.navItem.landing_page.editor.display_section')} {$t('course.navItem.landing_page.editor.display_section')}
</p> </p>
<Toggle bind:toggled={show} size="sm"> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span> <Switch bind:checked={show} />
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
</Toggle> <Label class="font-bold">
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
</Label>
</div>
</div> </div>
@@ -1,10 +1,13 @@
<script lang="ts"> <script lang="ts">
import TextEditor from '$lib/components/TextEditor/index.svelte';
import get from 'lodash/get'; 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 type { Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
import { NAV_ITEM_KEY } from '../../constants'; import { NAV_ITEM_KEY } from '../../constants';
import { t } from '$lib/utils/functions/translations';
import TextEditor from '$lib/components/TextEditor/index.svelte';
interface Props { interface Props {
course: Course; course: Course;
@@ -21,16 +24,19 @@
</script> </script>
<div class=""> <div class="">
<TextEditor bind:value={course.metadata.goals} /> <TextEditor content={course.metadata.goals} onChange={(content) => setter(content, `metadata.goals`)} />
</div> </div>
<div class="mt-5"> <div class="mt-5 flex flex-col gap-2">
<p class="font-bold"> <p class="font-semibold">
{$t('course.navItem.landing_page.editor.display_section')} {$t('course.navItem.landing_page.editor.display_section')}
</p> </p>
<Toggle bind:toggled={show} size="sm"> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span> <Switch bind:checked={show} />
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
</Toggle> <Label class="text-gray-600">
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
</Label>
</div>
</div> </div>
@@ -38,7 +38,7 @@
bind:value={course.metadata.videoUrl} bind:value={course.metadata.videoUrl}
/> />
<div class="mt-7"> <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 <PrimaryButton
label={$t('course.navItem.landing_page.editor.header_form.replace')} label={$t('course.navItem.landing_page.editor.header_form.replace')}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
@@ -1,12 +1,15 @@
<script lang="ts"> <script lang="ts">
import get from 'lodash/get'; 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 TextField from '$lib/components/Form/TextField.svelte';
import TextEditor from '$lib/components/TextEditor/index.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 { interface Props {
course: Course; course: Course;
@@ -20,8 +23,8 @@
let showDiscount = $derived(get(course, 'metadata.showDiscount', false)); let showDiscount = $derived(get(course, 'metadata.showDiscount', false));
let giftToggled = $derived(get(course, 'metadata.reward.show', false)); let giftToggled = $derived(get(course, 'metadata.reward.show', false));
function handleChange(html: string) { function handleChange(content: string) {
setter(html, 'metadata.reward.description'); setter(content, 'metadata.reward.description');
} }
$effect(() => { $effect(() => {
@@ -39,10 +42,18 @@
</script> </script>
{#if typeof course !== 'undefined'} {#if typeof course !== 'undefined'}
<Select labelText={$t('course.navItem.landing_page.editor.pricing_form.currency')} bind:selected={course.currency}> <div>
<SelectItem value="NGN" /> <Label class="mb-2 font-bold">{$t('course.navItem.landing_page.editor.pricing_form.currency')}</Label>
<SelectItem value="USD" /> <Select.Root type="single" value={course.currency} onValueChange={(value) => setter(value, 'currency')}>
</Select> <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 <TextField
className="mt-5" className="mt-5"
@@ -63,11 +74,17 @@
{/if} {/if}
<div class="mt-5"> <div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> --> <div class="mb-2">
<Toggle labelText={$t('course.navItem.landing_page.editor.pricing_form.discount')} bind:toggled={showDiscount}> <Label class="font-bold">{$t('course.navItem.landing_page.editor.pricing_form.discount')}</Label>
<span slot="labelA">{$t('course.navItem.landing_page.editor.pricing_form.no')}</span> </div>
<span slot="labelB">{$t('course.navItem.landing_page.editor.pricing_form.yes')}</span> <div class="flex items-center space-x-2">
</Toggle> <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> </div>
{#if showDiscount} {#if showDiscount}
@@ -82,20 +99,30 @@
{/if} {/if}
<div class="mt-5"> <div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> --> <div class="mb-2">
<Toggle labelText="Gift on Completion" bind:toggled={giftToggled}> <Label class="font-bold">Gift on Completion</Label>
<span slot="labelA">{$t('course.navItem.landing_page.editor.pricing_form.no')}</span> </div>
<span slot="labelB">{$t('course.navItem.landing_page.editor.pricing_form.yes')}</span> <div class="flex items-center space-x-2">
</Toggle> <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> </div>
{#if giftToggled} {#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')} {$t('course.navItem.landing_page.editor.pricing_form.gift')}
</p> </p>
<div class="h-2/5"> <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> </div>
{/if} {/if}
{/if} {/if}
@@ -1,10 +1,13 @@
<script lang="ts"> <script lang="ts">
import TextEditor from '$lib/components/TextEditor/index.svelte';
import get from 'lodash/get'; 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 type { Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
import { NAV_ITEM_KEY } from '../../constants'; import { NAV_ITEM_KEY } from '../../constants';
import { t } from '$lib/utils/functions/translations';
import TextEditor from '$lib/components/TextEditor/index.svelte';
interface Props { interface Props {
course: Course; course: Course;
@@ -21,16 +24,19 @@
</script> </script>
<div> <div>
<TextEditor bind:value={course.metadata.requirements} /> <TextEditor content={course.metadata.requirements} onChange={(content) => setter(content, `metadata.requirements`)} />
</div> </div>
<div class="mt-5"> <div class="mt-5 flex flex-col gap-2">
<p class="font-bold"> <p class="font-semibold">
{$t('course.navItem.landing_page.editor.display_section')} {$t('course.navItem.landing_page.editor.display_section')}
</p> </p>
<Toggle bind:toggled={show} size="sm"> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span> <Switch bind:checked={show} />
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
</Toggle> <Label class="text-gray-600">
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
</Label>
</div>
</div> </div>
@@ -1,13 +1,16 @@
<script lang="ts"> <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 SaveIcon from '@lucide/svelte/icons/save';
import TrashIcon from '@lucide/svelte/icons/trash'; 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 { IconButton } from '$lib/components/IconButton';
import UploadImage from '$lib/components/UploadImage/index.svelte';
import TextArea from '$lib/components/Form/TextArea.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { uploadAvatar } from '$lib/utils/services/courses'; import UploadImage from '$lib/components/UploadImage/index.svelte';
import { t } from '$lib/utils/functions/translations';
let { courseId, reviews = $bindable([]), review = $bindable({}), errors = {}, onExpand = () => {} } = $props(); let { courseId, reviews = $bindable([]), review = $bindable({}), errors = {}, onExpand = () => {} } = $props();
@@ -53,10 +56,10 @@
errorMessage={errors.description} errorMessage={errors.description}
isAIEnabled={true} isAIEnabled={true}
initAIPrompt="Generate a 20 word review from a student saying good things about me teaching" 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 --> <!-- Rating -->
<TextField <TextField
className="mt-2 w-20" className="mt-2 w-20"
@@ -73,12 +76,12 @@
/> />
<!-- Toggle --> <!-- Toggle -->
<div class="w-24"> <div class="flex w-24 flex-col items-center gap-2 space-x-2">
<Toggle <Switch bind:checked={review.hide} />
labelText={$t('course.navItem.landing_page.editor.reviews_form.hide')}
size="sm" <Label class="text-sm">
bind:toggled={review.hide} {review.hide ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
/> </Label>
</div> </div>
</div> </div>
@@ -1,14 +1,13 @@
<script lang="ts"> <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 { 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 { 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 { interface Props {
open?: boolean; open?: boolean;
paymentLink?: string; paymentLink?: string;
@@ -104,7 +103,7 @@
{:else if step === STEPS.STEP_2} {: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> <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"> <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> </div>
{/if} {/if}
</Modal> </Modal>
@@ -1,35 +1,34 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition';
import get from 'lodash/get'; import get from 'lodash/get';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { page } from '$app/state'; import { page } from '$app/state';
import { fade } from 'svelte/transition';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import PlayIcon from '@lucide/svelte/icons/play'; import PlayIcon from '@lucide/svelte/icons/play';
import { ImageLoader, InlineLoading } from 'carbon-components-svelte';
import { getLectureNo } from '../Course/function'; import { getLectureNo } from '../Course/function';
import { currentOrg } from '$lib/utils/store/org'; 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 { t } from '$lib/utils/functions/translations';
import { calDateDiff } from '$lib/utils/functions/date'; import { calDateDiff } from '$lib/utils/functions/date';
import { handleOpenWidget, reviewsModalStore } from './store'; import { handleOpenWidget, reviewsModalStore } from './store';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { COURSE_VERSION, type Course } from '$lib/utils/types'; 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 Chip from '../Chip/index.svelte';
import Modal from '../Modal/index.svelte'; import Modal from '../Modal/index.svelte';
import ImageRenderer from '../Org/ImageRenderer.svelte';
import Avatar from '$lib/components/Avatar/index.svelte'; import Avatar from '$lib/components/Avatar/index.svelte';
import PricingSection from './components/PricingSection.svelte'; import PricingSection from './components/PricingSection.svelte';
import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte'; import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte';
import SectionsDisplay from './components/SectionsDisplay.svelte';
import UploadWidget from '$lib/components/UploadWidget/index.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 HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { observeIntersection } from './components/IntersectionObserver'; 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 { interface Props {
editMode?: boolean; 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"> <div class="flex w-full flex-col-reverse items-center justify-between md:w-5/6 md:flex-row">
<!-- Course Description --> <!-- Course Description -->
<div class="w-11/12 py-10 md:w-2/5"> <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', '')} {get(courseData, 'title', '')}
</h1> </h1>
<p class="text-md mb-6 text-white dark:text-white"> <p class="text-md mb-6 text-white dark:text-white">
@@ -218,7 +217,7 @@
transition:fade={{ delay: 250, duration: 300 }} transition:fade={{ delay: 250, duration: 300 }}
class="mt-8 border-b border-gray-300 pb-10" 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')} {$t('course.navItem.landing_page.requirement')}
</h3> </h3>
@@ -235,7 +234,7 @@
transition:fade={{ delay: 250, duration: 300 }} transition:fade={{ delay: 250, duration: 300 }}
class="mt-8 border-b border-gray-300 pb-10" 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')} {$t('course.navItem.landing_page.description')}
</h3> </h3>
@@ -248,7 +247,7 @@
<!-- Sections - Goal --> <!-- Sections - Goal -->
{#if navItemKeys.includes(NAV_ITEM_KEY.GOALS)} {#if navItemKeys.includes(NAV_ITEM_KEY.GOALS)}
<section id="goals" transition:fade={{ delay: 250, duration: 300 }} class="mt-8 pb-10"> <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"> <ul class="list font-light">
<HtmlRender>{@html get(courseData, 'metadata.goals', '')}</HtmlRender> <HtmlRender>{@html get(courseData, 'metadata.goals', '')}</HtmlRender>
</ul> </ul>
@@ -262,21 +261,16 @@
transition:fade={{ delay: 250, duration: 300 }} transition:fade={{ delay: 250, duration: 300 }}
class="mt-8 border-b border-gray-300 pb-10" 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"> <p class="mb-3 text-sm font-light dark:text-white">
{$t('course.navItem.landing_page.certificate_text')} {$t('course.navItem.landing_page.certificate_text')}
</p> </p>
<ImageLoader <ImageRenderer
class="certificate-img max-h-[215px]"
src={certificate?.templateUrl} src={certificate?.templateUrl}
alt="certificate template" alt="certificate template"
> className="certificate-img max-h-[215px]"
<svelte:fragment slot="loading"> />
<InlineLoading />
</svelte:fragment>
<svelte:fragment slot="error">An error occurred.</svelte:fragment>
</ImageLoader>
</section> </section>
{/if} {/if}
@@ -284,7 +278,7 @@
{#if courseData.version === COURSE_VERSION.V1} {#if courseData.version === COURSE_VERSION.V1}
<section id="lessons" class="mt-8 border-b border-gray-300 pb-10"> <section id="lessons" class="mt-8 border-b border-gray-300 pb-10">
<div class="mb-3 flex w-full items-center justify-between"> <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')} {$t('course.navItem.landing_page.content')}
</h3> </h3>
<p class="text-sm font-light dark:text-white"> <p class="text-sm font-light dark:text-white">
@@ -391,7 +385,7 @@
<div class="flex"> <div class="flex">
<!-- ratings --> <!-- ratings -->
<div class="w-1/3"> <div class="w-1/3">
<h2 class="text-xl font-bold"> <h2 class="text-xl">
{averageRating} {averageRating}
{$t('course.navItem.landing_page.reviews_modal.rating')} {$t('course.navItem.landing_page.reviews_modal.rating')}
</h2> </h2>
@@ -439,7 +433,7 @@
<!-- Sections - Instructor --> <!-- Sections - Instructor -->
<section id="instructor" class="mt-8 pb-10"> <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')} {$t('course.navItem.landing_page.instructor')}
</h3> </h3>
<div class="mb-4 flex items-center"> <div class="mb-4 flex items-center">
@@ -1,21 +1,23 @@
<script lang="ts"> <script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte'; import { Skeleton } from '@cio/ui/base/skeleton';
</script> </script>
<div class="root relative mb-4 mr-4 w-72 rounded-md border bg-gray-100 transition ease-in-out dark:bg-black"> <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"> <div class="p-3 pb-0">
<!-- Title --> <!-- Title -->
<SkeletonText style="width: 50%;" /> <Skeleton class="h-4 w-2/4" />
<!-- Description --> <!-- Description -->
<SkeletonText style="width: 100%; margin-top: 1rem;" /> <Skeleton class="h-4 w-2/4" />
<SkeletonText style="width: 100%; margin-bottom: 1rem;" /> <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"> <div class="mb-3 flex justify-between">
<SkeletonText style="width: 25%" /> <Skeleton class="h-4 w-1/4" />
<SkeletonText style="width: 25%" />
</div> </div>
<SkeletonText style="width: 25%" /> <Skeleton class="h-4 w-1/4" />
</div> </div>
</div> </div>
@@ -1,15 +1,20 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; 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 { VARIANTS } from '$lib/components/PrimaryButton/constants';
import ImageRenderer from '$lib/components/Org/ImageRenderer.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.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 { calcCourseDiscount } from '$lib/utils/functions/course';
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter'; 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'; import { copyCourseModal, deleteCourseModal } from '$lib/components/Courses/store';
interface Props { interface Props {
@@ -129,30 +134,37 @@
<div class="p-4"> <div class="p-4">
<div class="relative mb-5"> <div class="relative mb-5">
{#if !isLMS && !isOnLandingPage} {#if !isLMS && !isOnLandingPage}
<OverflowMenu <DropdownMenu.Root>
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" <DropdownMenu.Trigger
size="sm" 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"
on:click={(e) => e.stopPropagation()} onclick={(e) => e.stopPropagation()}
> >
<OverflowMenuItem text={$t('courses.course_card.context_menu.clone')} on:click={handleCloneCourse} /> <EllipsisVerticalIcon size={16} />
<OverflowMenuItem text={$t('courses.course_card.context_menu.share')} on:click={handleShareCourse} /> </DropdownMenu.Trigger>
<OverflowMenuItem text={$t('courses.course_card.context_menu.invite')} on:click={handleInvite} /> <DropdownMenu.Content align="end">
<OverflowMenuItem danger text={$t('courses.course_card.context_menu.delete')} on:click={handleDeleteCourse} /> <DropdownMenu.Item onclick={handleCloneCourse}>
</OverflowMenu> {$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} {/if}
<ImageLoader <ImageRenderer
src={bannerImage} src={bannerImage}
alt="Course Logo" alt="Banner Image"
class="relative h-[200px] w-full rounded dark:border dark:border-neutral-600" className="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>
{#if type} {#if type}
{@const tag = COURSE_TAG[type]} {@const tag = COURSE_TAG[type]}
<span <span
@@ -203,13 +215,13 @@
</div> </div>
{/if} {/if}
{:else} {:else}
<Tag type={isPublished ? 'green' : 'cool-gray'}> <Badge variant={isPublished ? 'default' : 'secondary'}>
{#if isPublished} {#if isPublished}
{$t('courses.course_card.published')} {$t('courses.course_card.published')}
{:else} {:else}
{$t('courses.course_card.unpublished')} {$t('courses.course_card.unpublished')}
{/if} {/if}
</Tag> </Badge>
{/if} {/if}
</p> </p>
</div> </div>
@@ -1,7 +1,7 @@
<script> <script>
import { SkeletonText } from 'carbon-components-svelte'; import { Skeleton } from '@cio/ui/base/skeleton';
</script> </script>
<div class="my-2"> <div class="my-2">
<SkeletonText heading /> <Skeleton class="h-4 w-full" />
</div> </div>
@@ -1,7 +1,11 @@
<script lang="ts"> <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 { 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'; import { t } from '$lib/utils/functions/translations';
interface Props { interface Props {
@@ -49,37 +53,48 @@
} }
</script> </script>
<StructuredListRow label for="row-{id}" on:click={() => goto(`/courses/${id}`)}> <Table.Row class="cursor-pointer" onclick={() => goto(`/courses/${id}`)}>
<StructuredListCell><p class="font-semibold">{title}</p></StructuredListCell> <Table.Cell class="truncate"><p class="font-semibold">{title}</p></Table.Cell>
<StructuredListCell> <Table.Cell class="truncate">
<p class="line-clamp-2">{description}</p> <p>{description}</p>
</StructuredListCell> </Table.Cell>
{#if !$isMobile} {#if !$isMobile}
<StructuredListCell>{type}</StructuredListCell> <Table.Cell class="truncate">{type}</Table.Cell>
<StructuredListCell>{totalLessons}</StructuredListCell> <Table.Cell>{totalLessons}</Table.Cell>
<StructuredListCell>{totalStudents}</StructuredListCell> <Table.Cell>{totalStudents}</Table.Cell>
<StructuredListCell> <Table.Cell>
<Tag class="break-normal" type={isPublished ? 'green' : 'cool-gray'}> <Badge variant={isPublished ? 'default' : 'secondary'}>
{#if isPublished} {#if isPublished}
{$t('courses.course_card.published')} {$t('courses.course_card.published')}
{:else} {:else}
{$t('courses.course_card.unpublished')} {$t('courses.course_card.unpublished')}
{/if} {/if}
</Tag> </Badge>
</StructuredListCell> </Table.Cell>
{/if} {/if}
<StructuredListCell> <Table.Cell class="text-center">
<OverflowMenu <DropdownMenu.Root>
id="course-list" <DropdownMenu.Trigger
flipped class="flex items-center justify-center rounded-md p-1 hover:bg-gray-100"
on:click={(e) => { onclick={(e) => e.stopPropagation()}
e.stopPropagation(); >
}} <EllipsisVerticalIcon size={16} />
> </DropdownMenu.Trigger>
<OverflowMenuItem text={$t('courses.course_card.context_menu.clone')} on:click={handleCloneCourse} /> <DropdownMenu.Content align="end">
<OverflowMenuItem text={$t('courses.course_card.context_menu.share')} on:click={handleShareCourse} /> <DropdownMenu.Item onclick={handleCloneCourse}>
<OverflowMenuItem text={$t('courses.course_card.context_menu.invite')} on:click={handleInvite} /> {$t('courses.course_card.context_menu.clone')}
<OverflowMenuItem danger text={$t('courses.course_card.context_menu.delete')} on:click={handleDeleteCourse} /> </DropdownMenu.Item>
</OverflowMenu> <DropdownMenu.Item onclick={handleShareCourse}>
</StructuredListCell> {$t('courses.course_card.context_menu.share')}
</StructuredListRow> </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 = [ const options = [
{ {
title: 'Live Class', title: $t('new_course_modal.live_class_label'),
subtitle: subtitle: $t('new_course_modal.live_class_subtitle'),
'This course type is ideal for bootcamps where lessons are time based and you need attendance and grading',
type: COURSE_TYPE.LIVE_CLASS, type: COURSE_TYPE.LIVE_CLASS,
isDisabled: false isDisabled: false
}, },
{ {
title: 'Self Paced', title: $t('new_course_modal.self_paced_label'),
subtitle: subtitle: $t('new_course_modal.self_paced_subtitle'),
'This course type is ideal for courses where students can take lessons on their own pace without a teacher',
type: COURSE_TYPE.SELF_PACED, type: COURSE_TYPE.SELF_PACED,
isDisabled: false isDisabled: false
} }
@@ -153,10 +151,10 @@
{$t('courses.new_course_modal.type_selector_title')} {$t('courses.new_course_modal.type_selector_title')}
</h2> </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} {#each options as option}
<button <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-primary-400'
: `border-gray-200 dark:border-neutral-600 ${ : `border-gray-200 dark:border-neutral-600 ${
!option.isDisabled && 'hover:scale-95' !option.isDisabled && 'hover:scale-95'
@@ -169,7 +167,7 @@
</div> </div>
<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> <span class="mr-2 text-sm">{option.title}</span>
{#if option.isDisabled} {#if option.isDisabled}
<ComingSoon /> <ComingSoon />
@@ -1,24 +1,20 @@
<script lang="ts"> <script lang="ts">
import * as Table from '@cio/ui/base/table';
import Box from '../Box/index.svelte'; import Box from '../Box/index.svelte';
import Card from './components/Card/index.svelte'; import Card from './components/Card/index.svelte';
import List from './components/List/index.svelte'; import List from './components/List/index.svelte';
import CardLoader from './components/Card/Loader.svelte'; import CardLoader from './components/Card/Loader.svelte';
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.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 { courseMetaDeta } from './store';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
import { globalStore } from '$lib/utils/store/app'; 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 { 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 { snackbar } from '$lib/components/Snackbar/store';
import { deleteCourse } from '$lib/utils/services/courses';
import { deleteCourseModal, deleteCourseModalInitialState, courses as coursesStore } from './store'; import { deleteCourseModal, deleteCourseModalInitialState, courses as coursesStore } from './store';
interface Props { interface Props {
@@ -84,44 +80,34 @@
<CardLoader /> <CardLoader />
</section> </section>
{:else if $courseMetaDeta.view === 'list'} {:else if $courseMetaDeta.view === 'list'}
<StructuredList selection class="w-full"> <div class="w-full overflow-hidden rounded-md border">
<StructuredListHead> <Table.Root class="w-full table-fixed">
<StructuredListRow head> <Table.Header>
<StructuredListCell head> <Table.Row>
{$t('courses.course_card.list_view.title')} <Table.Head class="w-[20%]">{$t('courses.course_card.list_view.title')}</Table.Head>
</StructuredListCell> <Table.Head class="w-[30%]">{$t('courses.course_card.list_view.description')}</Table.Head>
<StructuredListCell head> <Table.Head class="w-[10%]">{$t('courses.course_card.list_view.type')}</Table.Head>
{$t('courses.course_card.list_view.description')} <Table.Head class="w-[10%]">{$t('courses.course_card.list_view.lessons')}</Table.Head>
</StructuredListCell> <Table.Head class="w-[10%]">{$t('courses.course_card.list_view.students')}</Table.Head>
<StructuredListCell head> <Table.Head class="w-[12%]">{$t('courses.course_card.list_view.published')}</Table.Head>
{$t('courses.course_card.list_view.type')} <Table.Head class="w-[8%]"></Table.Head>
</StructuredListCell> </Table.Row>
<StructuredListCell head> </Table.Header>
{$t('courses.course_card.list_view.lessons')} <Table.Body>
</StructuredListCell> {#each courses as courseData}
<StructuredListCell head> <List
{$t('courses.course_card.list_view.students')} id={courseData.id}
</StructuredListCell> title={courseData.title}
<StructuredListCell head> type={$t(`course.navItem.settings.${courseData.type.toLowerCase()}`)}
{$t('courses.course_card.list_view.published')} description={courseData.description}
</StructuredListCell> isPublished={courseData.is_published}
<StructuredListCell head></StructuredListCell> totalLessons={courseData.total_lessons}
</StructuredListRow> totalStudents={courseData.total_students}
</StructuredListHead> />
<StructuredListBody> {/each}
{#each courses as courseData} </Table.Body>
<List </Table.Root>
id={courseData.id} </div>
title={courseData.title}
type={$t(`course.navItem.settings.${courseData.type.toLowerCase()}`)}
description={courseData.description}
isPublished={courseData.is_published}
totalLessons={courseData.total_lessons}
totalStudents={courseData.total_students}
/>
{/each}
</StructuredListBody>
</StructuredList>
{:else} {:else}
<section class={`relative ${$courseMetaDeta.isLoading || courses ? 'cards-container' : ''} `}> <section class={`relative ${$courseMetaDeta.isLoading || courses ? 'cards-container' : ''} `}>
{#each courses as courseData} {#each courses as courseData}
@@ -148,7 +134,7 @@
</div> </div>
{#if !$courseMetaDeta.isLoading && !courses.length} {#if !$courseMetaDeta.isLoading && !courses.length}
<Box className="w-full"> <Box className="w-full">
<CoursesEmptyIcon size={16} /> <CoursesEmptyIcon />
<h3 class="my-5 text-2xl dark:text-white">{emptyTitle}</h3> <h3 class="my-5 text-2xl dark:text-white">{emptyTitle}</h3>
<p class="w-1/3 text-center dark:text-white"> <p class="w-1/3 text-center dark:text-white">
{emptyDescription} {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" tabindex="0"
onclick={handleClick} onclick={handleClick}
> >
<p class="flex items-center font-bold dark:text-white {titleClass}"> <p class="flex items-center dark:text-white {titleClass}">
{title} {title}
{#if supportsLink} {#if supportsLink}
<a class="ml-2" href="{page.url.pathname}#{id}" onclick={(e) => e.stopPropagation()}> <a class="ml-2" href="{page.url.pathname}#{id}" onclick={(e) => e.stopPropagation()}>
@@ -32,7 +32,7 @@
{/if} {/if}
<select <select
bind:value 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} required={isRequired}
onblur={() => {}} onblur={() => {}}
onchange={onChange} onchange={onChange}
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
// import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte'; // 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'; import { t } from '$lib/utils/functions/translations';
interface Props { interface Props {
@@ -20,7 +20,7 @@
ref?: HTMLTextAreaElement | null; ref?: HTMLTextAreaElement | null;
isAIEnabled?: boolean; isAIEnabled?: boolean;
initAIPrompt?: string; initAIPrompt?: string;
aiAlignPopover?: PopoverProps['align']; // $: minHeight = `${1 + parseInt(rows, 10) * 1.2}em`; aiAlignPopover?: PopoverPrimitive.ContentProps['align'];
iconbutton?: import('svelte').Snippet; iconbutton?: import('svelte').Snippet;
} }
@@ -82,7 +82,7 @@
bind:value bind:value
bind:this={ref} bind:this={ref}
required={isRequired} 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'}" {bgColor} {errorMessage ? 'border-red-500' : 'border-gray-300'}"
onchange={onChange} onchange={onChange}
></textarea> ></textarea>
@@ -1,20 +1,19 @@
<script lang="ts"> <script lang="ts">
import type { Component } from 'svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import BadgeHelpIcon from '@lucide/svelte/icons/badge-help'; import type { Component } from 'svelte';
import ListTodoIcon from '@lucide/svelte/icons/list-todo'; import * as Sidebar from '@cio/ui/base/sidebar';
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 UsersIcon from '@lucide/svelte/icons/users'; 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 { 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 { currentOrg } from '$lib/utils/store/org';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'; import { sideBar } from '$lib/components/Org/store';
import ProfileMenu from '$lib/components/Org/ProfileMenu/index.svelte'; import { t } from '$lib/utils/functions/translations';
import NavUser from '../Sidebar/components/nav-user.svelte';
interface SideLinks { interface SideLinks {
name: string; name: string;
@@ -23,6 +22,12 @@
show?: () => boolean; show?: () => boolean;
} }
const user = $derived.by(() => ({
name: $profile.fullname,
email: $profile.email,
avatar: $profile.avatar_url
}));
function isActive(pagePath: string, itemPath: string) { function isActive(pagePath: string, itemPath: string) {
const pageLinkItems = pagePath.split('/'); const pageLinkItems = pagePath.split('/');
const itemLinkItems = itemPath.split('/'); const itemLinkItems = itemPath.split('/');
@@ -75,73 +80,44 @@
}; };
</script> </script>
<div bind:this={$profileMenu.ref} class="static md:relative"> <Sidebar.Provider class="w-fit">
<aside <Sidebar.Root collapsible="icon" class="inset-y-12 h-[calc(100vh-48px)] {$sideBar.hidden ? 'hidden' : ''}">
class={`${ <Sidebar.Content>
$sideBar.hidden <Sidebar.Group>
? 'absolute top-[48px] z-40 -translate-x-[100%] md:relative md:top-0 md:translate-x-0' <Sidebar.GroupLabel>LMS Navigation</Sidebar.GroupLabel>
: 'absolute top-[48px] z-40 translate-x-0 md:relative md:top-0' <Sidebar.Menu>
} h-[calc(100vh-48px)] w-[250px] min-w-[250px] overflow-y-auto bg-gray-100 transition dark:bg-neutral-900`} {#each sideLinks as item (item.link)}
> <Sidebar.MenuItem>
<div class="flex h-full flex-col"> <Sidebar.MenuButton tooltipContent={item.name}>
<div class="border-b border-gray-200 px-4 pt-5 dark:border-neutral-600"> <a
<div class="flex w-full flex-col items-center"> href={item.link}
<Avatar src={$profile.avatar_url} name={$profile.fullname} shape="rounded-full" width="w-20" height="h-20" /> onclick={toggleSidebar}
class="flex w-full items-center gap-4 {isActive(page.url.pathname, item.link)
<div class="mt-5 flex w-full justify-center"> ? 'bg-accent text-accent-foreground rounded-md px-3 py-2'
<p class="max-w-[80%] truncate whitespace-nowrap text-center text-lg font-bold dark:text-white"> : ''}"
{$profile.fullname} >
</p> <item.icon size={16} />
</div> {item.name}
</div> </a>
</Sidebar.MenuButton>
<ul class="my-5"> </Sidebar.MenuItem>
{#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>
</a>
{/each} {/each}
</ul> </Sidebar.Menu>
</div> </Sidebar.Group>
<span class="flex-grow"></span> </Sidebar.Content>
<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>
<ProfileMenu /> {#if user}
</div> <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> <script>
import { Grid, Row, Column } from 'carbon-components-svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import Row from '$lib/components/Org/Settings/Layout/Row.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import Grid from '$lib/components/Org/Settings/Layout/Grid.svelte';
import UploadImage from '$lib/components/UploadImage/index.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 { 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 { snackbar } from '$lib/components/Snackbar/store';
import generateUUID from '$lib/utils/functions/generateUUID'; import generateUUID from '$lib/utils/functions/generateUUID';
import { t } from '$lib/utils/functions/translations'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import LanguagePicker from '$lib/components/Org/Settings/LanguagePicker.svelte';
import { handleLocaleChange } from '$lib/utils/functions/translations'; import { handleLocaleChange } from '$lib/utils/functions/translations';
let avatar = $state(''); let avatar = $state('');
@@ -80,9 +80,9 @@
.container { .container {
max-width: 1000px; max-width: 1000px;
} }
.body { /* .body {
max-height: 60vh; max-height: 60vh;
} } */
.small { .small {
max-width: 388px; max-width: 388px;
@@ -29,7 +29,7 @@
); );
</script> </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"> <ul class="flex w-full items-center gap-2">
<div class="flex items-center text-white"> <div class="flex items-center text-white">
<li class="md:hidden"> <li class="md:hidden">
@@ -63,7 +63,7 @@
className="mr-2" className="mr-2"
/> />
{:else} {: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} {/if}
<span class="line-clamp-1"> <span class="line-clamp-1">
{$currentOrg.name} {$currentOrg.name}
@@ -1,18 +1,15 @@
<script lang="ts"> <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 { 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 { t } from '$lib/utils/functions/translations';
import { currentOrg, orgAudience } from '$lib/utils/store/org'; 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 { interface Props {
isLoading?: boolean; isLoading?: boolean;
} }
@@ -25,34 +22,107 @@
{ key: 'date_joined', value: $t('audience.date_joined') } { key: 'date_joined', value: $t('audience.date_joined') }
]; ];
let pageSize = $state(5); let pageSize = $state(5);
let page = $state(1); let currentPage = $state(1);
let filteredRowIds = $state([]); let searchValue = $state('');
let searchValue = '';
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> </script>
{#if isLoading} <div class="w-full space-y-4">
<DataTableSkeleton {headers} rows={pageSize} /> <!-- Search Toolbar -->
{:else} <div class="flex items-center gap-2">
<DataTable size="tall" {headers} rows={$orgAudience} {pageSize} {page}> <div class="relative flex-1">
<Toolbar> <Search class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<ToolbarContent> <Input type="text" placeholder="Search..." bind:value={searchValue} />
<ToolbarSearch persistent value={searchValue} shouldFilterRows bind:filteredRowIds /> </div>
</ToolbarContent> </div>
</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}`}>
<Avatar src={row.avatar_url} width="w-5" height="h-5" />
{cell.value}
</Link>
{:else}
{cell.value}
{/if}
</svelte:fragment>
</DataTable>
{/if}
<Pagination bind:pageSize bind:page totalItems={filteredRowIds.length} pageSizeInputDisabled /> <!-- Table -->
{#if isLoading}
<div class="space-y-2">
{#each Array(pageSize) as _}
<Skeleton class="h-12 w-full" />
{/each}
</div>
{:else}
<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" />
{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 -->
<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"> <script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte'; import { Skeleton } from '@cio/ui/base/skeleton';
</script> </script>
<div <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" 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"> <div class="mr-2 flex w-10 flex-col justify-evenly rounded-md px-1">
<SkeletonPlaceholder style="width: 100%; height: 40%; border-radius: 4px;" /> <Skeleton class="h-[40%] w-full rounded-sm" />
<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" />
</div> </div>
<div class="mt-1.5 flex w-9/12 flex-col justify-evenly rounded-md"> <div class="mt-1.5 flex w-9/12 flex-col justify-evenly rounded-md">
<!-- first text --> <!-- first text -->
<div class="h-full w-3/6"> <div class="h-full w-3/6">
<SkeletonText style="height: 50%; border-radius: 4px;" /> <Skeleton class="h-[50%] w-full rounded-sm" />
</div> </div>
<!-- second text --> <!-- second text -->
<SkeletonText style="width: 10%; height: 50%; border-radius: 4px;" /> <Skeleton class="h-4 w-1/4 rounded-sm" />
</div> </div>
<div class="ml-auto mt-1.5 w-10 flex-col justify-evenly rounded-md px-1"> <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>
</div> </div>
@@ -1,20 +1,23 @@
<script lang="ts"> <script lang="ts">
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Box from '$lib/components/Box/index.svelte'; import { Input } from '@cio/ui/base/input';
import { courses } from '$lib/components/Courses/store'; import * as Select from '@cio/ui/base/select';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte'; import Search from '@lucide/svelte/icons/search';
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 MessageCirclePlusIcon from '@lucide/svelte/icons/message-circle-plus'; 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 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 { interface Props {
isLMS?: boolean; isLMS?: boolean;
@@ -102,23 +105,31 @@
); );
</script> </script>
<div class="flex justify-between"> <div class="flex w-full gap-2">
<Search <div class="relative flex-1">
placeholder={$t('community.find_question')} <Search class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
bind:value={searchValue} <Input
searchClass="mr-2" type="text"
class=" bg-gray-100 dark:bg-neutral-800" placeholder={$t('community.find_question')}
/> bind:value={searchValue}
<Dropdown class="bg-gray-100 dark:bg-neutral-800"
class="h-full w-[25%]" />
size="xl" </div>
label={$t('community.ask.select_course')} <div class="w-[25%]">
items={[ <Select.Root type="single" bind:value={selectedId}>
{ id: '', text: $t('community.all') }, <Select.Trigger class="w-full bg-gray-100 dark:bg-neutral-800">
...allCourses.map((course) => ({ id: course.id, text: course.title })) <p class="truncate">
]} {selectedId ? allCourses.find((course) => course.id === selectedId)?.title : $t('community.all')}
bind:selectedId </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>
<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" 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> <script>
import { Search } from 'carbon-components-svelte'; import { Input } from '@cio/ui/base/input';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import Search from '@lucide/svelte/icons/search';
let searchValue = $state(''); let searchValue = $state('');
</script> </script>
@@ -9,14 +11,17 @@
<div class="flex w-full flex-col-reverse items-center justify-center md:flex-row"> <div class="flex w-full flex-col-reverse items-center justify-center md:flex-row">
<!-- Course Description --> <!-- Course Description -->
<div class="course-descr w-11/12 py-10"> <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')} {$t('course.navItem.landing_page.explore')}
</h1> </h1>
<p class="text-md mb-6 text-center text-white dark:text-white"> <p class="text-md mb-6 text-center text-white dark:text-white">
{$t('course.navItem.landing_page.find')}🌎. {$t('course.navItem.landing_page.find')}🌎.
</p> </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>
</div> </div>
</section> </section>
@@ -1,21 +1,21 @@
<script> <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'; import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte';
</script> </script>
<section class="h-screen w-screen p-5"> <section class="h-screen w-screen p-5">
<!-- navbar --> <!-- navbar -->
<div class="flex w-full items-center justify-between overflow-hidden rounded-md"> <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"> <div class="flex w-[20%] items-center justify-between">
<SkeletonText style="width:2%; margin-right:1rem; border-radius:10px" /> <Skeleton class="mr-1 h-4 w-[2%] rounded-[10px]" />
<SkeletonText style="width:2%; margin-right:1rem; border-radius:10px" /> <Skeleton class="mr-1 h-4 w-[2%] rounded-[10px]" />
<SkeletonText style="width:2%; border-radius:10px" /> <Skeleton class="h-4 w-[2%] rounded-[10px]" />
</div> </div>
</div> </div>
<!-- hero --> <!-- hero -->
<div class="my-8 flex w-full flex-row flex-wrap items-start justify-center gap-5 overflow-hidden rounded-md"> <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> </div>
<!-- cards --> <!-- cards -->
<div class="my-4 flex flex-wrap items-center justify-center gap-5 overflow-hidden rounded-md"> <div class="my-4 flex flex-wrap items-center justify-center gap-5 overflow-hidden rounded-md">
@@ -1,37 +1,41 @@
<script lang="ts"> <script lang="ts">
import get from 'lodash/get';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { preventDefault } from '$lib/utils/functions/svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Box from '$lib/components/Box/index.svelte'; import { fade } from 'svelte/transition';
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 MailIcon from '@lucide/svelte/icons/mail'; 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 PhoneIcon from '@lucide/svelte/icons/phone';
import RocketIcon from '@lucide/svelte/icons/rocket'; import RocketIcon from '@lucide/svelte/icons/rocket';
import get from 'lodash/get'; import MapPinIcon from '@lucide/svelte/icons/map-pin';
import { fade } from 'svelte/transition'; 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 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 { interface Props {
orgSiteName?: string; orgSiteName?: string;
@@ -219,7 +223,7 @@
<p class=" text-primary-600 text-2xl font-semibold capitalize"> <p class=" text-primary-600 text-2xl font-semibold capitalize">
{org.name} {org.name}
</p> </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.title} <br /><span class="text-primary-600"
>{$landingPageSettings.header.titleHighlight}</span >{$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="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"> <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> <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.title} <br /><span class="text-primary-600"
>{$landingPageSettings.header.titleHighlight}</span >{$landingPageSettings.header.titleHighlight}</span
> >
@@ -309,7 +313,7 @@
<section id="courses" transition:fade class="mx-auto my-10 w-full max-w-6xl"> <section id="courses" transition:fade class="mx-auto my-10 w-full max-w-6xl">
<div class="w-full"> <div class="w-full">
<div class="mx-auto w-11/12 max-w-[500px] py-10"> <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} {$landingPageSettings.courses.title}
<span class="text-primary-600">{$landingPageSettings.courses.titleHighlight}</span> <span class="text-primary-600">{$landingPageSettings.courses.titleHighlight}</span>
</h1> </h1>
@@ -349,7 +353,7 @@
</div> </div>
{:else} {:else}
<Box> <Box>
<CoursesEmptyIcon size={16} /> <CoursesEmptyIcon />
<h3 class="my-5 text-2xl dark:text-white"> <h3 class="my-5 text-2xl dark:text-white">
{$t('course.navItem.landing_page.no_course_published')} {$t('course.navItem.landing_page.no_course_published')}
</h3> </h3>
@@ -376,20 +380,25 @@
{#if $landingPageSettings.faq.show} {#if $landingPageSettings.faq.show}
<section id="faq" transition:fade class="mx-auto my-10 w-full max-w-[700px]"> <section id="faq" transition:fade class="mx-auto my-10 w-full max-w-[700px]">
<div class="py-10"> <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} {$landingPageSettings.faq.title}
</h1> </h1>
</div> </div>
<div class="mx-2"> <div class="mx-2">
<Accordion size="xl"> <Accordion.Root type="single" class="w-full">
{#each $landingPageSettings.faq.questions as faq} {#each $landingPageSettings.faq.questions as faq, index}
<AccordionItem title={faq.title} class="text-3xl"> <Accordion.Item value="item-{index}">
<p class="text-lg"> <Accordion.Trigger class="text-left text-lg font-medium">
{faq.content} {faq.title}
</p> </Accordion.Trigger>
</AccordionItem> <Accordion.Content>
<p class="text-lg">
{faq.content}
</p>
</Accordion.Content>
</Accordion.Item>
{/each} {/each}
</Accordion> </Accordion.Root>
</div> </div>
</section> </section>
{/if} {/if}
@@ -399,7 +408,7 @@
<section id="contact" transition:fade class="bg-primary-50 my-10 w-full"> <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-full max-w-6xl">
<div class="mx-auto w-11/12 max-w-[500px] py-10"> <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} {$landingPageSettings.contact.title}
<span class="text-primary-600">{$landingPageSettings.contact.titleHighlight}</span> <span class="text-primary-600">{$landingPageSettings.contact.titleHighlight}</span>
</h1> </h1>
@@ -491,7 +500,7 @@
<section id="waitlist" transition:fade class="mx-auto my-10 w-[95%] max-w-6xl"> <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="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%]"> <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} {$landingPageSettings.mailinglist.title}
</h1> </h1>
<p class="text-lg text-white"> <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"> <script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte'; import { Skeleton } from '@cio/ui/base/skeleton';
</script> </script>
<div <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" 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"> <div class="mx-5 flex w-full flex-col justify-evenly">
<!-- Title --> <!-- Title -->
<SkeletonText style="width: 50%;" /> <Skeleton class="h-4 w-2/4" />
<!-- Description --> <!-- Description -->
<SkeletonText style="width: 50%; height: 4rem;" /> <Skeleton class="h-16 w-2/4" />
</div> </div>
</div> </div>
@@ -53,7 +53,7 @@
{#snippet footer()} {#snippet footer()}
<div class="flex items-center justify-center"> <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} /> <PrimaryButton label="View Players" variant={VARIANTS.OUTLINED} onClick={goToPlayersStep} />
</div> </div>
{/snippet} {/snippet}
@@ -1,15 +1,17 @@
<script lang="ts"> <script lang="ts">
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { ImageLoader } from 'carbon-components-svelte';
import EllipsisIcon from '@lucide/svelte/icons/ellipsis'; import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
import { currentOrgPath } from '$lib/utils/store/org'; import { currentOrgPath } from '$lib/utils/store/org';
import Dropdown from '$lib/components/Dropdown/index.svelte'; import { t } from '$lib/utils/functions/translations';
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 { themeImages } from '$lib/utils/constants/quiz'; import { themeImages } from '$lib/utils/constants/quiz';
import { calDateDiff } from '$lib/utils/functions/date'; 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(); let { quiz, totalQuestions } = $props();
@@ -55,15 +57,15 @@
<div <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" 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} src={themeImages[quiz.theme]?.card || themeImages.standard.card}
alt="quiz-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 w-full flex-col justify-between p-2 md:p-5">
<div class="flex justify-between"> <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> <a href="{$currentOrgPath}/quiz/{quiz.id}">{quiz.title}</a>
</h4> </h4>
@@ -23,7 +23,7 @@
</script> </script>
{#if isPreview} {#if isPreview}
<h1 class="mb-5 font-bold text-white">{currentQuestion.label}</h1> <h1 class="mb-5 text-white">{currentQuestion.label}</h1>
{:else} {:else}
<TextField <TextField
placeholder={$t('components.quiz.start_typing')} placeholder={$t('components.quiz.start_typing')}
@@ -1,11 +1,14 @@
<script> <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 { 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 { 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 SectionTitle from '../SectionTitle.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
let isRedirecting = $state(false); let isRedirecting = $state(false);
@@ -43,7 +46,7 @@
} }
</script> </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"> <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={4} md={4} lg={4}><SectionTitle>{$t('settings.billing.sub_title')}</SectionTitle></Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
@@ -1,17 +1,22 @@
<script lang="ts"> <script lang="ts">
import { Grid, Row, Column } from 'carbon-components-svelte'; import { Label } from '@cio/ui/base/label';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import { Switch } from '@cio/ui/base/switch';
import SaveIcon from '@lucide/svelte/icons/save'; 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 { 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 { 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(); const supabase = getSupabase();
@@ -44,37 +49,40 @@
</script> </script>
<Grid class="border-c relative mt-5 w-full rounded border-gray-200 dark:border-neutral-600"> <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}> <Column sm={4} md={4} lg={4}>
<SectionTitle>{$t('components.settings.customize_lms.dashboard.title')}</SectionTitle> <SectionTitle>{$t('components.settings.customize_lms.dashboard.title')}</SectionTitle>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<div> <Column sm={8} md={8} lg={8} class="space-y-2">
<p>{$t('components.settings.customize_lms.dashboard.community')}</p> <div class="flex flex-col gap-3">
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.dashboard.community}> <p class="text-sm">{$t('components.settings.customize_lms.dashboard.community')}</p>
<span slot="labelA" style="color: gray"> <div class="mb-3 flex items-center space-x-2">
{$t('components.settings.customize_lms.disabled')} <Switch bind:checked={$currentOrg.customization.dashboard.community} />
</span>
<span slot="labelB" style="color: gray"> <Label class="text-sm">
{$t('components.settings.customize_lms.enabled')} {$currentOrg.customization.dashboard.community
</span> ? $t('components.settings.customize_lms.enabled')
</Toggle> : $t('components.settings.customize_lms.disabled')}
</Label>
</div>
</div> </div>
<div> <div class="flex flex-col gap-3">
<p>{$t('components.settings.customize_lms.dashboard.exercises')}</p> <p class="text-sm">{$t('components.settings.customize_lms.dashboard.exercises')}</p>
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.dashboard.exercise}> <div class="mb-3 flex items-center space-x-2">
<span slot="labelA" style="color: gray"> <Switch bind:checked={$currentOrg.customization.dashboard.exercise} />
{$t('components.settings.customize_lms.disabled')}
</span> <Label class="text-sm">
<span slot="labelB" style="color: gray"> {$currentOrg.customization.dashboard.exercise
{$t('components.settings.customize_lms.enabled')} ? $t('components.settings.customize_lms.enabled')
</span> : $t('components.settings.customize_lms.disabled')}
</Toggle> </Label>
</div>
</div> </div>
<div class="mb-3"> <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 <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label={$t('components.settings.customize_lms.dashboard.banner_image_btn')} label={$t('components.settings.customize_lms.dashboard.banner_image_btn')}
@@ -94,7 +102,6 @@
<TextField <TextField
label={$t('components.settings.customize_lms.dashboard.banner_text_label')} label={$t('components.settings.customize_lms.dashboard.banner_text_label')}
placeholder={$t('components.settings.customize_lms.dashboard.banner_text_placeholder')} placeholder={$t('components.settings.customize_lms.dashboard.banner_text_placeholder')}
className="mb-5"
bind:value={$currentOrg.customization.dashboard.bannerText} bind:value={$currentOrg.customization.dashboard.bannerText}
/> />
</div> </div>
@@ -105,25 +112,30 @@
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}
><SectionTitle>{$t('components.settings.customize_lms.course.title')}</SectionTitle></Column ><SectionTitle>{$t('components.settings.customize_lms.course.title')}</SectionTitle></Column
> >
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="space-y-4">
<div> <div class="flex flex-col gap-3">
<p>{$t('components.settings.customize_lms.course.newsfeed')}</p> <p class="text-sm">{$t('components.settings.customize_lms.course.newsfeed')}</p>
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.course.newsfeed}> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray"> <Switch bind:checked={$currentOrg.customization.course.newsfeed} />
{$t('components.settings.customize_lms.disabled')}
</span> <Label class="text-gray-600">
<span slot="labelB" style="color: gray"> {$currentOrg.customization.course.newsfeed
{$t('components.settings.customize_lms.enabled')} ? $t('components.settings.customize_lms.enabled')
</span> : $t('components.settings.customize_lms.disabled')}
</Toggle> </Label>
</div>
</div> </div>
<div> <div class="flex flex-col gap-3">
<p>{$t('components.settings.customize_lms.course.grading')}</p> <p class="text-sm">{$t('components.settings.customize_lms.course.grading')}</p>
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.course.grading}> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('components.settings.customize_lms.disabled')}</span> <Switch bind:checked={$currentOrg.customization.course.grading} />
<span slot="labelB" style="color: gray">{$t('components.settings.customize_lms.enabled')}</span> <Label class="text-gray-600">
</Toggle> {$currentOrg.customization.course.grading
? $t('components.settings.customize_lms.enabled')
: $t('components.settings.customize_lms.disabled')}
</Label>
</div>
</div> </div>
</Column> </Column>
</Row> </Row>
@@ -132,20 +144,30 @@
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}
><SectionTitle>{$t('components.settings.customize_lms.apps.title')}</SectionTitle></Column ><SectionTitle>{$t('components.settings.customize_lms.apps.title')}</SectionTitle></Column
> >
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="space-y-4">
<div> <div class="flex flex-col gap-3">
<p>{$t('components.settings.customize_lms.apps.poll')}</p> <p class="text-sm">{$t('components.settings.customize_lms.apps.poll')}</p>
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.apps.poll}> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('components.settings.customize_lms.disabled')}</span> <Switch bind:checked={$currentOrg.customization.apps.poll} />
<span slot="labelB" style="color: gray">{$t('components.settings.customize_lms.enabled')}</span>
</Toggle> <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>
<div> <div class="flex flex-col gap-3">
<p>{$t('components.settings.customize_lms.apps.live_comment')}</p> <p class="text-sm">{$t('components.settings.customize_lms.apps.live_comment')}</p>
<Toggle size="sm" class="mb-3" bind:toggled={$currentOrg.customization.apps.comments}> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('components.settings.customize_lms.disabled')}</span> <Switch bind:checked={$currentOrg.customization.apps.comments} />
<span slot="labelB" style="color: gray">{$t('components.settings.customize_lms.enabled')}</span>
</Toggle> <Label class="text-gray-600">
{$currentOrg.customization.apps.comments
? $t('components.settings.customize_lms.enabled')
: $t('components.settings.customize_lms.disabled')}
</Label>
</div>
</div> </div>
</Column> </Column>
</Row> </Row>
@@ -1,31 +1,36 @@
<script lang="ts"> <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 ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw'; import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
import TrashIcon from '@lucide/svelte/icons/trash'; import TrashIcon from '@lucide/svelte/icons/trash';
import Copy from '@lucide/svelte/icons/copy';
import isValidDomain from 'is-valid-domain'; import isValidDomain from 'is-valid-domain';
import { goto } from '$app/navigation';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { parse } from 'tldts'; 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 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 TextArea from '$lib/components/Form/TextArea.svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { IconButton } from '$lib/components/IconButton'; import ComingSoon from '$lib/components/ComingSoon/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 UpgradeBanner from '$lib/components/Upgrade/Banner.svelte'; import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
import UploadImage from '$lib/components/UploadImage/index.svelte'; import UploadImage from '$lib/components/UploadImage/index.svelte';
import { blockedSubdomain } from '$lib/utils/constants/app'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { sanitizeDomain, sendDomainRequest } from '$lib/utils/functions/domain'; import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
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';
let siteName = $derived($currentOrg.siteName); let siteName = $derived($currentOrg.siteName);
let customDomain = $state(''); let customDomain = $state('');
@@ -202,10 +207,11 @@
<Grid class="mt-5 w-full rounded border border-gray-200 dark:border-neutral-600"> <Grid class="mt-5 w-full rounded border border-gray-200 dark:border-neutral-600">
<Row class="border-bottom-c py-7"> <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> <SectionTitle>{$t('components.settings.domains.add')}</SectionTitle>
</Column> </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"> <p class="text-md mb-5 text-gray-500 dark:text-white">
{$t('settings.organization.organization_profile.custom_domain.body')} {$t('settings.organization.organization_profile.custom_domain.body')}
</p> </p>
@@ -225,7 +231,7 @@
<div class="mb-6 flex items-center"> <div class="mb-6 flex items-center">
<PrimaryButton <PrimaryButton
label={$t('components.settings.domains.update')} label={$t('components.settings.domains.update')}
className="py-4" className="py-2"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={handleSaveSiteName} onClick={handleSaveSiteName}
isDisabled={isLoading} isDisabled={isLoading}
@@ -239,12 +245,13 @@
<Row class="py-7"> <Row class="py-7">
<UpgradeBanner className="mb-5">{$t('upgrade.domain')}</UpgradeBanner> <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> <SectionTitle>
{$t('components.settings.domains.custom')} {$t('components.settings.domains.custom')}
</SectionTitle> </SectionTitle>
</Column> </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"> <div class="border-bottom-c pb-10">
<!-- DNS Configuration --> <!-- DNS Configuration -->
{#if $currentOrg.customDomain} {#if $currentOrg.customDomain}
@@ -290,14 +297,18 @@
<p class="text-sm font-light">{$t('components.settings.domains.dns_value')}</p> <p class="text-sm font-light">{$t('components.settings.domains.dns_value')}</p>
<p class=" flex items-center gap-1"> <p class=" flex items-center gap-1">
cname.vercel-dns.com 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> </p>
</div> </div>
</div> </div>
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="py-4 flex items-center gap-2" className="py-2 flex items-center gap-2"
onClick={handleRefreshCustomDomain} onClick={handleRefreshCustomDomain}
isLoading={isRefreshing} isLoading={isRefreshing}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
@@ -309,7 +320,7 @@
</PrimaryButton> </PrimaryButton>
<PrimaryButton <PrimaryButton
className="py-4 flex items-center gap-2" className="py-2 flex items-center gap-2"
onClick={handleRemoveCustomDomain} onClick={handleRemoveCustomDomain}
isLoading={isCustomDomainLoading} isLoading={isCustomDomainLoading}
variant={VARIANTS.CONTAINED_DANGER} variant={VARIANTS.CONTAINED_DANGER}
@@ -338,7 +349,7 @@
<div class="mt-5 flex items-center"> <div class="mt-5 flex items-center">
<PrimaryButton <PrimaryButton
label={$t('components.settings.domains.save')} label={$t('components.settings.domains.save')}
className="py-4" className="py-2"
onClick={$isFreePlan ? () => {} : handleSaveCustomDomain} onClick={$isFreePlan ? () => {} : handleSaveCustomDomain}
isLoading={isCustomDomainLoading} isLoading={isCustomDomainLoading}
isDisabled={isLoading || !isDomainValid} isDisabled={isLoading || !isDomainValid}
@@ -1,15 +1,18 @@
<script lang="ts"> <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 { 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 { 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 TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.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 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); let chatId: number | null = $state(null);
@@ -46,7 +49,7 @@
} }
</script> </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"> <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"> <Column sm={4} md={4} lg={4} class="flex items-center justify-center">
<img src="/telegram-svg.svg" alt="" class="mr-2 w-10" /> <img src="/telegram-svg.svg" alt="" class="mr-2 w-10" />
@@ -75,7 +78,7 @@
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={() => window.open('https://t.me/classroomio_bot', '_blank')} onClick={() => window.open('https://t.me/classroomio_bot', '_blank')}
><span class="mr-2">{$t('settings.integrations.open_bot_button')}</span> ><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> <h4 class="font-normal">{$t('settings.integrations.step_chatId')}</h4>
<TextField <TextField
@@ -1,21 +1,28 @@
<script lang="ts"> <script lang="ts">
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store'; import { Label } from '@cio/ui/base/label';
import TextArea from '$lib/components/Form/TextArea.svelte'; import { Switch } from '@cio/ui/base/switch';
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 SaveIcon from '@lucide/svelte/icons/save'; import SaveIcon from '@lucide/svelte/icons/save';
import TrashIcon from '@lucide/svelte/icons/trash'; 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 { 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 isSaving = $state(false);
let creatingNewQuestion = $state(false); let creatingNewQuestion = $state(false);
@@ -170,14 +177,19 @@
<UnsavedChanges bind:hasUnsavedChanges /> <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"> <Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
<Column sm={4} md={4} lg={4}> <Column sm={4} md={4} lg={4}>
<SectionTitle>{$t('settings.landing_page.heading')}</SectionTitle> <SectionTitle>{$t('settings.landing_page.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.header.show} size="sm" on:change={() => (hasUnsavedChanges = true)}> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span> <Switch bind:checked={$landingPageSettings.header.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
</Toggle> <Label class="text-gray-600">
{$landingPageSettings.header.show
? $t('settings.landing_page.show_section')
: $t('settings.landing_page.hide_section')}
</Label>
</div>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
@@ -220,29 +232,37 @@
bind:value={$landingPageSettings.header.action.link} bind:value={$landingPageSettings.header.action.link}
onChange={() => (hasUnsavedChanges = true)} onChange={() => (hasUnsavedChanges = true)}
/> />
<Toggle <div class="mt-3 flex items-center space-x-2">
bind:toggled={$landingPageSettings.header.action.redirect} <Switch
size="sm" bind:checked={$landingPageSettings.header.action.redirect}
on:change={() => (hasUnsavedChanges = true)} onCheckedChange={() => (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> <Label class="text-gray-600">
</Toggle> {$landingPageSettings.header.action.redirect
? $t('settings.landing_page.actions.redirect')
: $t('settings.landing_page.actions.no_redirect')}
</Label>
</div>
</div> </div>
<RadioButtonGroup <div class="mb-5 mt-10">
legendText={$t('settings.landing_page.actions.banner_type.heading')} <Label class="mb-3 block font-medium">{$t('settings.landing_page.actions.banner_type.heading')}</Label>
bind:selected={$landingPageSettings.header.banner.type} <RadioGroup.Root
on:click={() => { bind:value={$landingPageSettings.header.banner.type}
if (hasUnsavedChanges) return; onValueChange={() => {
hasUnsavedChanges = true; if (hasUnsavedChanges) return;
}} hasUnsavedChanges = true;
class="mb-5 mt-10" }}
> >
{#each banner as item} {#each banner as item}
<RadioButton value={item.value} labelText={item.label} /> <div class="flex items-center space-x-2">
{/each} <RadioGroup.Item value={item.value} id={item.value} />
</RadioButtonGroup> <Label for={item.value} class="cursor-pointer">{item.label}</Label>
</div>
{/each}
</RadioGroup.Root>
</div>
{#if $landingPageSettings.header.banner.type === 'video'} {#if $landingPageSettings.header.banner.type === 'video'}
<div class="mb-5 mt-3 gap-2"> <div class="mb-5 mt-3 gap-2">
<TextField <TextField
@@ -264,14 +284,17 @@
<img alt="bannerImage" src={$landingPageSettings.header.banner.image} class="mt-2 w-full rounded-md" /> <img alt="bannerImage" src={$landingPageSettings.header.banner.image} class="mt-2 w-full rounded-md" />
{/if} {/if}
<Toggle <div class="flex items-center space-x-2">
bind:toggled={$landingPageSettings.header.banner.show} <Switch
size="sm" bind:checked={$landingPageSettings.header.banner.show}
on:change={() => (hasUnsavedChanges = true)} onCheckedChange={() => (hasUnsavedChanges = true)}
> />
<span slot="labelA" style="color: gray">{$t('settings.landing_page.actions.hide_banner')}</span> <Label class="text-gray-600">
<span slot="labelB" style="color: gray">{$t('settings.landing_page.actions.show_banner')}</span> {$landingPageSettings.header.banner.show
</Toggle> ? $t('settings.landing_page.actions.show_banner')
: $t('settings.landing_page.actions.hide_banner')}
</Label>
</div>
{#if $handleOpenWidget.open && widgetKey === 'banner'} {#if $handleOpenWidget.open && widgetKey === 'banner'}
<UploadWidget <UploadWidget
bind:imageURL={$landingPageSettings.header.banner.image} bind:imageURL={$landingPageSettings.header.banner.image}
@@ -282,7 +305,7 @@
<!-- background --> <!-- background -->
<div class="mt-4"> <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 <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label={$t('settings.landing_page.about.select_image')} label={$t('settings.landing_page.about.select_image')}
@@ -298,24 +321,27 @@
/> />
{/if} {/if}
<Toggle <div class="mt-2 flex items-center space-x-2">
toggled={$landingPageSettings.header.background?.show} <Switch
on:change={() => { checked={$landingPageSettings.header.background?.show}
hasUnsavedChanges = true; onCheckedChange={() => {
if (!$landingPageSettings.header.background) { hasUnsavedChanges = true;
$landingPageSettings.header.background = { if (!$landingPageSettings.header.background) {
image: '', $landingPageSettings.header.background = {
show: false image: '',
}; show: true
} else { };
$landingPageSettings.header.background.show = !$landingPageSettings.header.background.show; } 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> <Label class="text-gray-600">
<span slot="labelB" style="color: gray">{$t('settings.landing_page.background.show_background')}</span> {$landingPageSettings.header.background?.show
</Toggle> ? $t('settings.landing_page.background.show_background')
: $t('settings.landing_page.background.hide_background')}
</Label>
</div>
{#if $handleOpenWidget.open && widgetKey === 'background'} {#if $handleOpenWidget.open && widgetKey === 'background'}
<UploadWidget <UploadWidget
@@ -339,14 +365,17 @@
</Row> </Row>
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row"> <Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}>
><SectionTitle>{$t('settings.landing_page.about.heading')}</SectionTitle> <SectionTitle>{$t('settings.landing_page.about.heading')}</SectionTitle>
<div class="flex items-center space-x-2">
<Toggle bind:toggled={$landingPageSettings.aboutUs.show} on:change={() => (hasUnsavedChanges = true)} size="sm"> <Switch bind:checked={$landingPageSettings.aboutUs.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
<span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span> <Label class="text-gray-600">
<span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span> {$landingPageSettings.aboutUs.show
</Toggle></Column ? $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"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
label={$t('settings.landing_page.about.title')} label={$t('settings.landing_page.about.title')}
@@ -385,12 +414,16 @@
</Row> </Row>
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row"> <Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}>
><SectionTitle>{$t('settings.landing_page.courses.heading')}</SectionTitle> <SectionTitle>{$t('settings.landing_page.courses.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.courses.show} size="sm" on:change={() => (hasUnsavedChanges = true)}> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.courses.hide_section')}</span> <Switch bind:checked={$landingPageSettings.courses.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
<span slot="labelB" style="color: gray">{$t('settings.landing_page.courses.show_section')}</span> <Label class="text-gray-600">
</Toggle> {$landingPageSettings.courses.show
? $t('settings.landing_page.courses.show_section')
: $t('settings.landing_page.courses.hide_section')}
</Label>
</div>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
@@ -419,11 +452,15 @@
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row"> <Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
<Column sm={4} md={4} lg={4}> <Column sm={4} md={4} lg={4}>
<SectionTitle>{$t('settings.landing_page.faq.heading')}</SectionTitle> <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)}> <Label class="text-gray-600">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.faq.hide_section')}</span> {$landingPageSettings.faq.show
<span slot="labelB" style="color: gray">{$t('settings.landing_page.faq.show_section')}</span> ? $t('settings.landing_page.faq.show_section')
</Toggle> : $t('settings.landing_page.faq.hide_section')}
</Label>
</div>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<TextField <TextField
@@ -496,12 +533,16 @@
</Row> </Row>
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row"> <Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}>
><SectionTitle>{$t('settings.landing_page.contact_us.heading')}</SectionTitle> <SectionTitle>{$t('settings.landing_page.contact_us.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.contact.show} size="sm" on:change={() => (hasUnsavedChanges = true)}> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.contact_us.hide_section')}</span> <Switch bind:checked={$landingPageSettings.contact.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
<span slot="labelB" style="color: gray">{$t('settings.landing_page.contact_us.show_section')}</span> <Label class="text-gray-600">
</Toggle> {$landingPageSettings.contact.show
? $t('settings.landing_page.contact_us.show_section')
: $t('settings.landing_page.contact_us.hide_section')}
</Label>
</div>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<TextField <TextField
@@ -553,16 +594,19 @@
</Row> </Row>
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row"> <Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}>
><SectionTitle>{$t('settings.landing_page.mailing_list.heading')}</SectionTitle> <SectionTitle>{$t('settings.landing_page.mailing_list.heading')}</SectionTitle>
<Toggle <div class="flex items-center space-x-2">
bind:toggled={$landingPageSettings.mailinglist.show} <Switch
size="sm" bind:checked={$landingPageSettings.mailinglist.show}
on:change={() => (hasUnsavedChanges = true)} onCheckedChange={() => (hasUnsavedChanges = true)}
> />
<span slot="labelA" style="color: gray">{$t('settings.landing_page.mailing_list.hide_section')}</span> <Label class="text-gray-600">
<span slot="labelB" style="color: gray">{$t('settings.landing_page.mailing_list.show_section')}</span> {$landingPageSettings.mailinglist.show
</Toggle> ? $t('settings.landing_page.mailing_list.show_section')
: $t('settings.landing_page.mailing_list.hide_section')}
</Label>
</div>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
@@ -593,12 +637,17 @@
</Row> </Row>
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row"> <Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}>
><SectionTitle>{$t('settings.landing_page.custom_links.heading')}</SectionTitle> <SectionTitle>{$t('settings.landing_page.custom_links.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.customLinks.show} size="sm"> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.custom_links.hide_links')}</span> <Switch bind:checked={$landingPageSettings.customLinks.show} />
<span slot="labelB" style="color: gray">{$t('settings.landing_page.custom_links.show_links')}</span>
</Toggle> <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>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<p class="mb-4 text-sm text-gray-600"> <p class="mb-4 text-sm text-gray-600">
@@ -682,12 +731,17 @@
</Row> </Row>
<Row class="flex flex-col py-7 lg:flex-row"> <Row class="flex flex-col py-7 lg:flex-row">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}>
><SectionTitle>{$t('settings.landing_page.footer.heading')}</SectionTitle> <SectionTitle>{$t('settings.landing_page.footer.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.footer.show} size="sm" on:change={() => (hasUnsavedChanges = true)}> <div class="flex items-center space-x-2">
<span slot="labelA" style="color: gray">{$t('settings.landing_page.footer.hide_section')}</span> <Switch bind:checked={$landingPageSettings.footer.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
<span slot="labelB" style="color: gray">{$t('settings.landing_page.footer.show_section')}</span>
</Toggle> <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>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
@@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Dropdown } from 'carbon-components-svelte'; import { Label } from '@cio/ui/base/label';
import { t, initialized } from '$lib/utils/functions/translations'; import * as Select from '@cio/ui/base/select';
import { LANGUAGES } from '$lib/utils/constants/translation';
import { LOCALE } from '$lib/utils/types'; import { LOCALE } from '$lib/utils/types';
import { LANGUAGES } from '$lib/utils/constants/translation';
import { t, initialized } from '$lib/utils/functions/translations';
interface Props { interface Props {
className?: string; className?: string;
@@ -13,23 +15,25 @@
let { className = '', value = $bindable(LOCALE.EN), hasLangChanged = $bindable(false), change }: Props = $props(); let { className = '', value = $bindable(LOCALE.EN), hasLangChanged = $bindable(false), change }: Props = $props();
const dropdownItems = [{ id: '', text: 'Pick a Language' }, ...LANGUAGES]; function handleSelect(selectedValue: string) {
value = selectedValue as LOCALE;
function handleSelect(event) {
value = event.detail.selectedId;
hasLangChanged = true; hasLangChanged = true;
change?.(); change?.();
} }
</script> </script>
{#if $initialized} {#if $initialized}
<Dropdown <div class={className}>
titleText={$t('content.toggle_label')} <Label class="mb-2 block">{$t('content.toggle_label')}</Label>
items={dropdownItems} <Select.Root type="single" bind:value onValueChange={handleSelect}>
selectedId={value} <Select.Trigger class="w-full">
on:select={handleSelect} <p>{value ? LANGUAGES.find((lang) => lang.id === value)?.text : 'Pick a Language'}</p>
class="h-full {className}" </Select.Trigger>
/> <Select.Content>
{#each LANGUAGES as language}
<Select.Item value={language.id}>{language.text}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if} {/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"> <script lang="ts">
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { Column, Grid, Row } from 'carbon-components-svelte';
import ZapIcon from '@lucide/svelte/icons/zap'; import ZapIcon from '@lucide/svelte/icons/zap';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import ColorPicker from 'svelte-awesome-color-picker'; 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 { 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 { updateOrgNameValidation } from '$lib/utils/functions/validator';
import { currentOrg, currentOrgPath, isFreePlan } from '$lib/utils/store/org'; 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 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 PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import UnsavedChanges from '$lib/components/UnsavedChanges/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 avatar = $state<string | undefined>();
let hasUnsavedChanges = $state(false); let hasUnsavedChanges = $state(false);
@@ -155,7 +157,7 @@
<UnsavedChanges bind:hasUnsavedChanges /> <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"> <Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
<Column sm={4} md={4} lg={4}> <Column sm={4} md={4} lg={4}>
<SectionTitle> <SectionTitle>
@@ -1,20 +1,23 @@
<script lang="ts"> <script lang="ts">
import TextField from '$lib/components/Form/TextField.svelte'; import { LOCALE } from '$lib/utils/types';
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 { profile } from '$lib/utils/store/user'; 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 SectionTitle from '../SectionTitle.svelte';
import LanguagePicker from './LanguagePicker.svelte'; import LanguagePicker from './LanguagePicker.svelte';
import type { ProfileStore } from '$lib/utils/store/user'; import generateUUID from '$lib/utils/functions/generateUUID';
import { LOCALE } from '$lib/utils/types'; 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 avatar = $state<string | undefined>();
let loading = $state(false); let loading = $state(false);
@@ -92,12 +95,12 @@
<UnsavedChanges bind:hasUnsavedChanges /> <UnsavedChanges bind:hasUnsavedChanges />
<Grid class="border-c mt-5 w-full rounded border-gray-200 dark:border-neutral-600"> <Grid class="mt-0 w-full md:mt-5">
<Row class="border-bottom-c flex flex-col items-center py-7 lg:flex-row lg:items-start "> <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"> <Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
<SectionTitle>{$t('settings.profile.profile_picture.heading')}</SectionTitle> <SectionTitle>{$t('settings.profile.profile_picture.heading')}</SectionTitle>
</Column> </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 <UploadImage
bind:avatar bind:avatar
src={$profile.avatar_url} src={$profile.avatar_url}
@@ -141,14 +144,14 @@
</Column> </Column>
</Row> </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 <PrimaryButton
label={$t('settings.profile.update_profile')} label={$t('settings.profile.update_profile')}
variant={VARIANTS.CONTAINED_DARK} variant={VARIANTS.CONTAINED_DARK}
className="mr-5" className="mr-5 w-fit"
isLoading={loading} isLoading={loading}
isDisabled={loading} isDisabled={loading}
onClick={handleUpdate} onClick={handleUpdate}
/> />
</Row> </div>
</Grid> </Grid>
@@ -1,28 +1,34 @@
<script lang="ts"> <script lang="ts">
import { untrack } from 'svelte'; 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 { Moon } from 'svelte-loading-spinners';
import TextField from '$lib/components/Form/TextField.svelte';
import TextChip from '$lib/components/Chip/Text.svelte'; import { profile } from '$lib/utils/store/user';
import ComingSoon from '$lib/components/ComingSoon/index.svelte'; import { isFreePlan } from '$lib/utils/store/org';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; 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 { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { validateEmailInString } from '$lib/utils/functions/validator'; 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 { 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 Row from './Layout/Row.svelte';
import { t } from '$lib/utils/functions/translations'; 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 emailsStr = $state('');
let errorMessage = $state(''); let errorMessage = $state('');
let role = $state(ROLE.TUTOR); let role = $state(ROLE.TUTOR.toString());
let isFetching = $state(false); let isFetching = $state(false);
let isLoading = $state(false); let isLoading = $state(false);
let isRemoving: number | null = $state(null); 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"> <Grid class="relative mt-5 w-full rounded border border-gray-200 dark:border-neutral-600">
<Row class="border-bottom-c py-7"> <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 ><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"> <p class="text-md mb-5 text-gray-500 dark:text-white">
{$t('course.navItem.people.teams.add_team')} {$t('course.navItem.people.teams.add_team')}
</p> </p>
@@ -155,15 +161,18 @@
{errorMessage} {errorMessage}
/> />
<Select <div class="mb-5">
labelText={$t('course.navItem.people.teams.role')} <Label class="mb-2 block">{$t('course.navItem.people.teams.role')}</Label>
bind:selected={role} <Select.Root type="single" bind:value={role} disabled={$isFreePlan}>
class="mb-5 w-40" <Select.Trigger class="w-40">
disabled={$isFreePlan} <p>{role ? $t(ROLE_LABEL[role]) : $t('course.navItem.people.teams.select_role')}</p>
> </Select.Trigger>
<SelectItem value={ROLE.ADMIN} text={$t(ROLE_LABEL[ROLE.ADMIN])} /> <Select.Content>
<SelectItem value={ROLE.TUTOR} text={$t(ROLE_LABEL[ROLE.TUTOR])} /> <Select.Item value={ROLE.ADMIN.toString()}>{$t(ROLE_LABEL[ROLE.ADMIN])}</Select.Item>
</Select> <Select.Item value={ROLE.TUTOR.toString()}>{$t(ROLE_LABEL[ROLE.TUTOR])}</Select.Item>
</Select.Content>
</Select.Root>
</div>
<PrimaryButton <PrimaryButton
label={$t('course.navItem.people.teams.send_invite')} label={$t('course.navItem.people.teams.send_invite')}
@@ -176,10 +185,10 @@
</Row> </Row>
<Row class="border-bottom-c py-7"> <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 ><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} {#if isFetching}
<Moon /> <Moon />
{:else} {:else}
@@ -1,14 +1,16 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; 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 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 Billing from './Billing.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { isOrgAdmin } from '$lib/utils/store/org'; import OrgSettings from './OrgSettings.svelte';
import Integrations from './Integrations.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'; import { t } from '$lib/utils/functions/translations';
interface TabItem { interface TabItem {
@@ -59,45 +61,41 @@
disabled: false disabled: false
} }
]); ]);
let selected = $derived(getSelectedByTab(tabs, tabKey));
function getSelectedByTab(_tabs: TabItem[], tabKey = '') { function onTabChange(value: string) {
const tab = _tabs.find((t) => t.tabKey === tabKey); const tab = tabs.find((t) => t.tabKey === value);
return tab ? tab.key : 0;
}
function onTabChange(e: CustomEvent<number>) {
const key = e.detail;
const tab = tabs.find((t) => t.key === key);
if (tab) { if (tab) {
goto(tab.href); goto(tab.href);
} }
} }
</script> </script>
<Tabs autoWidth bind:selected on:change={onTabChange}> <Tabs.Root value={tabKey} onValueChange={onTabChange} class="w-full p-0">
{#each tabs as tab} <Tabs.List class="w-full">
<Tab label={tab.label} href={tab.href} disabled={tab.disabled} /> {#each tabs as tab}
{/each} <Tabs.Trigger value={tab.tabKey} disabled={tab.disabled}>
<!-- <Tab label="Account" /> --> {tab.label}
<svelte:fragment slot="content"> </Tabs.Trigger>
<TabContent class="w-full p-0"> {/each}
<Profile /> </Tabs.List>
</TabContent>
<!-- <TabContent> <Tabs.Content value="" class="w-full p-0">
<Account /> <Profile />
</TabContent> --> </Tabs.Content>
<TabContent class="w-full p-0">
<OrgSettings /> <Tabs.Content value="org" class="w-full p-0">
</TabContent> <OrgSettings />
<TabContent class="w-full p-0"> </Tabs.Content>
<LandingpageSettings />
</TabContent> <Tabs.Content value="landingpage" class="w-full p-0">
<TabContent class="w-full p-0"> <LandingpageSettings />
<Billing /> </Tabs.Content>
</TabContent>
<TabContent class="w-full p-0"> <Tabs.Content value="billing" class="w-full p-0">
<Integrations /> <Billing />
</TabContent> </Tabs.Content>
</svelte:fragment>
</Tabs> <Tabs.Content value="integrations" class="w-full p-0">
<Integrations />
</Tabs.Content>
</Tabs.Root>

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