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 <[email protected]>
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,3 +1,6 @@
import { lesson } from '$lib/components/Course/components/Lesson/store/lessons';
import { snackbar } from '$lib/components/Snackbar/store';
export function formatYoutubeVideo(url: string, errors: Record<string, string>) {
console.log(`url`, url);
const prefix = 'https://www.youtube.com/embed';
@@ -49,3 +52,27 @@ export function getEmbedId(url: string) {
return query.get('v');
}
export function getVideoUrls(urls = '') {
return (urls || '').split(',').filter((url) => !!url.trim());
}
export async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
snackbar.success('Copied to clipboard');
} catch (err) {
console.error('Failed to copy:', err);
snackbar.error('Failed to copy');
}
}
export function removeVideo(index = 0) {
lesson.update((currentLesson) => ({
...currentLesson,
materials: {
...currentLesson.materials,
videos: currentLesson.materials.videos.filter((_v, i) => i !== index)
}
}));
}
@@ -1,161 +0,0 @@
import { env } from '$env/dynamic/public';
let win, output, textarea;
// Follow this guide to get source code: https://editor.codecogs.com/docs/3-Tiny_MCE_v6.php
export const addMathPlugin = (tinymce: any) => {
tinymce.PluginManager.add('eqneditor', function (editor, url) {
// Load necessary javascript for editor from CodeCogs
const sl = new tinymce.dom.ScriptLoader();
sl.add('https://assets.cdn.clsrio.com/eqneditor.api.min.js');
const loaded = sl.loadQueue();
// Load custom CSS
tinymce.DOM.loadCSS('https://assets.cdn.clsrio.com/eqneditor_1.css');
editor.ui.registry.addIcon(
'logo',
`<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="18px" height="20px" viewBox="0 0 18 20" enable-background="new 0 0 18 20" xml:space="preserve">
<rect x="4.4" y="5.1" fill="none" width="38.6" height="43.4"/>
<g enable-background="new ">
<path d="M4,9.5H2L2.3,7h2.1l0.3-1.8C4.7,4.6,4.9,4.1,5,3.6c0.2-0.5,0.4-0.9,0.7-1.3c0.3-0.4,0.8-0.7,1.3-0.9C7.7,1.1,8.4,1,9.2,1
c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0.1,0.9,0.1l-0.5,2.7c-0.2-0.1-0.3-0.1-0.5-0.2c-0.1,0-0.3-0.1-0.5-0.1C9,3.7,8.6,3.8,8.4,4
C8.1,4.2,7.9,4.7,7.8,5.4L7.6,7h2.3L9.5,9.5H7.2l-1.1,7.7H3L4,9.5z"/>
</g>
<g enable-background="new ">
<path d="M11.1,15.3l-2.3-3.2h2.5l1.2,1.8l1.2-1.8H16l-2.2,3.2l2.6,3.6h-2.5l-1.5-2.3L10.8,19H8.4L11.1,15.3z"/>
</g>
</svg>`
);
editor.ui.registry.addIcon(
'menu',
`<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='11.473512pt' height='12.266481pt' viewBox='-.239051 -.242965 11.473512 12.266481'>
<defs>
<path id='g0-120' d='M3.993026-3.180075C3.642341-3.092403 3.626401-2.781569 3.626401-2.749689C3.626401-2.574346 3.761893-2.454795 3.937235-2.454795S4.383562-2.590286 4.383562-2.933001C4.383562-3.387298 3.881445-3.514819 3.58655-3.514819C3.211955-3.514819 2.909091-3.251806 2.725778-2.940971C2.550436-3.363387 2.13599-3.514819 1.809215-3.514819C.940473-3.514819 .454296-2.518555 .454296-2.295392C.454296-2.223661 .510087-2.191781 .573848-2.191781C.669489-2.191781 .68543-2.231631 .70934-2.327273C.892653-2.909091 1.370859-3.291656 1.785305-3.291656C2.096139-3.291656 2.247572-3.068493 2.247572-2.781569C2.247572-2.622167 2.15193-2.255542 2.088169-2.000498C2.032379-1.769365 1.857036-1.060025 1.817186-.908593C1.705604-.478207 1.41868-.143462 1.060025-.143462C1.028144-.143462 .820922-.143462 .653549-.255044C1.020174-.342715 1.020174-.67746 1.020174-.68543C1.020174-.868742 .876712-.980324 .70137-.980324C.486177-.980324 .255044-.797011 .255044-.494147C.255044-.127522 .645579 .079701 1.052055 .079701C1.474471 .079701 1.769365-.239103 1.912827-.494147C2.088169-.103611 2.454795 .079701 2.83736 .079701C3.706102 .079701 4.184309-.916563 4.184309-1.139726C4.184309-1.219427 4.120548-1.243337 4.064757-1.243337C3.969116-1.243337 3.953176-1.187547 3.929265-1.107846C3.769863-.573848 3.315567-.143462 2.8533-.143462C2.590286-.143462 2.399004-.318804 2.399004-.653549C2.399004-.812951 2.446824-.996264 2.558406-1.44259C2.614197-1.681694 2.789539-2.383064 2.82939-2.534496C2.940971-2.948941 3.219925-3.291656 3.57858-3.291656C3.618431-3.291656 3.825654-3.291656 3.993026-3.180075Z'/>
<path id='g1-102' d='M5.332005-4.805978C5.571108-4.805978 5.66675-4.805978 5.66675-5.033126C5.66675-5.152677 5.571108-5.152677 5.355915-5.152677H4.387547C4.614695-6.38406 4.782067-7.232877 4.877709-7.615442C4.94944-7.902366 5.200498-8.177335 5.511333-8.177335C5.762391-8.177335 6.01345-8.069738 6.133001-7.962142C5.66675-7.914321 5.523288-7.567621 5.523288-7.364384C5.523288-7.12528 5.702615-6.981818 5.929763-6.981818C6.168867-6.981818 6.527522-7.185056 6.527522-7.639352C6.527522-8.141469 6.025405-8.416438 5.499377-8.416438C4.985305-8.416438 4.483188-8.033873 4.244085-7.567621C4.028892-7.149191 3.90934-6.718804 3.634371-5.152677H2.833375C2.606227-5.152677 2.486675-5.152677 2.486675-4.937484C2.486675-4.805978 2.558406-4.805978 2.797509-4.805978H3.56264C3.347447-3.694147 2.857285-.992279 2.582316 .286924C2.379078 1.327024 2.199751 2.199751 1.601993 2.199751C1.566127 2.199751 1.219427 2.199751 1.004234 1.972603C1.613948 1.924782 1.613948 1.398755 1.613948 1.3868C1.613948 1.147696 1.43462 1.004234 1.207472 1.004234C.968369 1.004234 .609714 1.207472 .609714 1.661768C.609714 2.175841 1.135741 2.438854 1.601993 2.438854C2.82142 2.438854 3.323537 .251059 3.455044-.3467C3.670237-1.267248 4.25604-4.447323 4.315816-4.805978H5.332005Z'/>
</defs>
<g id='page1' transform='matrix(1.13 0 0 1.13 -63.986043 -65.03376)'>
<use x='56.413267' y='65.753425' xlink:href='#g1-102'/>
<use x='62.183256' y='67.546688' xlink:href='#g0-120'/>
</g>
</svg>`
);
function showDialog(inp = '') {
const http = 'https:' == document.location.protocol ? 'https://' : 'http://';
win = editor.windowManager.open({
title: 'Equation Editor',
size: 'medium',
body: {
type: 'panel',
items: [
{
type: 'htmlpanel',
html:
'<div id="equation-editor">' +
'<div id="history"></div>' +
'<div id="toolbar"></div>' +
'<div id="latexInput" autocorrect="off"></div>' +
'<div id="equation-output">' +
'<img id="output"></div></div>'
}
]
},
buttons: [
{
type: 'cancel',
name: 'closeButton',
text: 'Cancel'
},
{
type: 'submit',
name: 'submitButton',
text: 'Ok',
buttonType: 'primary'
}
],
onSubmit: async function (w) {
let content = output.exportAs('html');
// The generated content always points to https://latex.codecogs.com/svg.image, this means we have to always rely on their server to serve formulas. We have a dedicated endpoint for generating formulas on our api service: GET `/katex`. We will need to manually update the URL in the generate HTML to point to our server:
// https://latex.codecogs.com/svg.image
// 1. Get only the URL in the img tag generated by codecogs library
const srcRegex = /<img[^>]+src\s*=\s*['"]([^'"]+)['"][^>]*>/i;
const srcMatch = content.match(srcRegex);
if (srcMatch) {
// 2. Point the formula to our server
const customAPI = env.PUBLIC_SERVER_URL + '/course/katex';
const url = srcMatch[1]?.replaceAll('https://latex.codecogs.com/svg.image', customAPI);
// 3. Fetch the HTML from the server
const response = await fetch(url);
const html = await response.text();
// 4. Set the content to the html
content = html;
} else {
console.error('No src attribute found in the img tag.');
}
tinymce.activeEditor.execCommand('mceInsertContent', false, content);
textarea.pushToHistory();
w.close();
},
onCancel: function (w) {
w.close();
},
onAction: function (w, dets) {
if (dets.name == 'logo') {
window.open(http + 'editor.codecogs.com', '_blank')?.focus();
}
}
});
// Ensure EqnEditor js is loaded before continuing
loaded.then(() => {
output = new EqEditor.Output('output');
textarea = EqEditor.TextArea.link('latexInput')
.addOutput(output)
.addHistoryMenu(new EqEditor.History('history'));
EqEditor.Toolbar.link('toolbar').addTextArea(textarea);
const nodes = document.getElementById('history')?.childNodes || [];
for (let i = 0; i < nodes.length; i++) {
nodes[i].style.padding = 'revert';
}
textarea.clear();
textarea.insert(inp.replace(/&space;/g, ' '), inp.length);
});
}
editor.ui.registry.addButton('eqneditor', {
title: 'Equation',
icon: 'menu',
tooltip: 'Insert Equation',
onAction: () => {
showDialog();
}
// stateSelector: 'img[src*="latex"]'
});
editor.on('dblclick', function (e) {
if (e.target.nodeName.toLowerCase() == 'img') {
const sName = e.target.src.match(/(gif|svg)\.image\?(.*)/);
if (sName != null) showDialog(sName[2]);
}
});
return {
getMetadata: () => ({
name: 'EqnEditor',
url: 'https://editor.codecogs.com/docs/3-Tiny_MCE_v4x.php'
})
};
});
};
@@ -1581,5 +1581,11 @@
"resend_in": "Send verifikation i",
"seconds": "sekunder",
"snackbar_error": "Fejl under afsendelse af kode"
},
"new_course_modal": {
"live_class_label": "Live klasse",
"live_class_subtitle": "Denne kursustype er ideel til bootcamps, hvor undervisningen er tidsbaseret, og du har brug for deltagelse og karaktergivning",
"self_paced_label": "Selv tempo",
"self_paced_subtitle": "Denne kursustype er ideel til kurser, hvor elever kan tage lektioner i deres eget tempo uden en lærer"
}
}
}
@@ -1612,5 +1612,11 @@
"unsaved_changes": {
"message": "Sie haben nicht gerettete Änderungen. Bist du sicher, dass du gehen willst?"
}
},
"new_course_modal": {
"live_class_label": "Live-Klasse",
"live_class_subtitle": "Dieser Kurstyp ist ideal für Bootcamps, bei denen der Unterricht zeitbasiert ist und Anwesenheit und Benotung erforderlich sind",
"self_paced_label": "Eigenständig",
"self_paced_subtitle": "Dieser Kurstyp ist ideal für Kurse, bei denen die Studierenden den Unterricht in ihrem eigenen Tempo ohne Lehrer absolvieren können"
}
}
}
@@ -17,6 +17,13 @@
"yes": "Yes"
},
"new_course_modal": {
"live_class_label": "Live Class",
"live_class_subtitle": "This course type is ideal for bootcamps where lessons are time based and you need attendance and grading",
"self_paced_label": "Self Paced",
"self_paced_subtitle": "This course type is ideal for courses where students can take lessons on their own pace without a teacher"
},
"onboarding": {
"sell": "Sell courses online",
"teach": "Teach my current students online",
@@ -1612,5 +1612,11 @@
"unsaved_changes": {
"message": "Tienes cambios no salvos. ¿Estás seguro de que quieres irte?"
}
},
"new_course_modal": {
"live_class_label": "Clase en vivo",
"live_class_subtitle": "Este tipo de curso es ideal para bootcamps donde las lecciones se basan en el tiempo y necesitas asistencia y calificaciones.",
"self_paced_label": "a tu propio ritmo",
"self_paced_subtitle": "Este tipo de curso es ideal para cursos donde los estudiantes pueden tomar lecciones a su propio ritmo sin un profesor."
}
}
}
@@ -1612,5 +1612,11 @@
"unsaved_changes": {
"message": "Vous avez des changements non sauvés. Êtes-vous sûr de vouloir partir?"
}
},
"new_course_modal": {
"live_class_label": "Cours en direct",
"live_class_subtitle": "Ce type de cours est idéal pour les bootcamps où les cours sont basés sur le temps et où vous avez besoin d'assiduité et de notation.",
"self_paced_label": "À votre rythme",
"self_paced_subtitle": "Ce type de cours est idéal pour les cours où les étudiants peuvent suivre des cours à leur rythme sans professeur."
}
}
}
@@ -1612,5 +1612,11 @@
"unsaved_changes": {
"message": "आपके पास अनसुना परिवर्तन हैं। क्या आपकी वाकई छोड़ने की इच्छा है?"
}
},
"new_course_modal": {
"live_class_label": "लाइव क्लास",
"live_class_subtitle": "यह पाठ्यक्रम प्रकार बूटकैंप के लिए आदर्श है जहां पाठ समय आधारित होते हैं और आपको उपस्थिति और ग्रेडिंग की आवश्यकता होती है",
"self_paced_label": "स्वयं गति",
"self_paced_subtitle": "यह पाठ्यक्रम प्रकार उन पाठ्यक्रमों के लिए आदर्श है जहां छात्र शिक्षक के बिना अपनी गति से पाठ ले सकते हैं"
}
}
}
@@ -1582,5 +1582,11 @@
"unsaved_changes": {
"message": "Masz niezbawione zmiany. Czy na pewno chcesz odejść?"
}
},
"new_course_modal": {
"live_class_label": "Zajęcia na żywo",
"live_class_subtitle": "Ten typ kursu jest idealny w przypadku obozów typu bootcampy, gdzie lekcje zależą od czasu i wymagana jest obecność i ocena",
"self_paced_label": "Samodzielne tempo",
"self_paced_subtitle": "Ten typ kursu jest idealny do kursów, w których uczniowie mogą brać udział w lekcjach we własnym tempie, bez nauczyciela"
}
}
}
@@ -1612,5 +1612,11 @@
"unsaved_changes": {
"message": "Você tem mudanças não salvas. Tem certeza de que deseja sair?"
}
},
"new_course_modal": {
"live_class_label": "Aula ao vivo",
"live_class_subtitle": "Este tipo de curso é ideal para bootcamps onde as aulas são baseadas no tempo e você precisa de frequência e notas",
"self_paced_label": "Individualizado",
"self_paced_subtitle": "Este tipo de curso é ideal para cursos onde os alunos podem ter aulas em seu próprio ritmo, sem professor."
}
}
}
@@ -1612,5 +1612,11 @@
"unsaved_changes": {
"message": "У вас есть неспасенные изменения. Вы уверены, что хотите уйти?"
}
},
"new_course_modal": {
"live_class_label": "Живой урок",
"live_class_subtitle": "Этот тип курса идеально подходит для учебных курсов, где уроки основаны на времени и вам необходимы посещаемость и оценки.",
"self_paced_label": "Самостоятельный темп",
"self_paced_subtitle": "Этот тип курса идеально подходит для курсов, на которых учащиеся могут брать уроки в своем темпе без учителя."
}
}
}
@@ -1612,5 +1612,11 @@
"unsaved_changes": {
"message": "Bạn có những thay đổi chưa được lưu. Bạn có chắc bạn muốn rời đi?"
}
},
"new_course_modal": {
"live_class_label": "Lớp học trực tiếp",
"live_class_subtitle": "Loại khóa học này lý tưởng cho các chương trình đào tạo trong đó các bài học dựa trên thời gian và bạn cần tham dự và chấm điểm",
"self_paced_label": "tự nhịp độ",
"self_paced_subtitle": "Loại khóa học này lý tưởng cho các khóa học mà học sinh có thể học theo tốc độ của riêng mình mà không cần giáo viên"
}
}
}
@@ -0,0 +1,22 @@
import type { Icon } from '@lucide/svelte';
import type { CurrentOrg } from './org';
export interface SidebarData {
teams: CurrentOrg[];
navMain: Array<{
title: string;
url: string;
icon?: typeof Icon;
isActive: boolean;
items?: {
title: string;
url: string;
}[];
}>;
user: {
name: string;
email: string;
avatar: string;
};
}