From 9f668f818c326e56c91b04fbb68403c023669ada Mon Sep 17 00:00:00 2001 From: kynhix Date: Mon, 25 Mar 2024 19:24:45 -0400 Subject: [PATCH 1/4] Added reactive videos to landing page --- src/components/home/SecondaryFeatures.jsx | 234 +++++++++++++--------- 1 file changed, 142 insertions(+), 92 deletions(-) diff --git a/src/components/home/SecondaryFeatures.jsx b/src/components/home/SecondaryFeatures.jsx index fba12c4c..fcc1a1d0 100644 --- a/src/components/home/SecondaryFeatures.jsx +++ b/src/components/home/SecondaryFeatures.jsx @@ -1,6 +1,6 @@ 'use client' -import { useId } from 'react' +import { useEffect, useId, useRef } from 'react' import Image from 'next/image' import { Tab } from '@headlessui/react' import clsx from 'clsx' @@ -48,18 +48,18 @@ const features = [ 'Review recorded student lab sessions.', description: 'We automatically records lab sessions, so you can review them later.', - image: '../../videoproof.mp4', - icon: function InventoryIcon() { + image: '../../videoproof.mp4', + icon: function InventoryIcon() { return ( <> - - - - + + + + ) @@ -71,19 +71,19 @@ const features = [ 'Cloud machines streamed to the browser.', description: 'Easily access cloud machines from your browser, no need to install anything.', - image: '../../terminalproof.mp4', - icon: function ContactsIcon() { + image: '../../terminalproof.mp4', + icon: function ContactsIcon() { return ( <> - - - + + + + - ) }, @@ -123,32 +123,43 @@ function Feature({ feature, isActive, className, ...props }) { } function FeaturesMobile() { + let observer = useRef(null); + + useEffect(() => { + if (!observer.current) { + observer.current = new IntersectionObserver((entries) => { + if (entries[0].intersectionRatio > 0.4) { + entries[0].target.play(); + } else { + entries[0].target.pause(); + } + }, { threshold: [0, 0.5] }); + } + }, []); + return (
{features.map((feature) => (
- - - - + muted + ref={(el) => setTimeout(() => { observer.current.observe(el) })} + + autoSave='true' + loop + onLoadedMetadata={(e) => { + e.target.currentTime = 2; // Skip first two seconds + }} + > + + +
))}
@@ -156,69 +167,108 @@ function FeaturesMobile() { } function FeaturesDesktop() { + let observer = useRef(null); + let selectedVideo = useRef(null); + + useEffect(() => { + if (!observer.current) { + observer.current = new IntersectionObserver((entries) => { + if (entries[0].intersectionRatio > 0.4) { + selectedVideo.current?.play(); + console.log("playing") + } else { + selectedVideo.current?.pause(); + console.log("pausing") + } + }, { threshold: [0, 0.5] }); + } + }, []); + + let previousVideo = null; + const restartVideo = (id) => { + if (typeof window === "undefined") { + return; + } + const vid = document.getElementById(id); + if (!vid || previousVideo == vid) { + return; + } + selectedVideo.current = vid; + console.log("Restarted", id) + vid.currentTime = 0; + if (previousVideo !== null) { + vid.play(); + previousVideo.pause(); + previousVideo.currentTIme = 0; + } + previousVideo = vid; + }; + return ( - - {({ selectedIndex }) => ( - <> - - {features.map((feature, featureIndex) => ( - - - {feature.name} - - ), - }} - isActive={featureIndex === selectedIndex} - className="relative" - /> - ))} - - -
+ setTimeout(() => observer.current?.observe(el) || restartVideo("video-0"))} className="hidden lg:mt-20 lg:block" onChange={(id) => restartVideo("video-" + id.toString())}> + {({ selectedIndex }) => { + return ( + <> + {features.map((feature, featureIndex) => ( - -
- - - + feature={{ + ...feature, + name: ( + + + {feature.name} + + ), + }} + isActive={selectedIndex === featureIndex} + className="relative" + /> + ))} + + +
+ {features.map((feature, featureIndex) => ( + +
+ -
-
- ))} -
-
- - - )} + + +
+ + ))} +
+
+ + + ) + }} ) } @@ -235,11 +285,11 @@ export function SecondaryFeatures() {

We're the best platform for teaching cybersecurity.

- +
- - + +
From 75973e72f2af35bf59897641af6d10868f12be18 Mon Sep 17 00:00:00 2001 From: kynhix Date: Mon, 25 Mar 2024 19:26:51 -0400 Subject: [PATCH 2/4] Fixed small bug when hot reloading in vite --- src/components/home/SecondaryFeatures.jsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/home/SecondaryFeatures.jsx b/src/components/home/SecondaryFeatures.jsx index fcc1a1d0..95b92d9d 100644 --- a/src/components/home/SecondaryFeatures.jsx +++ b/src/components/home/SecondaryFeatures.jsx @@ -148,7 +148,7 @@ function FeaturesMobile() { width={2432} height={1442} muted - ref={(el) => setTimeout(() => { observer.current.observe(el) })} + ref={(el) => setTimeout(() => { el && observer.current.observe(el) })} autoSave='true' loop @@ -175,10 +175,8 @@ function FeaturesDesktop() { observer.current = new IntersectionObserver((entries) => { if (entries[0].intersectionRatio > 0.4) { selectedVideo.current?.play(); - console.log("playing") } else { selectedVideo.current?.pause(); - console.log("pausing") } }, { threshold: [0, 0.5] }); } @@ -194,18 +192,18 @@ function FeaturesDesktop() { return; } selectedVideo.current = vid; - console.log("Restarted", id) vid.currentTime = 0; if (previousVideo !== null) { vid.play(); previousVideo.pause(); previousVideo.currentTIme = 0; } + previousVideo = vid; }; return ( - setTimeout(() => observer.current?.observe(el) || restartVideo("video-0"))} className="hidden lg:mt-20 lg:block" onChange={(id) => restartVideo("video-" + id.toString())}> + setTimeout(() => el && observer.current?.observe(el) || restartVideo("video-0"))} className="hidden lg:mt-20 lg:block" onChange={(id) => restartVideo("video-" + id.toString())}> {({ selectedIndex }) => { return ( <> From e7f2accc5a10aa13fa57d85bbcf10edf3d198cbc Mon Sep 17 00:00:00 2001 From: kynhix Date: Mon, 25 Mar 2024 20:36:43 -0400 Subject: [PATCH 3/4] Added autoplay for videos on desktop Once a video finishes playing, it automatically cycles to the next tab --- src/components/home/SecondaryFeatures.jsx | 48 ++++++++++++++++------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/components/home/SecondaryFeatures.jsx b/src/components/home/SecondaryFeatures.jsx index 95b92d9d..1719937d 100644 --- a/src/components/home/SecondaryFeatures.jsx +++ b/src/components/home/SecondaryFeatures.jsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useId, useRef } from 'react' +import { useEffect, useId, useRef, useState } from 'react' import Image from 'next/image' import { Tab } from '@headlessui/react' import clsx from 'clsx' @@ -167,8 +167,15 @@ function FeaturesMobile() { } function FeaturesDesktop() { - let observer = useRef(null); - let selectedVideo = useRef(null); + const observer = useRef(null); + const selectedVideo = useRef(null); + const previousVideo = useRef(null); + const [selectedIndex, _setSelectedIndex] = useState(0); + const selectedIndexRef = useRef(selectedIndex); + const setSelectedIndex = (index) => { + selectedIndexRef.current = index; + _setSelectedIndex(index); + }; useEffect(() => { if (!observer.current) { @@ -182,29 +189,42 @@ function FeaturesDesktop() { } }, []); - let previousVideo = null; - const restartVideo = (id) => { + const restartVideo = () => { if (typeof window === "undefined") { return; } - const vid = document.getElementById(id); - if (!vid || previousVideo == vid) { + const vid = document.getElementById("video-" + selectedIndexRef.current); + if (!vid || previousVideo.current == vid) { return; } selectedVideo.current = vid; vid.currentTime = 0; - if (previousVideo !== null) { + if (previousVideo.current !== null) { vid.play(); - previousVideo.pause(); - previousVideo.currentTIme = 0; + previousVideo.current.pause(); + previousVideo.current.currentTIme = 0; } - previousVideo = vid; + previousVideo.current = vid; }; + const onVideoEnd = () => { + setSelectedIndex((selectedIndex + 1) % features.length); + restartVideo(); + } + return ( - setTimeout(() => el && observer.current?.observe(el) || restartVideo("video-0"))} className="hidden lg:mt-20 lg:block" onChange={(id) => restartVideo("video-" + id.toString())}> - {({ selectedIndex }) => { + setTimeout(() => el && observer.current?.observe(el) || restartVideo("video-0"))} + className="hidden lg:mt-20 lg:block" + onChange={(index) => { + setSelectedIndex(index); + restartVideo(); + }} + selectedIndex={selectedIndex} + > + {() => { return ( <> @@ -245,11 +265,11 @@ function FeaturesDesktop() { width={1055} height={810} autoSave='true' - loop onLoadedMetadata={(e) => { e.target.currentTime = 2; // Skip first two seconds }} id={"video-" + featureIndex.toString()} + onEnded={onVideoEnd} > From ee31a397c2883a8f9ba9c392f10b84b8934c31ac Mon Sep 17 00:00:00 2001 From: Stephen Stefanatos Date: Tue, 26 Mar 2024 11:13:45 -0400 Subject: [PATCH 4/4] Dynamic Labs --- .../groups/assignments/create-challenge.jsx | 16 +++++++-- .../groups/assignments/createAssignment.jsx | 32 +++++++++++++++-- src/pages/assignments/student/[code].jsx | 36 +++++++++++++++---- src/pages/assignments/teacher/[code].jsx | 23 ++++++++++++ src/utils/request.js | 8 ++++- 5 files changed, 102 insertions(+), 13 deletions(-) diff --git a/src/components/groups/assignments/create-challenge.jsx b/src/components/groups/assignments/create-challenge.jsx index 5879ad6b..fafe46db 100644 --- a/src/components/groups/assignments/create-challenge.jsx +++ b/src/components/groups/assignments/create-challenge.jsx @@ -22,6 +22,7 @@ const styles = { const auth = getAuth(); export default function Createchall(props) { + const pages = [ { name: 'Create Assignment', @@ -36,6 +37,7 @@ export default function Createchall(props) { click: () => {}, }, ]; + const [contentPreview, setContentPreview] = useState(''); const [penalty, setPenalty] = useState([0, 0, 0]); const [hints, setHints] = useState([ @@ -43,6 +45,7 @@ export default function Createchall(props) { 'No hints set', 'No hints set', ]); + const [solution, setSolution] = useState(''); const [difficulty, setDifficulty] = useState('Easy'); const [category, setCategory] = useState('forensics'); @@ -100,13 +103,15 @@ export default function Createchall(props) { const uploadChallenge = async (fileId) => { try { const nConfig = newConfig.replace('\n', ' && '); + let new_solution = props.assignmentInfo.option !== "dynamicLab" ? solution : ""; + const challengeInfo = { name: newChallengeName, category: [category], hints, penalty, content: contentPreview, - solution, + solution: new_solution, difficulty, category, commands: nConfig, @@ -116,7 +121,10 @@ export default function Createchall(props) { const classCode = window.location.pathname.split('/')[2]; const assignmentInfo = props.assignmentInfo; const url = `${process.env.NEXT_PUBLIC_API_URL}/classroom-assignments/create-new-assignment/${classCode}`; - const data = await request(url, 'POST', { challengeInfo, assignmentInfo, username: localStorage.getItem('username') }); + const data = await request(url, 'POST', { + challengeInfo, assignmentInfo, + username: localStorage.getItem('username') + }); if (data && data.success) { toast.success('Assignment Created', { position: 'bottom-right', @@ -394,6 +402,8 @@ export default function Createchall(props) {
+ { + props.assignmentInfo && props.assignmentInfo.option !== "dynamicLab" && (

Challenge Solution @@ -408,6 +418,8 @@ export default function Createchall(props) { >

+ ) + }

diff --git a/src/components/groups/assignments/createAssignment.jsx b/src/components/groups/assignments/createAssignment.jsx index 1fed2f6e..32968ad7 100644 --- a/src/components/groups/assignments/createAssignment.jsx +++ b/src/components/groups/assignments/createAssignment.jsx @@ -26,6 +26,7 @@ export default function CreateGroup(props) { const [displayExistingChallenge, setDisplayExistingChallenge] = useState(false); const [displayCustomChallenge, setDisplayCustomChallenge] = useState(false); + const [displayDynamicLab, setDisplayDynamicLab] = useState(false); const [errMessage, setErrMessage] = useState([]); @@ -120,6 +121,8 @@ export default function CreateGroup(props) { setDisplayExistingChallenge(true); } else if (selectedOption === 'customChallenge') { setDisplayCustomChallenge(true); + } else if(selectedOption === 'dynamicLab') { + setDisplayDynamicLab(true); } else { setErrMessage('Please select an assignment type'); } @@ -137,6 +140,7 @@ export default function CreateGroup(props) { assignmentPoints, selectedCategory, latePenalty: parseInt(latePenalty), + option: selectedOption }} setDisplay={setDisplayExistingChallenge} /> @@ -153,10 +157,28 @@ export default function CreateGroup(props) { assignmentPoints, selectedCategory, latePenalty: parseInt(latePenalty), + option: selectedOption }} setDisplay={setDisplayCustomChallenge} /> ); + } else if(displayDynamicLab) { + return ( + + ); } return ( @@ -408,12 +430,16 @@ export default function CreateGroup(props) {

setSelectedOption('dynamicLab')} + className={`cursor-pointer bg-neutral-800 px-2 py-2 text-center ${ + selectedOption === 'dynamicLab' + ? 'border-2 border-blue-600' + : 'border-2 border-neutral-800 hover:border-neutral-900 hover:bg-neutral-900' + }`} + onClick={() => setSelectedOption('dynamicLab')} >

- Dynamic Lab (Coming Soon) + Dynamic Lab

Create a simulated Cybersecurity environent graded by AI{' '} diff --git a/src/pages/assignments/student/[code].jsx b/src/pages/assignments/student/[code].jsx index 1139192e..5f06d29e 100644 --- a/src/pages/assignments/student/[code].jsx +++ b/src/pages/assignments/student/[code].jsx @@ -52,9 +52,11 @@ export default function Slug() { const [terminalPopup, setTerminalPopup] = useState(false); const [loadingMessage, setLoadingMessage] = useState(''); const [isStudent, setIsStudent] = useState(true); - const [submissionId, setSubmissionId] = useState(null); + const [isDynamicLab, setIsDynmaicLab] = useState(false); + const [loadingLabStatus, setLoadingLabStatus] = useState(true); + function copy(tags) { var copyText = document.getElementById(tags); copyText.type = 'text'; @@ -106,7 +108,6 @@ export default function Slug() { console.log(err); } }; - const getAssignment = async () => { const params = window.location.href.split('/'); @@ -122,6 +123,7 @@ export default function Slug() { setSubmissionId(data.submissionId); setChallengeHints(data.body.challenge.hints); await getChallenge(data.body); + await checkIfDynamicLab(data.body.challenge.id); } else { console.log('You are not apart of this class'); router.push('/groups'); @@ -216,11 +218,24 @@ export default function Slug() { } }, []); + const checkIfDynamicLab = async (challengeId) => { + const url = `${baseUrl}/classroom-assignments/check-flag`; + const body = { flag: "", challengeId: challengeId}; + const response = await request(url, "POST", body); + if(response && response.success) { + if(response.body.solved) { + setIsDynmaicLab(true); + } + } + setLoadingLabStatus(false); + }; + const checkFlag = async () => { const url = `${baseUrl}/classroom-assignments/check-flag`; const body = { flag: flagInput, challengeId: challenge.id}; setLoading(true); const response = await request(url, "POST", body); + console.log(response); setLoading(false); if(response && response.success) { if(response.body.solved) { @@ -275,7 +290,7 @@ export default function Slug() { const url = `${baseUrl}/submission/create/${assignment.classroom.classCode}`; const body = { - solved: solved, + solved: isDynamicLab ? true : solved, classroomId: assignment.classroomId, assignmentId: params[5], keyword: flagInput, @@ -356,6 +371,7 @@ export default function Slug() { function handleDataAsk() { socketRef.current.emit('data_ask', { whoami: password }); } + console.log(isDynamicLab); return ( <> @@ -433,12 +449,15 @@ export default function Slug() { content={challenge && challenge.content} /> -

- Submission Area - + { + !loadingLabStatus && !isDynamicLab ? 'Submission Area' : 'Dynamic Lab' + }

+ { + !loadingLabStatus && !isDynamicLab && ( +

FLAG SUBMISSION


@@ -451,7 +470,7 @@ export default function Slug() {
+ ) + }

HINTS


diff --git a/src/pages/assignments/teacher/[code].jsx b/src/pages/assignments/teacher/[code].jsx index 0e564029..ac1a20f6 100644 --- a/src/pages/assignments/teacher/[code].jsx +++ b/src/pages/assignments/teacher/[code].jsx @@ -73,6 +73,9 @@ export default function id() { const [code, setCode] = useState(0); const [loadingMessage, setLoadingMessage] = useState(''); + const [isDynamicLab, setIsDynmaicLab] = useState(false); + const [loadingLabStatus, setLoadingLabStatus] = useState(true); + const router = useRouter(); const parseDate = (dateString) => { @@ -108,6 +111,7 @@ export default function id() { setAssignment(data.body); getChallenge(data.body); await getSubmissions(data.body); + await checkIfDynamicLab(data.body.challenge.id); } else { console.log('You are not apart of this class'); router.push('/groups'); @@ -264,6 +268,18 @@ export default function id() { router.replace(`/assignments/${assignment.id}/submissions/${id}?key=t`); }; + const checkIfDynamicLab = async (challengeId) => { + const url = `${baseUrl}/classroom-assignments/check-flag`; + const body = { flag: "", challengeId: challengeId}; + const response = await request(url, "POST", body); + if(response && response.success) { + if(response.body.solved) { + setIsDynmaicLab(true); + } + } + setLoadingLabStatus(false); + }; + const checkIfTerminalExists = async () => { setFetchingTerminal(true); const token = auth.currentUser.accessToken; @@ -460,6 +476,9 @@ export default function id() {
+ { + !isDynamicLab && !loadingLabStatus && ( +

FLAG SUBMISSION


@@ -500,6 +519,10 @@ export default function id() { ) )} +
+ ) + } +

HINTS


{hints.map((hint, idx) => { diff --git a/src/utils/request.js b/src/utils/request.js index a431609e..59404822 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -1,7 +1,6 @@ const request = async (url, req_method, body) => { try { - //const token = getCookie(); let method = req_method.toUpperCase(); if(method === 'GET' || method === "DELETE") { @@ -25,6 +24,13 @@ const request = async (url, req_method, body) => { } const response = await fetch(url, requestOptions); const data = await response.json(); + + if(response.status === 401 && data.error) { + console.log("Unauthorized"); + window.location.href = "/login"; + return null; + } + return data; } else { return null;