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

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