diff --git a/next.config.js b/next.config.js
index 7fd39d2f..a89afb9b 100644
--- a/next.config.js
+++ b/next.config.js
@@ -6,25 +6,32 @@ const withPWA = require('next-pwa')({
/** @type {import('next').NextConfig} */
const nextConfig = {
- webpack: (config, { dev }) => {
- if (!dev) {
- config.plugins = config.plugins.filter(
- (plugin) => plugin.constructor.name !== 'ESLintWebpackPlugin'
- );
- }
-
- return config;
- },
reactStrictMode: false,
eslint: {
ignoreDuringBuilds: true,
},
experimental: {
scrollRestoration: true,
- esmExternals: true, // Add this to ensure esmExternals is true
+ esmExternals: 'loose',
+ },
+ transpilePackages: ['@uiw/react-md-editor', '@uiw/react-markdown-preview'],
+ async headers() {
+ return [
+ {
+ source: '/:path*',
+ headers: [
+ {
+ key: 'Cross-Origin-Embedder-Policy',
+ value: 'require-corp',
+ },
+ {
+ key: 'Cross-Origin-Opener-Policy',
+ value: 'same-origin',
+ },
+ ],
+ },
+ ];
},
- ignoreDuringBuilds: true,
- transpilePackages: ['react-md-editor']
};
module.exports = withPWA(removeImports(nextConfig));
\ No newline at end of file
diff --git a/package.json b/package.json
index 684e21e6..55576fc9 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.2",
"@heroicons/react": "^2.0.13",
+ "@monaco-editor/react": "^4.6.0",
"@mui/material": "^5.15.2",
"@mui/x-charts": "^6.18.4",
"@react-oauth/google": "^0.12.1",
@@ -26,8 +27,10 @@
"@tailwindcss/forms": "^0.5.3",
"@tremor/react": "^1.8.1",
"@uiw/react-markdown-editor": "^6.1.1",
- "@uiw/react-md-editor": "3.6.0",
+ "@uiw/react-markdown-preview": "^5.1.3",
+ "@uiw/react-md-editor": "^4.0.5",
"@vercel/analytics": "^1.3.1",
+ "@webcontainer/api": "^1.5.1-internal.5",
"asciinema-player": "3.6.3",
"autoprefixer": "^10.4.12",
"babel-plugin-macros": "^3.1.0",
@@ -39,7 +42,7 @@
"easymde": "^2.18.0",
"enable": "^3.4.0",
"focus-visible": "^5.2.0",
- "framer-motion": "^10.2.4",
+ "framer-motion": "^11.15.0",
"heroicons": "^2.0.13",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.456.0",
@@ -53,10 +56,11 @@
"puppeteer": "^22.0.0",
"react": "18.2.0",
"react-activity-calendar": "^2.2.11",
+ "react-beautiful-dnd": "^13.1.1",
"react-chartjs-2": "^5.2.0",
"react-circular-progressbar": "^2.1.0",
"react-collapsible": "^2.10.0",
- "react-confetti": "^6.1.0",
+ "react-confetti": "^6.2.2",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0",
@@ -81,6 +85,7 @@
"react-tooltip": "^5.25.1",
"react-top-loading-bar": "^2.3.1",
"react-transition-group": "^4.4.5",
+ "react-use": "^17.6.0",
"react-visibility-sensor": "^5.1.1",
"reactjs-popup": "^2.0.5",
"reactour": "^1.19.4",
@@ -92,7 +97,8 @@
"tailwind-merge": "^2.3.0",
"tailwindcss": "^3.2.1",
"tailwindcss-animate": "^1.0.7",
- "xterm": "^5.3.0"
+ "xterm": "^5.3.0",
+ "xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"eslint": "8.26.0",
diff --git a/public/loader.webp b/public/loader.webp
new file mode 100644
index 00000000..0dd8747d
Binary files /dev/null and b/public/loader.webp differ
diff --git a/public/whereami.jpeg b/public/whereami.jpeg
new file mode 100644
index 00000000..0b8884b9
Binary files /dev/null and b/public/whereami.jpeg differ
diff --git a/src/components/StandardNav.jsx b/src/components/StandardNav.jsx
index 3b7f37e5..93ab8f7d 100644
--- a/src/components/StandardNav.jsx
+++ b/src/components/StandardNav.jsx
@@ -314,6 +314,16 @@ export function StandardNav({ guestAllowed, alignCenter = true }) {
+
+ Learn
+
+
New!
+
Dashboard
+
+ Learn
+
{
- const fetchData = async () => {
- const response = await fetch(url);
- const data = await response.text();
- setMarkdown(data);
- };
- fetchData();
- }, [url]);
-
- return {markdown};
-}
diff --git a/src/components/learn/LearnNav.jsx b/src/components/learn/LearnNav.jsx
deleted file mode 100644
index d1b1a5ff..00000000
--- a/src/components/learn/LearnNav.jsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import { DonutChart } from '@tremor/react';
-import Link from 'next/link';
-import { useState, useEffect } from 'react';
-
-import request from '@/utils/request';
-
-export function LearnNav({ navElements, lessonNum }) {
- const [lessonProgress, setLessonProgress] = useState(null);
-
- useEffect(() => {
- const url = `${process.env.NEXT_PUBLIC_API_URL}/lessons/${lessonNum}/progress`;
- request(url, 'GET', null)
- .then((data) => {
- console.log(data);
- setLessonProgress(data);
- })
- .catch((err) => {
- console.log(err);
- });
- }, []);
-
- const progressArray = [];
- const colorArray = [];
- if (lessonProgress?.sublessons) {
- for (let i = 0; i < lessonProgress?.sublessons.length; i++) {
- progressArray.push({
- name: `Section ${i + 1}`,
- progress:
- lessonProgress.sublessons[i].progresses.length != 0
- ? parseInt(lessonProgress.sublessons[i].progresses[0]?.progress)
- : 0,
- });
- colorArray.push('blue');
- progressArray.push({
- name: `Section ${i + 1}`,
- progress:
- lessonProgress.sublessons[i].progresses.length != 0
- ? 100 -
- parseInt(lessonProgress.sublessons[i].progresses[0]?.progress)
- : 100,
- });
- colorArray.push('stone');
- }
- }
-
- return (
- <>
-
-
-
-
-
-
- %
-
-
Lesson Progress
- {/**
*/}
-
- -
-
-
- {navElements[0].title}
-
-
- -
-
-
- {navElements[1].title}
-
-
- -
-
-
- {navElements[2].title}
-
-
- -
-
-
- {navElements[3].title}
-
-
-
-
- >
- );
-}
diff --git a/src/components/learn/LearningModule.jsx b/src/components/learn/LearningModule.jsx
deleted file mode 100644
index 374ba907..00000000
--- a/src/components/learn/LearningModule.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { useState, useEffect } from 'react';
-import { ProgressBar } from '@tremor/react';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import request from '@/utils/request';
-
-export function LearningModule({
- lessonId,
- title,
- sections,
- sectionHrefs,
- imgSrc,
- link,
-}) {
- const [lessonProgress, setLessonProgress] = useState(null);
- const router = useRouter();
-
- useEffect(() => {
- const url = `${process.env.NEXT_PUBLIC_API_URL}/lessons/${lessonId}/progress`;
- request(url, 'GET', null)
- .then((data) => {
- setLessonProgress(data);
- })
- .catch((err) => {
- console.log(err);
- });
- }, []);
-
- return (
- <>
-
-

-
- {title}
-
- {lessonProgress ? lessonProgress.totalProgress : 0}%
-
-
-
-
-
-
- {sections[0]}
- {
- router.push(sectionHrefs[0]);
- }}
- className="ml-auto cursor-pointer text-blue-500 hover:text-blue-600"
- >
- View Content →
-
-
-
- {sections[1]}
- {
- router.push(sectionHrefs[1]);
- }}
- className="ml-auto cursor-pointer text-blue-500 hover:text-blue-600"
- >
- View Content →
-
-
-
- {sections[2]}
- {
- router.push(sectionHrefs[2]);
- }}
- className="ml-auto cursor-pointer text-blue-500 hover:text-blue-600"
- >
- Start Task →
-
-
-
- {sections[3]}
- {
- router.push(sectionHrefs[3]);
- }}
- className="ml-auto cursor-pointer text-blue-500 hover:text-blue-600"
- >
- Start Task →
-
-
-
-
-
- >
- );
-}
diff --git a/src/components/learn/LessonNavbar.js b/src/components/learn/LessonNavbar.js
new file mode 100644
index 00000000..40213009
--- /dev/null
+++ b/src/components/learn/LessonNavbar.js
@@ -0,0 +1,49 @@
+import { useState } from 'react';
+import { useRouter } from 'next/router';
+
+export default function LessonNavbar({ lessonId }) {
+ const [isImporting, setIsImporting] = useState(false);
+ const router = useRouter();
+
+ const handleImport = async () => {
+ setIsImporting(true);
+ try {
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/lessons/import/${lessonId}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (!response.ok) throw new Error('Import failed');
+
+ alert('Lesson imported successfully!');
+ } catch (error) {
+ console.error('Error importing lesson:', error);
+ alert('Failed to import lesson');
+ } finally {
+ setIsImporting(false);
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/learn/MarkDone.jsx b/src/components/learn/MarkDone.jsx
deleted file mode 100644
index 3c95b1f0..00000000
--- a/src/components/learn/MarkDone.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useState, useEffect } from 'react';
-import Link from 'next/link';
-import request from '@/utils/request';
-
-export function MarkDone({ sublesson, section, href }) {
- const [marked, setMarked] = useState(false);
- const [showPopup, setShowPopup] = useState(false);
- const [sfx, setSfx] = useState(null);
-
- useEffect(() => {
- setSfx(new Audio('../sounds/success.wav'));
- }, []);
-
- const handleSubmit = () => {
- if (sfx) {
- sfx.currentTime = 0; // Reset the audio to the beginning
- sfx.play(); // Play the audio
- }
-
- // Mark Progress
- const url = `${process.env.NEXT_PUBLIC_API_URL}/lessons/sublesson/${sublesson}/progress/${section}`;
- request(url, 'PUT', {})
- .then((data) => {
- setMarked(true);
- setShowPopup(true);
- setTimeout(() => setShowPopup(false), 4000);
- })
- .catch((err) => {
- // Trigger Unauthenticated Popup
- });
- };
-
- return (
- <>
-
-
-
- {showPopup && (
-
- Progress Saved!
-
- )}
- {marked && }
- >
- );
-}
diff --git a/src/components/learn/Quiz.jsx b/src/components/learn/Quiz.jsx
deleted file mode 100644
index bdd62c9b..00000000
--- a/src/components/learn/Quiz.jsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { useState } from 'react';
-import request from '@/utils/request';
-
-export function Quiz({ page, sublesson, quizData }) {
- const [selectedAnswer, setSelectedAnswer] = useState(null);
- const [showPopup, setShowPopup] = useState(false);
- const [showError, setErrorPopup] = useState(false);
-
- const handleAnswerSelection = (event) => {
- setSelectedAnswer(event.target.value);
- };
-
- const handleSubmit = (event) => {
- event.preventDefault();
-
- const solution = quizData ? quizData[page - 1].solution : '';
- const isCorrect = selectedAnswer === solution;
-
- if (isCorrect) {
- console.log('Submission correct!');
- setShowPopup(true);
- setTimeout(() => setShowPopup(false), 4000);
-
- const url = `${process.env.NEXT_PUBLIC_API_URL}/lessons/sublesson/${sublesson}/progress/${page}`;
- request(url, 'PUT', {})
- .catch((err) => {
- // Trigger Unauthenticated Popup
- });
- } else {
- console.log('Submission incorrect!');
- setErrorPopup(true);
- setTimeout(() => setErrorPopup(false), 4000);
- }
- };
-
- const { question, answers } = quizData
- ? quizData[page - 1]
- : { question: '', answers: [''] };
-
- return (
-
-
- Question {page}:
-
-
- {question}
-
-
- {showPopup && (
-
- Correct!
-
- )}
- {showError && (
-
- Incorrect!
-
- )}
-
- );
-}
diff --git a/src/components/learn/QuizPage.jsx b/src/components/learn/QuizPage.jsx
deleted file mode 100644
index ff399791..00000000
--- a/src/components/learn/QuizPage.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Quiz } from './Quiz';
-import { SectionsNav } from './SectionsNav';
-import { useRouter } from 'next/router';
-import Link from 'next/link';
-import { motion } from 'framer-motion';
-
-function QuizPage({ totalQuizPages, sublesson, quizData, nextPage }) {
- const router = useRouter();
- const [page, setPage] = useState(1);
-
- // Update page state when query param changes
- useEffect(() => {
- const queryPage = parseInt(router.query.quizPage);
- if (queryPage && !isNaN(queryPage)) {
- setPage(queryPage);
- }
- }, [router.query.quizPage]);
-
- const handleNext = () => {
- setPage(page + 1);
- };
-
- const handlePrev = () => {
- setPage(page - 1);
- };
-
- const pagePercentage = parseInt(100 / totalQuizPages);
-
- return (
-
-
-
-
- {[...Array(totalQuizPages)].map(
- (_, index) =>
- page === index + 1 && (
-
- )
- )}
-
-
- {page > 1 && (
-
- )}
- {page < totalQuizPages && (
-
- )}
- {page === totalQuizPages && (
-
-
-
- )}
-
-
- );
-}
-
-export default QuizPage;
diff --git a/src/components/learn/SectionsNav.jsx b/src/components/learn/SectionsNav.jsx
deleted file mode 100644
index c27f0d9a..00000000
--- a/src/components/learn/SectionsNav.jsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { useEffect, useState } from 'react';
-import { ProgressBar } from '@tremor/react';
-import request from '@/utils/request';
-
-export function SectionsNav({ currentPage, cpv, colors, sublesson }) {
- const [lessonProgress, setLessonProgress] = useState(null);
-
- useEffect(() => {
- const url = `${process.env.NEXT_PUBLIC_API_URL}/lessons/sublesson/${sublesson}/progress`;
- request(url, 'GET', null)
- .then((data) => {
- console.log(data);
- setLessonProgress(data);
- })
- .catch((err) => {
- console.log(err);
- });
- }, []);
-
- console.log(lessonProgress);
- if (lessonProgress?.completion) {
- for (let i = 0; i < lessonProgress.completion.length; i++) {
- if (lessonProgress[i]) {
- colors[i] = 'green';
- }
- }
- // colors[currentPage - 1] = "blue";
- }
-
- return (
- <>
-
-
-
- Section Progress
-
-
-
-
- >
- );
-}
diff --git a/src/components/learn/editor/StudioEditor.jsx b/src/components/learn/editor/StudioEditor.jsx
new file mode 100644
index 00000000..aa11859f
--- /dev/null
+++ b/src/components/learn/editor/StudioEditor.jsx
@@ -0,0 +1,1405 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
+import dynamic from 'next/dynamic';
+import { MarkdownViewer } from '@/components/MarkdownViewer';
+import { motion, AnimatePresence } from 'framer-motion';
+import request from '../../../utils/request';
+
+// Dynamic import of MonacoEditor
+const MonacoEditor = dynamic(() => import('@monaco-editor/react'), {
+ ssr: false
+});
+
+const LANGUAGE_CONFIG = {
+ python: {
+ icon: 'fab fa-python',
+ name: 'Python',
+ monacoLang: 'python'
+ },
+ javascript: {
+ icon: 'fab fa-js',
+ name: 'JavaScript',
+ monacoLang: 'javascript'
+ },
+ cpp: {
+ icon: 'fas fa-code',
+ name: 'C++',
+ monacoLang: 'cpp'
+ },
+ bash: {
+ icon: 'fas fa-terminal',
+ name: 'Bash',
+ monacoLang: 'shell'
+ },
+ shell: {
+ icon: 'fas fa-terminal',
+ name: 'Shell Script',
+ monacoLang: 'shell'
+ }
+};
+
+const TUTORIAL_STEPS = [
+ {
+ id: 1,
+ target: 'editor-container',
+ content: 'Welcome to the Studio Editor! Right-click anywhere to start adding components.',
+ position: 'center'
+ },
+ {
+ id: 2,
+ target: 'markdown-component',
+ content: 'Add Markdown components to write formatted text, documentation, or instructions.',
+ position: 'bottom'
+ },
+ {
+ id: 3,
+ target: 'multiple-choice',
+ content: 'Create multiple choice questions to test knowledge. Don\'t forget to set the correct answer!',
+ position: 'bottom'
+ },
+ {
+ id: 4,
+ target: 'code-component',
+ content: 'Add code components to write examples or create coding challenges.',
+ position: 'bottom'
+ },
+ {
+ id: 5,
+ target: 'drag-handle',
+ content: 'Drag components to reorder them. Build your content in any order you like!',
+ position: 'right'
+ }
+];
+
+const getIconForType = (type) => {
+ switch (type) {
+ case 'markdown':
+ return 'markdown';
+ case 'multiple-choice':
+ return 'list-ul';
+ case 'code':
+ return 'code';
+ default:
+ return 'plus';
+ }
+};
+
+const ErrorModal = ({ isOpen, onClose, error }) => (
+
+
e.stopPropagation()}
+ >
+
+
+
Error Saving Lesson
+
+
+ {error || 'An unexpected error occurred while saving. Please try again.'}
+
+
+
+
+
+
+);
+
+// Add this Toast component near the top of the file, after other component imports
+const Toast = ({ message, type = 'success', onClose }) => {
+ // Add useEffect for auto-dismiss
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ onClose();
+ }, 2000);
+
+ // Cleanup timer
+ return () => clearTimeout(timer);
+ }, [onClose]);
+
+ return (
+
+
+ {message}
+
+
+ );
+};
+
+const StudioEditor = ({ initialLesson = null, onLessonCreated }) => {
+ const [isSaving, setIsSaving] = useState(false);
+ const [error, setError] = useState(null);
+ const [toast, setToast] = useState(null);
+ const [pages, setPages] = useState(() => {
+ if (initialLesson && initialLesson.content) {
+ try {
+ // Parse the content directly from initialLesson.content
+ const parsedPages = JSON.parse(initialLesson.content);
+ console.log('Parsed pages from content:', parsedPages);
+ return parsedPages;
+ } catch (error) {
+ console.error('Error parsing lesson content:', error);
+ return [{ id: Date.now(), title: 'Page 1', components: [] }];
+ }
+ }
+ return [{ id: Date.now(), title: 'Page 1', components: [] }];
+ });
+
+ // Remove the window.alert that was showing the content
+ useEffect(() => {
+ console.log('StudioEditor initialLesson:', initialLesson);
+ console.log('StudioEditor initial pages:', pages);
+ }, [initialLesson]);
+
+ // Log whenever pages change
+ useEffect(() => {
+ console.log('Current pages state:', pages);
+ }, [pages]);
+
+ // Initialize pages state
+ const [currentPageId, setCurrentPageId] = useState(() => pages[0]?.id || null);
+
+ // Find current page and its components
+ const currentPage = pages.find(p => p.id === currentPageId) || pages[0];
+ const currentComponents = currentPage?.components || [];
+
+ const [showPageModal, setShowPageModal] = useState(false);
+ const [newPageTitle, setNewPageTitle] = useState('');
+
+ const [menuVisible, setMenuVisible] = useState(false);
+ const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
+ const [showTutorial, setShowTutorial] = useState(false);
+ const [currentStep, setCurrentStep] = useState(0);
+ const [hasSeenTutorial, setHasSeenTutorial] = useState(false);
+ const [showJsonModal, setShowJsonModal] = useState(false);
+ const [showLoadJsonModal, setShowLoadJsonModal] = useState(false);
+ const [jsonInput, setJsonInput] = useState('');
+ const [jsonError, setJsonError] = useState('');
+ const [showImportProjectModal, setShowImportProjectModal] = useState(false);
+ const [projectJsonInput, setProjectJsonInput] = useState('');
+ const [importError, setImportError] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
+ const [saveError, setSaveError] = useState(null);
+
+ // Log the initial data
+ useEffect(() => {
+ // window.alert(JSON.stringify(initialLesson.content));
+ console.log('StudioEditor initialLesson:', initialLesson);
+ console.log('StudioEditor initial pages:', pages);
+ }, [initialLesson]);
+
+ const handleStartFresh = () => {
+ setIsLoading(false);
+ };
+
+ const handleInitialImport = () => {
+ try {
+ const parsedJson = JSON.parse(projectJsonInput);
+ if (!parsedJson.pages || !Array.isArray(parsedJson.pages)) {
+ throw new Error('Invalid project format: missing pages array');
+ }
+
+ setPages(parsedJson.pages);
+ setCurrentPageId(parsedJson.pages[0]?.id || null);
+ setIsLoading(false);
+ setProjectJsonInput('');
+ setImportError('');
+ } catch (error) {
+ setImportError(error.message);
+ }
+ };
+
+ useEffect(() => {
+ // Check if user has seen tutorial before
+ const tutorialSeen = localStorage.getItem('studioEditorTutorialSeen');
+ if (!tutorialSeen) {
+ setShowTutorial(true);
+ localStorage.setItem('studioEditorTutorialSeen', 'true');
+ }
+ }, []);
+
+ const nextTutorialStep = () => {
+ if (currentStep < TUTORIAL_STEPS.length - 1) {
+ setCurrentStep(currentStep + 1);
+ } else {
+ setShowTutorial(false);
+ setHasSeenTutorial(true);
+ }
+ };
+
+ const skipTutorial = () => {
+ setShowTutorial(false);
+ setHasSeenTutorial(true);
+ };
+
+ // Handle right-click to show component menu
+ const handleContextMenu = (e) => {
+ e.preventDefault();
+ setMenuPosition({ x: e.clientX, y: e.clientY });
+ setMenuVisible(true);
+ };
+
+ // Handle click outside to close menu
+ useEffect(() => {
+ const handleClickOutside = () => setMenuVisible(false);
+ document.addEventListener('click', handleClickOutside);
+ return () => document.removeEventListener('click', handleClickOutside);
+ }, []);
+
+ // Handle adding new components
+ const handleAddComponent = (type) => {
+ const newComponent = {
+ id: Date.now(),
+ type,
+ content: '',
+ config: getDefaultConfig(type)
+ };
+
+ setPages(prevPages => prevPages.map(page => {
+ if (page.id === currentPageId) {
+ return {
+ ...page,
+ components: [...page.components, newComponent]
+ };
+ }
+ return page;
+ }));
+ setMenuVisible(false);
+ };
+
+ // Get default configuration based on component type
+ const getDefaultConfig = (type) => {
+ switch (type) {
+ case 'markdown':
+ return { preview: false };
+ case 'multiple-choice':
+ return { options: [], correctAnswer: null };
+ case 'code':
+ return { language: 'python', testCases: [] };
+ default:
+ return {};
+ }
+ };
+
+ // Handle updating component content
+ const handleUpdateComponent = (id, updates) => {
+ setPages(prevPages => prevPages.map(page => {
+ if (page.id === currentPageId) {
+ return {
+ ...page,
+ components: page.components.map(comp =>
+ comp.id === id ? { ...comp, ...updates } : comp
+ )
+ };
+ }
+ return page;
+ }));
+ };
+
+ const insertText = (id, text) => {
+ const component = currentComponents.find(comp => comp.id === id);
+ if (!component) return;
+
+ const textarea = document.querySelector(`textarea[data-id="${id}"]`);
+ if (!textarea) return;
+
+ const start = textarea.selectionStart;
+ const end = textarea.selectionEnd;
+ const currentContent = component.content || '';
+ const newContent = currentContent.substring(0, start) + text + currentContent.substring(end);
+
+ handleUpdateComponent(id, { content: newContent });
+ };
+
+ // Handle deleting components
+ const handleDeleteComponent = (id) => {
+ setPages(prevPages => prevPages.map(page => {
+ return {
+ ...page,
+ components: page.components.filter(comp => comp.id !== id)
+ };
+ }));
+ };
+
+ // Handle drag and drop reordering
+ const onDragEnd = (result) => {
+ if (!result.destination) return;
+
+ const items = Array.from(currentComponents);
+ const [reorderedItem] = items.splice(result.source.index, 1);
+ items.splice(result.destination.index, 0, reorderedItem);
+
+ setPages(prevPages => prevPages.map(page => {
+ if (page.id === currentPageId) {
+ return {
+ ...page,
+ components: items
+ };
+ }
+ return page;
+ }));
+ };
+
+ // Render different component types
+ const renderComponent = (component) => {
+ switch (component.type) {
+ case 'markdown':
+ return (
+
+
+
+
+
Markdown Editor
+
+
+ {['bold', 'italic', 'heading', 'link', 'code'].map((tool) => (
+ insertText(component.id, getToolMarkdown(tool))}
+ className="hover:bg-white/10 p-1.5 rounded transition-all group"
+ >
+
+
+ ))}
+
+
+ e.stopPropagation()}>
+
+
+
+
+
+
+
Drag to reorder
+
handleDeleteComponent(component.id)}
+ className="bg-red-500/20 hover:bg-red-500/30 text-red-400 hover:text-red-300 px-4 py-1.5 rounded-lg transition-all text-sm font-medium flex items-center space-x-2"
+ >
+
+ Delete
+
+
+
+ );
+ case 'multiple-choice':
+ return (
+
+
+
+
+
Multiple Choice
+
+
+ e.stopPropagation()}>
+
+
+
handleUpdateComponent(component.id, { content: e.target.value })}
+ className="w-full bg-neutral-900/50 text-white p-4 rounded-xl border border-neutral-700/30 focus:border-blue-500/50 focus:ring-2 focus:ring-blue-500/20 transition-all text-sm"
+ placeholder="Enter your question..."
+ />
+
+
+
+ {component.config.options.map((option, index) => (
+
e.stopPropagation()}
+ >
+ {
+ e.stopPropagation();
+ handleUpdateComponent(component.id, {
+ config: { ...component.config, correctAnswer: index }
+ });
+ }}
+ className="flex items-center space-x-2 bg-neutral-900/50 px-4 py-2 rounded-xl border border-neutral-700/30 cursor-pointer group/toggle hover:bg-neutral-800/50 transition-all"
+ >
+
+
+
+
+ Correct Answer
+
+ {component.config.correctAnswer === index && (
+
+
+
+ )}
+
+ e.stopPropagation()}>
+
+
{
+ const newOptions = [...component.config.options];
+ newOptions[index] = e.target.value;
+ handleUpdateComponent(component.id, {
+ config: { ...component.config, options: newOptions }
+ });
+ }}
+ className={`
+ w-full bg-neutral-900/50 text-white p-3 rounded-xl border transition-all text-sm
+ ${component.config.correctAnswer === index
+ ? 'border-green-500/30 focus:border-green-500/50 focus:ring-2 focus:ring-green-500/20'
+ : 'border-neutral-700/30 focus:border-blue-500/50 focus:ring-2 focus:ring-blue-500/20'}
+ `}
+ placeholder={`Option ${index + 1}`}
+ />
+
+ {
+ e.stopPropagation();
+ const newOptions = component.config.options.filter((_, i) => i !== index);
+ let newCorrectAnswer = component.config.correctAnswer;
+ if (index === component.config.correctAnswer) {
+ newCorrectAnswer = null;
+ } else if (index < component.config.correctAnswer) {
+ newCorrectAnswer--;
+ }
+ handleUpdateComponent(component.id, {
+ config: {
+ ...component.config,
+ options: newOptions,
+ correctAnswer: newCorrectAnswer
+ }
+ });
+ }}
+ className="bg-red-500/20 hover:bg-red-500/30 text-red-400 hover:text-red-300 p-2 rounded-lg transition-all"
+ >
+
+
+
+ ))}
+
+
+
+ handleUpdateComponent(component.id, {
+ config: {
+ ...component.config,
+ options: [...component.config.options, '']
+ }
+ })}
+ className="bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 hover:text-blue-300 px-4 py-2 rounded-lg transition-all text-sm font-medium flex items-center space-x-2"
+ >
+
+ Add Option
+
+
+
+
+
Drag to reorder
+
handleDeleteComponent(component.id)}
+ className="bg-red-500/20 hover:bg-red-500/30 text-red-400 hover:text-red-300 px-4 py-1.5 rounded-lg transition-all text-sm font-medium flex items-center space-x-2"
+ >
+
+ Delete
+
+
+ {component.config.correctAnswer === null && component.config.options.length > 0 && (
+
+
+
+ Please select a correct answer
+
+
+ )}
+
+ );
+ case 'code':
+ return (
+
+
+
+
+
Code Editor
+
+
+
+
+
+ handleUpdateComponent(component.id, { content: value })}
+ theme="vs-dark"
+ options={{
+ minimap: { enabled: false },
+ fontSize: 14,
+ padding: { top: 16, bottom: 16 },
+ scrollBeyondLastLine: false,
+ renderLineHighlight: 'all',
+ fontFamily: 'JetBrains Mono, monospace',
+ roundedSelection: true,
+ cursorStyle: 'line-thin',
+ cursorBlinking: 'smooth',
+ }}
+ />
+
+
+
+
Drag to reorder
+
handleDeleteComponent(component.id)}
+ className="bg-red-500/20 hover:bg-red-500/30 text-red-400 hover:text-red-300 px-4 py-1.5 rounded-lg transition-all text-sm font-medium flex items-center space-x-2"
+ >
+
+ Delete
+
+
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ // Add this helper function for markdown toolbar
+ const getToolMarkdown = (tool) => {
+ switch (tool) {
+ case 'bold':
+ return '**bold text**';
+ case 'italic':
+ return '*italic text*';
+ case 'heading':
+ return '# Heading';
+ case 'link':
+ return '[link text](url)';
+ case 'code':
+ return '```\ncode block\n```';
+ default:
+ return '';
+ }
+ };
+
+ const JsonViewerModal = () => (
+
+ {showJsonModal && (
+ setShowJsonModal(false)}
+ >
+ e.stopPropagation()}
+ className="bg-neutral-800/90 rounded-xl border border-neutral-700/50 shadow-2xl backdrop-blur-sm w-full max-w-3xl m-4"
+ >
+
+
+
+ Component JSON Data
+
+
+
+
+
+
+ {JSON.stringify(currentComponents, null, 2)}
+
+
+
+
+ This data will be stored in the backend
+
+
+
+
+
+
+ )}
+
+ );
+
+ const handleLoadJson = () => {
+ try {
+ const parsedJson = JSON.parse(jsonInput);
+ if (!Array.isArray(parsedJson)) {
+ throw new Error('JSON must be an array of components');
+ }
+
+ // Validate each component has required fields
+ parsedJson.forEach(comp => {
+ if (!comp.type || !comp.id) {
+ throw new Error('Each component must have type and id fields');
+ }
+ });
+
+ setPages(prevPages => prevPages.map(page => {
+ if (page.id === currentPageId) {
+ return {
+ ...page,
+ components: parsedJson
+ };
+ }
+ return page;
+ }));
+ setJsonInput('');
+ setJsonError('');
+ setShowLoadJsonModal(false);
+ } catch (error) {
+ setJsonError(error.message);
+ }
+ };
+
+ const LoadJsonModal = () => (
+
+ {showLoadJsonModal && (
+ setShowLoadJsonModal(false)}
+ >
+ e.stopPropagation()}
+ className="bg-neutral-800/90 rounded-xl border border-neutral-700/50 shadow-2xl backdrop-blur-sm w-full max-w-3xl m-4"
+ >
+
+
+
+ Load Components from JSON
+
+
+
+
+
+ {jsonError && (
+
+
+ {jsonError}
+
+ )}
+
+
+
+
+
+
+
+ )}
+
+ );
+
+ // Add page management functions
+ const handleAddPage = () => {
+ if (!newPageTitle.trim()) return;
+
+ const newPage = {
+ id: Date.now(),
+ title: newPageTitle,
+ components: []
+ };
+
+ setPages(prevPages => [...prevPages, newPage]);
+ setCurrentPageId(newPage.id);
+ setNewPageTitle(''); // Clear the input
+ setShowPageModal(false); // Close modal
+ };
+
+ const handleDeletePage = (pageId) => {
+ if (pages.length <= 1) return; // Don't delete last page
+ setPages(pages.filter(p => p.id !== pageId));
+ if (currentPageId === pageId) {
+ setCurrentPageId(pages[0].id);
+ }
+ };
+
+ const handleRenamePage = (pageId, newTitle) => {
+ setPages(pages.map(page =>
+ page.id === pageId ? { ...page, title: newTitle } : page
+ ));
+ };
+
+ // Add handler for project import
+ const handleImportProject = () => {
+ try {
+ const parsedJson = JSON.parse(projectJsonInput);
+ if (!parsedJson.pages || !Array.isArray(parsedJson.pages)) {
+ throw new Error('Invalid project format: missing pages array');
+ }
+
+ // Validate project structure
+ parsedJson.pages.forEach(page => {
+ if (!page.id || !page.title || !Array.isArray(page.components)) {
+ throw new Error('Invalid page format');
+ }
+ });
+
+ setPages(parsedJson.pages);
+ setCurrentPageId(parsedJson.pages[0]?.id || null);
+ setProjectJsonInput('');
+ setImportError('');
+ setShowImportProjectModal(false);
+ } catch (error) {
+ setImportError(error.message);
+ }
+ };
+
+ // Add handler for project export
+ const handleExportProject = () => {
+ const projectData = {
+ pages,
+ version: "1.0",
+ exportedAt: new Date().toISOString()
+ };
+
+ const jsonString = JSON.stringify(projectData, null, 2);
+
+ // Create and trigger download
+ const blob = new Blob([jsonString], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `ctfguide-project-${Date.now()}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ };
+
+ // Add Import Project Modal component
+ const ImportProjectModal = () => {
+ if (!showImportProjectModal) return null;
+
+ return (
+ setShowImportProjectModal(false)}
+ >
+
e.stopPropagation()}
+ >
+
+
+
+ Import Project
+
+
+
+
+
+
+ {importError && (
+
+
+ {importError}
+
+ )}
+
+
+
+
+
+
+
+ );
+ };
+
+ // Add the context menu component
+ const ContextMenu = () => (
+
+ {menuVisible && (
+
+ {['markdown', 'multiple-choice', 'code'].map((type) => (
+ handleAddComponent(type)}
+ >
+
+ {type.replace('-', ' ')}
+
+ ))}
+
+ )}
+
+ );
+
+ const Sidebar = () => (
+
+ {/* Pages Section */}
+
+
+
+
+
+
+ Pages
+
+
setShowPageModal(true)}
+ className="bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 p-1.5 rounded-lg transition-all text-sm"
+ >
+
+
+
+
+ {(provided) => (
+
+ {pages.map((page, index) => (
+
+ {(provided, snapshot) => (
+ setCurrentPageId(page.id)}
+ >
+
+
+ {page.title}
+
+
+
+ {pages.length > 1 && (
+
+ )}
+
+
+ )}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+
+ {/* Renamed from JSON Actions to Lesson Actions */}
+
+
+
+ Lesson Actions
+
+
+ {/* Save button in green */}
+
+ {isSaving ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ <>
+
+ Save Lesson
+ >
+ )}
+
+
+ {/* Import button in orange */}
+ setShowImportProjectModal(true)}
+ className="w-full bg-orange-500/20 hover:bg-orange-500/30 text-orange-400 px-4 py-2 rounded-lg transition-all text-sm font-medium flex items-center justify-center space-x-2"
+ >
+
+ Import Project
+
+
+ {/* Rest of the buttons remain unchanged */}
+
+
+ Export Project
+
+ setShowJsonModal(true)}
+ className="w-full bg-purple-500/20 hover:bg-purple-500/30 text-purple-400 px-4 py-2 rounded-lg transition-all text-sm font-medium flex items-center justify-center space-x-2"
+ >
+
+ View JSON
+
+
+
+
+ );
+
+ // Add new page modal
+ const NewPageModal = () => {
+ if (!showPageModal) return null; // Don't render if not showing
+
+ return (
+ setShowPageModal(false)}
+ >
+
e.stopPropagation()}
+ >
+
Add New Page
+
setNewPageTitle(e.target.value)}
+ placeholder="Enter page title"
+ className="w-full bg-neutral-900/50 text-white p-3 rounded-xl border border-neutral-700/30 focus:border-blue-500/50 focus:ring-2 focus:ring-blue-500/20 transition-all mb-4"
+ autoFocus
+ />
+
+
+
+
+
+
+ );
+ };
+
+ // Add state for rename modal
+ const [showRenameModal, setShowRenameModal] = useState(false);
+ const [pageToRename, setPageToRename] = useState(null);
+ const [newTitle, setNewTitle] = useState('');
+
+ // Add RenamePageModal component
+ const RenamePageModal = () => {
+ if (!showRenameModal || !pageToRename) return null;
+
+ return (
+ {
+ setShowRenameModal(false);
+ setPageToRename(null);
+ setNewTitle('');
+ }}
+ >
+
e.stopPropagation()}
+ >
+
Rename Page
+
setNewTitle(e.target.value)}
+ placeholder="Enter new page title"
+ className="w-full bg-neutral-900/50 text-white p-3 rounded-xl border border-neutral-700/30 focus:border-blue-500/50 focus:ring-2 focus:ring-blue-500/20 transition-all mb-4"
+ autoFocus
+ />
+
+
+
+
+
+
+ );
+ };
+
+ useEffect(() => {
+ // Handle any additional initialization needed for imported projects
+ if (initialLesson) {
+ // You could add additional validation or data processing here
+ console.log('Project imported successfully');
+ }
+ }, [initialLesson]);
+
+ // Add this new function to handle page reordering
+ const onDragEndPages = (result) => {
+ if (!result.destination) return;
+
+ const items = Array.from(pages);
+ const [reorderedItem] = items.splice(result.source.index, 1);
+ items.splice(result.destination.index, 0, reorderedItem);
+
+ setPages(items);
+ };
+
+ const handleSaveLesson = async () => {
+ try {
+ setIsSaving(true);
+ setError(null);
+
+ // Format the lesson data
+ const lessonData = {
+ title: initialLesson?.title || pages[0]?.title || 'Untitled Lesson',
+ description: initialLesson?.description || '',
+ content: JSON.stringify(pages.map(page => ({
+ id: page.id,
+ title: page.title,
+ components: page.components || []
+ }))),
+ pages: pages.map((page, index) => ({
+ title: page.title,
+ content: JSON.stringify(page.components || []),
+ order: index
+ }))
+ };
+
+ console.log('Sending lesson data:', lessonData);
+
+ let response;
+ if (initialLesson?.id) {
+ response = await request(
+ `${process.env.NEXT_PUBLIC_API_URL}/lessons/${initialLesson.id}`,
+ 'PUT',
+ lessonData
+ );
+ } else {
+ response = await request(
+ `${process.env.NEXT_PUBLIC_API_URL}/lessons`,
+ 'POST',
+ lessonData
+ );
+ }
+
+ console.log('Save response:', response);
+
+ if (response.error) {
+ throw new Error(response.error);
+ }
+
+ // Replace alert with toast
+ setToast({ message: 'Lesson saved successfully!', type: 'success' });
+
+ if (!initialLesson?.id && response.id) {
+ onLessonCreated?.(response);
+ }
+
+ } catch (error) {
+ console.error('Failed to save lesson:', error);
+ setError(error.message || 'Failed to save lesson. Please try again.');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Main content area - remove bottom padding */}
+
+
+
+ {(provided) => (
+
+ {currentComponents.map((component, index) => (
+
+ {(provided) => (
+
+
+
+
+
+
+ {renderComponent(component)}
+
+
+
+ )}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+
+
+
setError(null)}
+ error={error}
+ />
+
+ {toast && (
+ setToast(null)}
+ />
+ )}
+
+
+ );
+};
+
+export default StudioEditor;
+
diff --git a/src/components/learn/modules/ModuleLayout.jsx b/src/components/learn/modules/ModuleLayout.jsx
new file mode 100644
index 00000000..e9ba1b5b
--- /dev/null
+++ b/src/components/learn/modules/ModuleLayout.jsx
@@ -0,0 +1,317 @@
+import { motion } from 'framer-motion';
+import { Fragment, useState, useEffect } from 'react';
+import { Dialog, Transition } from '@headlessui/react';
+import { XMarkIcon } from '@heroicons/react/24/outline';
+import { getCookie } from '@/utils/request';
+import { useRouter } from 'next/router';
+import UpNext from './UpNext';
+
+const ModuleCard = ({ title, description, progress, content, currentPage, id, onCardClick }) => {
+ const router = useRouter();
+
+ // Safely parse content and handle missing/invalid content
+ let parsedContent = [];
+ try {
+ if (content) {
+ parsedContent = typeof content === 'string' ? JSON.parse(content) : content;
+ parsedContent = Array.isArray(parsedContent) ? parsedContent : [];
+ }
+ } catch (error) {
+ console.warn(`Failed to parse content for module: ${title}`, error);
+ }
+
+ // Only show first 4 pages from the content
+ const previewPages = parsedContent.slice(0, 4);
+ const hasMorePages = parsedContent.length > 4;
+
+ const handlePageClick = (e, pageIndex) => {
+ e.stopPropagation(); // Prevent card click from triggering
+ router.push(`/learn/${id}?page=${pageIndex + 1}`);
+ };
+
+ // Determine status based on progress
+ const getStatus = () => {
+ if (progress === 100) return 'Completed';
+ if (progress > 0) return 'In Progress';
+ return 'Not Started';
+ };
+
+ // Get status color classes
+ const getStatusClasses = () => {
+ switch (getStatus()) {
+ case 'Completed':
+ return 'bg-green-500/20 text-green-400';
+ case 'In Progress':
+ return 'bg-blue-500/20 text-blue-400';
+ case 'Not Started':
+ return 'bg-gray-500/20 text-gray-400';
+ default:
+ return 'bg-gray-500/20 text-gray-400';
+ }
+ };
+
+ return (
+ onCardClick({ id, title, description, content, currentPage })}
+ >
+
+
+
{title}
+
{description}
+
+
+ {/* Pages Preview List */}
+
+ {previewPages.map((page, index) => (
+
handlePageClick(e, index)}
+ >
+
+
+ {page.title}
+
+
+ ))}
+
+ {/* More pages indicator */}
+ {hasMorePages && (
+
+ +{parsedContent.length - 4} more pages...
+
+ )}
+
+
+
+
+
+
+
+
+ {progress}% Complete
+
+ {getStatus()}
+
+
+
+
+
+ );
+};
+
+const ModuleSlideOver = ({ module, open, setOpen }) => {
+ const router = useRouter();
+ let parsedContent = [];
+ try {
+ if (module?.content) {
+ parsedContent = typeof module.content === 'string'
+ ? JSON.parse(module.content)
+ : module.content;
+ parsedContent = Array.isArray(parsedContent) ? parsedContent : [];
+ }
+ } catch (error) {
+ console.warn(`Failed to parse content for module: ${module?.title}`, error);
+ }
+
+ const handlePageClick = (pageIndex) => {
+ router.push(`/learn/${module.id}?page=${pageIndex + 1}`);
+ setOpen(false); // Close the slide over after navigation
+ };
+
+ return (
+
+
+
+ );
+};
+
+const ModuleLayout = () => {
+ const router = useRouter();
+ const [selectedModule, setSelectedModule] = useState(null);
+ const [isSlideOverOpen, setIsSlideOverOpen] = useState(false);
+ const [modules, setModules] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [stats, setStats] = useState({});
+
+ useEffect(() => {
+ const fetchModules = async () => {
+ try {
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/lessons/published`, {
+ headers: {
+ 'Authorization': `Bearer ${getCookie('idToken')}`
+ }
+ });
+
+ const data = await response.json();
+ const transformedModules = data.lessons.map(lesson => {
+ let parsedContent = [];
+ try {
+ if (lesson.content) {
+ parsedContent = typeof lesson.content === 'string'
+ ? JSON.parse(lesson.content)
+ : lesson.content;
+ parsedContent = Array.isArray(parsedContent) ? parsedContent : [];
+ }
+ } catch (error) {
+ console.warn(`Failed to parse content for lesson: ${lesson.title}`, error);
+ }
+
+ const userProgress = lesson.progress?.[0];
+ const currentPage = userProgress?.currentPage || 0;
+ const percentage = userProgress?.percentage || 0;
+
+ return {
+ id: lesson.id,
+ title: lesson.title,
+ description: lesson.description || 'No description available',
+ content: parsedContent,
+ currentPage,
+ progress: percentage,
+ status: percentage > 0 ? 'In Progress' : 'Not Started'
+ };
+ });
+
+ setModules(transformedModules);
+ setStats(data.stats);
+ } catch (error) {
+ console.error('Error fetching modules:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchModules();
+ }, []);
+
+ const findNextLesson = (modules) => {
+ // Find first incomplete or lowest progress lesson
+ return modules.find(module => module.progress < 100) || modules[0];
+ };
+
+ const handleModuleClick = (moduleData) => {
+ setSelectedModule(moduleData);
+ setIsSlideOverOpen(true);
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+
Learning Modules
+
+ {modules.map((module, index) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+export default ModuleLayout;
\ No newline at end of file
diff --git a/src/components/learn/modules/UpNext.jsx b/src/components/learn/modules/UpNext.jsx
new file mode 100644
index 00000000..3f76b268
--- /dev/null
+++ b/src/components/learn/modules/UpNext.jsx
@@ -0,0 +1,79 @@
+import { motion } from 'framer-motion';
+import { ArrowRightIcon } from '@heroicons/react/24/outline';
+import { useRouter } from 'next/router';
+
+const UpNext = ({ className, nextLesson }) => {
+ const router = useRouter();
+
+ if (!nextLesson) {
+ return null;
+ }
+
+ // Parse content to get the last accessed page info
+ let parsedContent = [];
+ let lastAccessedPage = null;
+ try {
+ parsedContent = Array.isArray(nextLesson.content) ? nextLesson.content : [];
+ lastAccessedPage = parsedContent[nextLesson.currentPage];
+ } catch (error) {
+ console.warn('Failed to parse content for next lesson', error);
+ }
+
+ const totalPages = parsedContent.length || 1; // Fallback to 1 if length is undefined
+ const currentPage = nextLesson.currentPage || 0;
+ const progressPercentage = (currentPage + 1) / totalPages * 100;
+
+ return (
+
+
+
Continue Learning
+
+
+
+
router.push(`/learn/${nextLesson.id}?page=${nextLesson.currentPage + 1}`)}
+ >
+
+
+
+
+
+ CONTINUE MODULE
+
+
+ {lastAccessedPage ? lastAccessedPage.title : nextLesson.title}
+
+ {lastAccessedPage && (
+
+
{nextLesson.title}
+
+ Page {nextLesson.currentPage + 1} of {parsedContent.length}
+
+
+ )}
+
+
+
+ Continue Learning
+
+
+
+
+
+
+
+ );
+};
+
+export default UpNext;
diff --git a/src/components/learn/modules/cards/ModuleCard.jsx b/src/components/learn/modules/cards/ModuleCard.jsx
new file mode 100644
index 00000000..d8585ada
--- /dev/null
+++ b/src/components/learn/modules/cards/ModuleCard.jsx
@@ -0,0 +1,45 @@
+import { motion } from 'framer-motion';
+
+const ModuleCard = ({ title, description, image, status, type, completed, active }) => {
+ return (
+
+
+ {image && (
+
+

+
+ )}
+
{title}
+
{description}
+
+
+
+ {completed && (
+
+ {completed}
+
+ )}
+
+ {status && (
+
+ {status}
+
+ )}
+
+
+
+ );
+};
+
+export default ModuleCard;
\ No newline at end of file
diff --git a/src/components/learn/modules/cards/UpNextCard.jsx b/src/components/learn/modules/cards/UpNextCard.jsx
new file mode 100644
index 00000000..f381fc16
--- /dev/null
+++ b/src/components/learn/modules/cards/UpNextCard.jsx
@@ -0,0 +1,69 @@
+import { motion } from 'framer-motion';
+
+const UpNextCard = ({ title, description, type, image, currentPage, totalPages, lastPageTitle }) => {
+ if (type === 'lab') {
+ return (
+
+
+
+
{lastPageTitle || title}
+
{lastPageTitle ? title : description}
+ {lastPageTitle && (
+
+
+ Page {currentPage + 1} of {totalPages}
+
+
+
+ )}
+
+
+
+ {type.toUpperCase()}
+
+
+
+
+ {image &&

}
+
+
+ );
+ }
+
+ if (type === 'task') {
+ return (
+
+
+
+
{title}
+
{description}
+
+
+
+ {type.toUpperCase()}
+
+
+
+
+ );
+ }
+
+ return (
+
+
Component failure. You did not specify a valid type for the UpNextCard component.
+
+ );
+};
+
+export default UpNextCard;
diff --git a/src/components/learn/modules/progress/LessonBar.jsx b/src/components/learn/modules/progress/LessonBar.jsx
new file mode 100644
index 00000000..429bf4b2
--- /dev/null
+++ b/src/components/learn/modules/progress/LessonBar.jsx
@@ -0,0 +1,34 @@
+import { motion } from 'framer-motion';
+
+const LessonBar = ({ name, progress, type }) => {
+ const getTypeStyles = () => {
+ switch(type.toLowerCase()) {
+ case 'lab':
+ return "bg-[#2b2b2b] hover:bg-[#3d3d3d] text-blue-400";
+ case 'task':
+ return "bg-gradient-to-r from-yellow-600/90 to-yellow-700/80 text-white";
+ case 'final':
+ return "bg-gradient-to-r from-green-600/90 to-green-700/80 text-white";
+ default:
+ return "bg-[#2b2b2b]";
+ }
+ };
+
+ return (
+
+
+
+ {name}
+
+
+ {type.toUpperCase()}
+
+
+
+ );
+};
+
+export default LessonBar;
diff --git a/src/components/learn/shared/ProgressSideBar.jsx b/src/components/learn/shared/ProgressSideBar.jsx
new file mode 100644
index 00000000..d5762107
--- /dev/null
+++ b/src/components/learn/shared/ProgressSideBar.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import ModuleCard from '../modules/cards/ModuleCard';
+import LessonBar from '../modules/progress/LessonBar';
+const ProgressSideBar = ({ active, progress }) => {
+ return (
+
+
Module Progress
+
+
{active}
+
{progress}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ProgressSideBar;
\ No newline at end of file
diff --git a/src/components/learn/viewer/LessonViewer.jsx b/src/components/learn/viewer/LessonViewer.jsx
new file mode 100644
index 00000000..ecc9b535
--- /dev/null
+++ b/src/components/learn/viewer/LessonViewer.jsx
@@ -0,0 +1,1055 @@
+"use client"
+import React, { useState, useEffect, useCallback, useRef } from 'react';
+import { MarkdownViewer } from '@/components/MarkdownViewer';
+import dynamic from 'next/dynamic';
+import { motion, AnimatePresence } from 'framer-motion';
+import ReactConfetti from 'react-confetti';
+import { useWindowSize } from 'react-use';
+import { WebContainer } from '@webcontainer/api';
+import { ToastContainer, toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { getCookie } from '@/utils/request';
+import api from '@/utils/terminal-api';
+import { useRouter } from 'next/router';
+// Dynamic import of MonacoEditor
+const MonacoEditor = dynamic(() => import('@monaco-editor/react'), {
+ ssr: false
+});
+
+// Remove the direct xterm imports and replace with dynamic imports
+const Terminal = dynamic(() =>
+ import('xterm').then(mod => mod.Terminal),
+ { ssr: false }
+);
+
+const FitAddon = dynamic(() =>
+ import('xterm-addon-fit').then(mod => mod.FitAddon),
+ { ssr: false }
+);
+
+const renderComponent = (component) => {
+ if (!component || !component.type) {
+ console.error('Invalid component:', component);
+ return null;
+ }
+
+ switch (component.type.toLowerCase()) {
+ case 'markdown':
+ return (
+
+ );
+
+ case 'multiple-choice':
+ return (
+
+ );
+
+ case 'code':
+ return (
+
+ );
+
+ default:
+ console.warn('Unknown component type:', component.type);
+ return (
+
+
Unknown component type: {component.type}
+
+ {JSON.stringify(component, null, 2)}
+
+
+ );
+ }
+};
+
+const MultipleChoiceQuestion = ({ question, options, correctAnswer }) => {
+ const [selectedAnswer, setSelectedAnswer] = useState(null);
+ const [hasSubmitted, setHasSubmitted] = useState(false);
+
+ const handleSubmit = () => {
+ if (selectedAnswer !== null) {
+ setHasSubmitted(true);
+ }
+ };
+
+ return (
+
+
+
{question}
+
+ {options.map((option, index) => (
+
+ ))}
+
+ {!hasSubmitted && (
+
+ )}
+ {hasSubmitted && (
+
+ {selectedAnswer === correctAnswer
+ ? 'Correct! Well done!'
+ : `Incorrect. The correct answer was: ${options[correctAnswer]}`}
+
+ )}
+
+
+ );
+};
+
+const CodeExecutor = ({ initialCode, language }) => {
+ const [code, setCode] = useState(initialCode);
+ const [output, setOutput] = useState('');
+ const [isRunning, setIsRunning] = useState(false);
+ const [isInitializing, setIsInitializing] = useState(false);
+ const webcontainerRef = useRef(null);
+ const currentProcessRef = useRef(null);
+
+ const cleanupWebContainer = async () => {
+ try {
+ // Kill current process if it exists
+ if (currentProcessRef.current) {
+ try {
+ await currentProcessRef.current.kill();
+ } catch (e) {
+ console.log('Process already terminated');
+ }
+ currentProcessRef.current = null;
+ }
+
+ // Teardown existing WebContainer
+ if (webcontainerRef.current) {
+ try {
+ await webcontainerRef.current.teardown();
+ } catch (e) {
+ console.log('WebContainer already torn down');
+ }
+ webcontainerRef.current = null;
+ }
+ } catch (error) {
+ console.error('Error during cleanup:', error);
+ }
+ };
+
+ const initializeWebContainer = async () => {
+ // Clean up any existing instances first
+ await cleanupWebContainer();
+
+ setIsInitializing(true);
+ try {
+ const container = await WebContainer.boot();
+ webcontainerRef.current = container;
+
+ // Set up basic file system
+ await container.mount({
+ 'index.js': {
+ file: {
+ contents: '',
+ },
+ },
+ });
+
+ return container;
+ } catch (error) {
+ console.error('Failed to initialize WebContainer:', error);
+ setOutput('Error: Failed to initialize code execution environment');
+ throw error;
+ } finally {
+ setIsInitializing(false);
+ }
+ };
+
+ const handleRunCode = async () => {
+ setIsRunning(true);
+ setOutput('');
+
+ try {
+ const webcontainer = await initializeWebContainer();
+
+ // Handle different languages
+ switch (language.toLowerCase()) {
+ case 'javascript':
+ case 'js':
+ await webcontainer.fs.writeFile('index.js', code);
+ currentProcessRef.current = await webcontainer.spawn('node', ['index.js']);
+ await handleProcessOutput(currentProcessRef.current);
+ break;
+
+ case 'bash':
+ case 'shell':
+ await webcontainer.fs.writeFile('script.sh', code);
+ currentProcessRef.current = await webcontainer.spawn('bash', ['script.sh']);
+ await handleProcessOutput(currentProcessRef.current);
+ break;
+
+ case 'python':
+ await webcontainer.fs.writeFile('script.py', code);
+ await installDependency(webcontainer, 'python3');
+ currentProcessRef.current = await webcontainer.spawn('python3', ['script.py']);
+ await handleProcessOutput(currentProcessRef.current);
+ break;
+
+ default:
+ setOutput(`Unsupported language: ${language}`);
+ break;
+ }
+ } catch (error) {
+ setOutput('Error executing code: ' + error.message);
+ } finally {
+ setIsRunning(false);
+ currentProcessRef.current = null;
+ }
+ };
+
+ const handleProcessOutput = async (process) => {
+ let outputText = '';
+
+ process.output.pipeTo(
+ new WritableStream({
+ write(chunk) {
+ outputText += chunk;
+ setOutput(outputText);
+ },
+ })
+ );
+
+ process.stderr.pipeTo(
+ new WritableStream({
+ write(chunk) {
+ outputText += chunk;
+ setOutput(outputText);
+ },
+ })
+ );
+
+ const exitCode = await process.exit;
+ if (exitCode !== 0) {
+ setOutput(prev => prev + '\nProcess exited with code ' + exitCode);
+ }
+ };
+
+ const installDependency = async (webcontainer, packageName) => {
+ try {
+ const installProcess = await webcontainer.spawn('apt-get', ['install', '-y', packageName]);
+ await handleProcessOutput(installProcess);
+ } catch (error) {
+ console.error(`Failed to install ${packageName}:`, error);
+ throw error;
+ }
+ };
+
+ // Add a function to map language aliases to Monaco editor language IDs
+ const getMonacoLanguage = (lang) => {
+ const languageMap = {
+ 'python': 'python',
+ 'py': 'python',
+ 'javascript': 'javascript',
+ 'js': 'javascript',
+ 'bash': 'shell',
+ 'shell': 'shell',
+ 'html': 'html',
+ 'css': 'css',
+ 'cpp': 'cpp',
+ 'c++': 'cpp',
+ 'c': 'c',
+ 'java': 'java',
+ 'ruby': 'ruby',
+ 'go': 'go',
+ 'rust': 'rust',
+ 'php': 'php',
+ 'typescript': 'typescript',
+ 'ts': 'typescript'
+ };
+
+ return languageMap[lang.toLowerCase()] || 'plaintext';
+ };
+
+ // Cleanup on component unmount
+ useEffect(() => {
+ return () => {
+ cleanupWebContainer();
+ };
+ }, []);
+
+ return (
+
+
+
+
+ {language.charAt(0).toUpperCase() + language.slice(1)}
+
+
+
+
+
+ {output && (
+
+ )}
+
+
+ );
+};
+
+const UbuntuTerminal = ({ onReady }) => {
+ const [isBooting, setIsBooting] = useState(true);
+ const [bootLogs, setBootLogs] = useState(['Initializing boot sequence...']);
+ const terminalRef = useRef(null);
+ const terminalInstance = useRef(null);
+ const webcontainerInstance = useRef(null);
+
+ const addBootLog = (message) => {
+ setBootLogs(prev => [...prev, message]);
+ console.log('Boot Log:', message);
+ };
+
+ const initializeTerminal = async () => {
+ try {
+ // 1. Initialize xterm
+ addBootLog('Setting up terminal...');
+ const { Terminal } = await import('xterm');
+ const { FitAddon } = await import('xterm-addon-fit');
+ const { WebContainer } = await import('@webcontainer/api');
+
+ const terminal = new Terminal({
+ cursorBlink: true,
+ fontSize: 14,
+ fontFamily: 'monospace',
+ theme: {
+ background: '#000000',
+ foreground: '#ffffff',
+ cursor: '#ffffff',
+ },
+ convertEol: true,
+ cursorStyle: 'block',
+ });
+
+ terminal.open(terminalRef.current);
+
+ const fitAddon = new FitAddon();
+ terminal.loadAddon(fitAddon);
+ fitAddon.fit();
+
+ window.addEventListener('resize', () => fitAddon.fit());
+ terminalInstance.current = terminal;
+
+ // 2. Boot WebContainer
+ addBootLog('Booting WebContainer...');
+ const webcontainer = await WebContainer.boot();
+ webcontainerInstance.current = webcontainer;
+
+ // Set up package.json first
+ await webcontainer.mount({
+ 'package.json': {
+ file: {
+ contents: `{
+ "name": "ctf-environment",
+ "type": "module",
+ "dependencies": {
+ "python-shell": "^3.0.1",
+ "node-fetch": "^3.3.0"
+ }
+ }`
+ }
+ },
+ 'setup.js': {
+ file: {
+ contents: `
+ import { PythonShell } from 'python-shell';
+ import fetch from 'node-fetch';
+
+ // Your Python code can be run using PythonShell
+ // Network requests can be made using fetch
+ console.log('Environment ready!');
+ `
+ }
+ }
+ });
+
+ // Start shell
+ addBootLog('Starting shell...');
+ const shellProcess = await webcontainer.spawn('sh', {
+ terminal: {
+ cols: terminal.cols,
+ rows: terminal.rows,
+ }
+ });
+
+ // Get a single writer for input
+ const input = shellProcess.input.getWriter();
+
+ // Set up environment with Node.js capabilities
+ const initCommands = [
+ 'npm install',
+ 'export PS1="ctf$ "',
+ 'alias python="node -e \'import { PythonShell } from \"python-shell\"; PythonShell.run(process.argv[1])\'"',
+ 'alias curl="node -e \'import fetch from \"node-fetch\"; fetch(process.argv[1])\'"',
+ 'clear',
+ 'echo "=== CTF Environment Ready ==="',
+ 'echo "Available commands:"',
+ 'echo " - node (JavaScript/Node.js)"',
+ 'echo " - python (via python-shell)"',
+ 'echo " - curl (via node-fetch)"',
+ 'echo " - npm install "',
+ ''
+ ];
+
+ // Execute init commands
+ for (const cmd of initCommands) {
+ input.write(cmd + '\n');
+ }
+
+ // Handle terminal input
+ terminal.onData((data) => {
+ input.write(data);
+ });
+
+ // Handle shell output
+ shellProcess.output.pipeTo(
+ new WritableStream({
+ write(data) {
+ terminal.write(data);
+ }
+ })
+ );
+
+ // Handle window resize
+ window.addEventListener('resize', () => {
+ fitAddon.fit();
+ shellProcess.resize({
+ cols: terminal.cols,
+ rows: terminal.rows,
+ });
+ });
+
+ // Focus the terminal
+ terminal.focus();
+ setIsBooting(false);
+
+ } catch (error) {
+ console.error('Failed to initialize terminal:', error);
+ addBootLog(`❌ Error: ${error.message}`);
+ }
+ };
+
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ setIsBooting(false);
+ setTimeout(() => {
+ initializeTerminal();
+ }, 500);
+ }
+
+ return () => {
+ if (terminalInstance.current) {
+ terminalInstance.current.dispose();
+ }
+ if (webcontainerInstance.current) {
+ webcontainerInstance.current.teardown();
+ }
+ };
+ }, []);
+
+ return (
+
+ {isBooting ? (
+
+
+
+
Initializing Terminal...
+
+
+ {bootLogs.map((log, index) => (
+
+ {log}
+
+ ))}
+
+
+
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const CompletionScreen = ({ onRestart }) => {
+ const { width, height } = useWindowSize();
+ const [isConfettiActive, setIsConfettiActive] = useState(true);
+ const router = useRouter();
+
+ useEffect(() => {
+ const timer = setTimeout(() => setIsConfettiActive(false), 5000);
+ return () => clearTimeout(timer);
+ }, []);
+
+ const handleRestart = () => {
+ // Update URL to page 1
+ router.push(
+ {
+ pathname: router.pathname,
+ query: {
+ ...router.query,
+ page: 1
+ }
+ },
+ undefined,
+ { shallow: true }
+ );
+ onRestart();
+ };
+
+ return (
+
+ {isConfettiActive &&
}
+
+
+
+
+
+
+
+
+ Lesson Completed! 🎉
+
+
+
+ Congratulations! You've successfully completed this lesson.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const LessonViewer = ({ lessonData }) => {
+ const router = useRouter();
+ const [currentPageIndex, setCurrentPageIndex] = useState(lessonData?.initialPage || 0);
+ const [pages, setPages] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [showLoadModal, setShowLoadModal] = useState(false);
+ const [jsonInput, setJsonInput] = useState('');
+ const [loadError, setLoadError] = useState('');
+ const [isChallengeFullscreen, setIsChallengeFullscreen] = useState(false);
+ const [showMessage, setShowMessage] = useState(false);
+ const [minutesRemaining, setMinutesRemaining] = useState(60);
+ const [showCompletion, setShowCompletion] = useState(false);
+ const [isTerminalBooted, setIsTerminalBooted] = useState(false);
+ const [terminalUrl, setTerminalUrl] = useState('');
+ const [userName, setUserName] = useState('');
+ const [password, setPassword] = useState('');
+ const [containerId, setContainerId] = useState('');
+ const [foundTerminal, setFoundTerminal] = useState(false);
+ const [fetchingTerminal, setFetchingTerminal] = useState(false);
+
+ useEffect(() => {
+ if (!lessonData || !lessonData.content) {
+ setIsLoading(false);
+ return;
+ }
+
+ try {
+ // The content is already parsed into an array in the parent component
+ setPages(lessonData.content);
+ } catch (error) {
+ console.error('Error processing lesson data:', error);
+ setPages([]);
+ }
+
+ setIsLoading(false);
+ }, [lessonData]);
+
+ const handleLoadLesson = () => {
+ try {
+ const parsedJson = JSON.parse(jsonInput);
+ if (!parsedJson.pages || !Array.isArray(parsedJson.pages)) {
+ throw new Error('Invalid lesson format: missing pages array');
+ }
+
+ setPages(parsedJson.pages);
+ setCurrentPageIndex(0);
+ setShowLoadModal(false);
+ setJsonInput('');
+ setLoadError('');
+ } catch (error) {
+ setLoadError(error.message);
+ }
+ };
+
+ const LoadLessonModal = () => (
+ setShowLoadModal(false)}
+ >
+
e.stopPropagation()}
+ >
+
+
Load Lesson
+
+
+
+
+
+
+
+ {loadError && (
+
+
+ {loadError}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+
+ const handleBootTerminal = async () => {
+ setFetchingTerminal(true);
+ try {
+ const cookie = getCookie('idToken');
+ const data = await api.buildDocketTerminal("e08fecf2-966e-419a-95d5-40e0a98b550e", cookie);
+
+ if (data && data.url) {
+ setPassword(data.terminalUserPassword);
+ setUserName(data.terminalUserName);
+ setContainerId(data.containerId);
+ setFoundTerminal(true);
+
+ // Set minutes based on user role
+ try {
+ const accountResponse = await request(`${process.env.NEXT_PUBLIC_API_URL}/account`, "GET", null);
+ setMinutesRemaining(accountResponse.role === 'PRO' ? 120 : 60);
+ } catch (error) {
+ setMinutesRemaining(60);
+ }
+
+ setTimeout(() => {
+ setTerminalUrl(data.url);
+ setFetchingTerminal(false);
+ setIsTerminalBooted(true);
+ setShowMessage(true);
+ }, 5000);
+ } else {
+ toast.error("Unable to create the terminal, please try again");
+ setFetchingTerminal(false);
+ }
+ } catch (error) {
+ toast.error("Error creating terminal: " + error.message);
+ setFetchingTerminal(false);
+ }
+ };
+
+ const copyToClipboard = (text) => {
+ navigator.clipboard.writeText(text);
+ toast.success("Copied to clipboard!");
+ };
+
+ const formatTime = (minutes) => {
+ const hrs = Math.floor(minutes / 60);
+ const mins = minutes % 60;
+ return `${hrs}:${mins.toString().padStart(2, '0')}`;
+ };
+
+ const handlePageChange = async (newIndex) => {
+ setCurrentPageIndex(newIndex);
+
+ // Update URL with new page number (add 1 for human-readable page numbers)
+ router.push(
+ {
+ pathname: router.pathname,
+ query: {
+ ...router.query,
+ page: newIndex + 1
+ }
+ },
+ undefined,
+ { shallow: true } // Prevents full page reload
+ );
+
+ try {
+ await fetch(`${process.env.NEXT_PUBLIC_API_URL}/lessons/${lessonData.id}/progress`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${getCookie('idToken')}`
+ },
+ body: JSON.stringify({
+ currentPage: newIndex,
+ totalPages: pages.length
+ })
+ });
+ } catch (error) {
+ console.error('Failed to save progress:', error);
+ }
+ };
+
+ if (isLoading) {
+ return Loading...
;
+ }
+
+ if (!pages || pages.length === 0) {
+ return No content available
;
+ }
+
+ const currentPage = pages[currentPageIndex] || null;
+ console.log('Current page index:', currentPageIndex);
+ console.log('Current page:', currentPage);
+ console.log('Total pages:', pages.length);
+
+ return (
+
+
+
+ {isLoading ? (
+
+ ) : (
+ <>
+ {/* Load Lesson Modal */}
+
+
+ {/* Show completion screen when lesson is finished */}
+ {showCompletion && (
+
{
+ setShowCompletion(false);
+ setCurrentPageIndex(0);
+ }}
+ />
+ )}
+
+ {/* Main Content */}
+
+
+ {/* Left side - Lesson Content */}
+
+
{lessonData?.title}
+
+
+ {currentPage?.components?.map((component, index) => (
+
+ {renderComponent(component)}
+
+ ))}
+
+
+
+
+ {/* Right side - Ubuntu Terminal */}
+
+
+ {fetchingTerminal ? (
+
+
+
+
Setting up your terminal...
+
If you see a black screen, please wait a few seconds and refresh the page.
+
+
+ ) : (
+ isTerminalBooted ? (
+ <>
+ {foundTerminal && (
+
+
+ Username: {userName}
+
+
+
+ Password: {password}
+
+
+
+ Time: {formatTime(minutesRemaining)}
+ window.open(terminalUrl, '_blank')} className="cursor-pointer hover:text-yellow-500 ml-2 fas fa-expand">
+
+
+ )}
+
+ >
+ ) : (
+
+
+
+ )
+ )}
+
+
+ {/* Mobile view toggle button */}
+
+
+
+
+
+ {/* Navigation Bar - Now at bottom */}
+
+ >
+ )}
+
+ );
+};
+
+export default LessonViewer;
\ No newline at end of file
diff --git a/src/components/learn/viewer/WebTerminal.jsx b/src/components/learn/viewer/WebTerminal.jsx
new file mode 100644
index 00000000..45634f49
--- /dev/null
+++ b/src/components/learn/viewer/WebTerminal.jsx
@@ -0,0 +1,135 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import { Terminal } from 'xterm';
+import 'xterm/css/xterm.css';
+
+const WebTerminal = () => {
+ const [isBooting, setIsBooting] = useState(false);
+ const [error, setError] = useState(null);
+ const terminalRef = useRef(null);
+ const webcontainerRef = useRef(null);
+ const terminalInstanceRef = useRef(null);
+
+ const initializeEnvironment = async () => {
+ if (typeof window === 'undefined') return;
+
+ if (!window.crossOriginIsolated) {
+ setError('Cross-origin isolation is not enabled. WebContainer requires this to function.');
+ return;
+ }
+
+ setIsBooting(true);
+ try {
+ // Dynamically import WebContainer only in browser
+ const { WebContainer } = await import('@webcontainer/api');
+ const webcontainer = await WebContainer.boot();
+ webcontainerRef.current = webcontainer;
+
+ // Set up basic Ubuntu-like environment
+ await webcontainer.mount({
+ 'setup.sh': {
+ file: {
+ contents: `
+ apt-get update
+ apt-get install -y vim nano git python3 curl
+ echo "root:root" | chpasswd
+ echo "PS1='\\u@\\h:\\w\\$ '" >> ~/.bashrc
+ `
+ }
+ }
+ });
+
+ // Initialize terminal
+ const terminal = new Terminal({
+ cursorBlink: true,
+ fontSize: 14,
+ fontFamily: 'Menlo, Monaco, "Courier New", monospace',
+ theme: {
+ background: '#1a1b1e'
+ }
+ });
+
+ terminal.open(terminalRef.current);
+ terminalInstanceRef.current = terminal;
+
+ // Start shell process
+ const shellProcess = await webcontainer.spawn('bash', {
+ terminal: {
+ cols: terminal.cols,
+ rows: terminal.rows
+ }
+ });
+
+ // Connect terminal to shell
+ terminal.onData((data) => {
+ shellProcess.write(data);
+ });
+
+ // Pipe shell output to terminal
+ shellProcess.output.pipeTo(
+ new WritableStream({
+ write(data) {
+ terminal.write(data);
+ }
+ })
+ );
+
+ // Run initial setup
+ await webcontainer.spawn('bash', ['setup.sh']);
+
+ } catch (error) {
+ console.error('Failed to initialize environment:', error);
+ setError(error.message);
+ } finally {
+ setIsBooting(false);
+ }
+ };
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ if (webcontainerRef.current) {
+ webcontainerRef.current.teardown();
+ }
+ };
+ }, []);
+
+ return (
+
+ {error ? (
+
+ ) : isBooting ? (
+
+
+
⚙️
+
Booting Ubuntu-like environment...
+
This might take a few seconds
+
+
+ ) : !webcontainerRef.current ? (
+
+
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default WebTerminal;
\ No newline at end of file
diff --git a/src/components/settingComponents/sidebar.jsx b/src/components/settingComponents/sidebar.jsx
index a93a5de9..476923e7 100644
--- a/src/components/settingComponents/sidebar.jsx
+++ b/src/components/settingComponents/sidebar.jsx
@@ -10,28 +10,32 @@ export default function Sidebar() {
>
-
-
+
+
+ General
+
+
-
-
+
+
+ Security
+
+
-
-
+
+
+ Email Preferences
+
+
-
-
+
+
+ Billing
+
+
diff --git a/src/pages/challenges/[...id].jsx b/src/pages/challenges/[...id].jsx
index 78202efe..62b4105b 100644
--- a/src/pages/challenges/[...id].jsx
+++ b/src/pages/challenges/[...id].jsx
@@ -1674,7 +1674,7 @@ function CommentsPage({ cache }) {
{comment.role === 'ADMIN' && (
<>
- developer
+ admin
>
)}
{comment.role === 'PRO' && (
diff --git a/src/pages/create.jsx b/src/pages/create.jsx
index d103c52e..272d5d32 100644
--- a/src/pages/create.jsx
+++ b/src/pages/create.jsx
@@ -40,6 +40,15 @@ export default function Create() {
const [showPopup, setShowPopup] = useState(false);
const [loading, setLoading] = useState(true);
+ const [moduleState, setModuleState] = useState('unverified');
+
+ const [writeupState, setWriteupState] = useState('all');
+ const [allWriteups, setAllWriteups] = useState([]);
+
+ const [modules, setModules] = useState([]);
+
+ const [moduleFilter, setModuleFilter] = useState('all');
+
const toggleDropdown = () => {
setIsOpen(!isOpen);
};
@@ -124,6 +133,7 @@ export default function Create() {
});
fetchChallenges("unverified");
+ fetchModules("unverified");
} catch (error) {
setLoading(false);
}
@@ -133,9 +143,10 @@ export default function Create() {
try {
const response = await request(`${process.env.NEXT_PUBLIC_API_URL}/users/${username}/writeups`, "GET", null);
if (Array.isArray(response) && response.length > 0) {
+ setAllWriteups(response);
setWriteups(response);
- console.log(response)
} else {
+ setAllWriteups([]);
setWriteups([]);
}
} catch (error) { }
@@ -302,623 +313,735 @@ export default function Create() {
fetchCreatorMode();
}, []);
- return (
- <>
-
- Create - CTFGuide
-
-
-
-
-
-
-
-
+ const fetchModules = async (selection) => {
+ try {
+ const response = await request(
+ `${process.env.NEXT_PUBLIC_API_URL}/lessons`,
+ "GET",
+ null
+ );
+
+ if (response && response.lessons) {
+ // Filter modules based on selection
+ const filteredModules = response.lessons.filter(module => {
+ if (selection === 'unverified') {
+ return !module.published;
+ } else if (selection === 'published') {
+ return module.published;
+ }
+ return true;
+ });
-
+ setModules(filteredModules);
+ setModuleState(selection);
+ } else {
+ setModules([]);
+ }
+ } catch (error) {
+ console.error('Error fetching modules:', error);
+ setModules([]);
+ }
+ };
-
-
+ const [isVideoModalOpen, setIsVideoModalOpen] = useState(false);
+ const [currentVideo, setCurrentVideo] = useState({ title: '', videoId: '' });
+
+ const tutorials = [
+ {
+ title: 'Challenge Creation',
+ description: 'Learn how to create engaging CTF challenges',
+ icon: 'fas fa-flag',
+ iconColor: 'text-blue-400/80',
+ gradient: 'from-blue-600/20 via-blue-800/20',
+ videoId: 'YOUR_YOUTUBE_VIDEO_ID_1'
+ },
+ {
+ title: 'Writeup Mastery',
+ description: 'Master the art of creating helpful writeups',
+ icon: 'fas fa-pen-fancy',
+ iconColor: 'text-purple-400/80',
+ gradient: 'from-purple-600/20 via-purple-800/20',
+ videoId: 'YOUR_YOUTUBE_VIDEO_ID_2'
+ },
+ {
+ title: 'Learning Modules',
+ description: 'Design educational content for CTF learners',
+ icon: 'fas fa-book',
+ iconColor: 'text-green-400/80',
+ gradient: 'from-green-600/20 via-green-800/20',
+ videoId: 'YOUR_YOUTUBE_VIDEO_ID_3'
+ }
+ ];
-
+ const filterWriteups = (state) => {
+ setWriteupState(state);
+ if (state === 'all') {
+ setWriteups(allWriteups);
+ } else {
+ const filtered = allWriteups.filter(writeup => {
+ if (state === 'draft') return writeup.draft;
+ if (state === 'published') return !writeup.draft;
+ return true;
+ });
+ setWriteups(filtered);
+ }
+ };
+ const renderModules = () => {
+ const filteredModules = modules.filter(module => {
+ if (moduleFilter === 'published') return module.published;
+ if (moduleFilter === 'draft') return !module.published;
+ return true;
+ });
-
+ return (
+
+
+
+
+
+
+
+ {filteredModules.map((module) => (
+
+
+
{module.title}
+
+ {module.published ? 'Published' : 'Draft'}
+
+
+
+ {module.description || 'No description provided'}
+
+
+
+ Last updated: {new Date(module.updatedAt).toLocaleDateString()}
+
+
+ Edit →
+
+
+
+ ))}
+
-
-
Your challenge has been created and submitted for approval.
-
+ {filteredModules.length === 0 && (
+
+ No {moduleFilter !== 'all' ? moduleFilter : ''} modules found
+
+ )}
+
+ );
+ };
+ return (
+ <>
+
+
Creator Studio - CTFGuide
+
+
+
+ {/* Secondary Navigation */}
+
+
+
+
+ Home
+
+
+
+ Earnings & Analytics
+
+
+
+ Creator Settings
+
+
+
+
+
+ {/* Header Section with Creator Mode Toggle and Tutorial Cards */}
+
+
+
+
Creator Studio
+
Manage your challenges, modules, and writeups
+
+
+
+
+
+
+ {/* Tutorial Cards */}
+
+ {tutorials.map((tutorial, index) => (
+
{
+ setCurrentVideo({
+ title: tutorial.title,
+ videoId: tutorial.videoId
+ });
+ setIsVideoModalOpen(true);
+ }}
+ >
+
+
+
+
+
{tutorial.title}
+
+
+
{tutorial.description}
+
+ Watch Tutorial
+
+
+
+
+ ))}
+
+
+ {/* Stats Section */}
+
+
Creator Overview
+
+ {stats.map((stat) => (
+
+
{stat.name}
+ {stat.value}
+
+ ))}
+
+
-
-
-
-
+ {/* Challenges Section */}
+
+
+
+
+
+
+
+
+ {hasChallenges ? (
+
+ {challenges.map((challenge) => (
+
-
-
-
-
- {stats.map((stat) => (
-
-
- {stat.name}
- - {stat.value}
-
- ))}
-
-
-
-
-
-
-
- {hasChallenges && (
-
- )}
-
- {!hasChallenges && (
-
- )}
+ {/* Learn Modules Section */}
+
+
+
-
-
-
-
-
-
- Your Writeups
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Writeup Name
- |
-
- Challenge Name
- |
-
-
- Last Updated
- |
-
- Edit
- |
-
-
-
- {writeups.map((writeup) => (
-
-
-
- {writeup.draft &&
- draft
- }
-
- {!writeup.draft &&
- published
- }
-
- {writeup.title} |
-
- {writeup.challenge.title}
- |
- {new Date(writeup.updatedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })} |
-
-
-
- Edit, {writeup.title}
-
- |
-
- )) ||
- }
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
- Your Learn Modules
-
-
-
+ {modules.length > 0 ? (
+
+ {modules.map((module) => (
+
+
+
{module.title}
+
+ Last updated {new Date(module.updatedAt).toLocaleDateString()}
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Module Name
- |
-
-
-
- Last Updated
- |
-
- Edit
- |
-
-
-
- {writeups.map((writeup) => (
-
-
-
- {writeup.draft &&
- draft
- }
-
- {!writeup.draft &&
- published
- }
-
- {writeup.title} |
-
- {new Date(writeup.updatedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })} |
-
-
-
- |
-
- )) ||
- }
-
-
-
-
+
-
-
-
+ ))}
-
-
-
+ ) : (
+
+ )}
-
-
-
-
-
-
-
-
-
-
-