diff --git a/client/src/App.js b/client/src/App.js index 336fa9eb..bf6c4c0f 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -7,6 +7,7 @@ import About from "./views/About/About" import Workspace from "./views/Workspace/Workspace" import Dashboard from "./views/Dashboard/Dashboard" import Student from "./views/Student/Student" +import StudentDetail from './views/StudentDetail/StudentDetail' import NotFound from "./views/NotFound" import StudentLogin from "./views/StudentLogin/StudentLogin"; import Sandbox from "./views/Sandbox/Sandbox" @@ -16,6 +17,7 @@ import TeacherLogin from "./views/TeacherLogin/TeacherLogin" import ContentCreator from './views/ContentCreator/ContentCreator' import UnitCreator from './views/ContentCreator/UnitCreator/UnitCreator' import UploadBlocks from './views/UploadBlocks/UploadBlocks' +import Replay from './components/Replay/Replay'; const App = () => { let history = useHistory(); @@ -23,20 +25,41 @@ const App = () => { return (
- }/> - }/> - }/> - }/> + + + + + + + + + + + + }/> } /> - }/> - } /> + + + + + + } /> } /> - }/> - }/> - }/> - + } /> + + + + + + + + + + + +
diff --git a/client/src/Utils/requests.js b/client/src/Utils/requests.js index 8763500a..bc6d3db6 100644 --- a/client/src/Utils/requests.js +++ b/client/src/Utils/requests.js @@ -212,13 +212,14 @@ export const setSelection = async (classroom, learningStandard) => ( }) ); -export const saveWorkspace = async (day, workspace) => ( +export const saveWorkspace = async (day, workspace, replay) => ( makeRequest({ method: POST, path: `${server}/saves`, data: { - day: day, - workspace: workspace + day, + workspace, + replay }, auth: true, error: 'Failed to save your workspace.' @@ -234,6 +235,15 @@ export const getSaves = async (day) => ( }) ); +export const getAllSaves = async () => ( + makeRequest({ + method: GET, + path: `${server}/saves`, + auth: true, + error: 'Past saves could not be retrieved.' + }) +); + export const createSubmission = async (day, workspace, sketch, path, isAuth) => ( makeRequest({ method: POST, diff --git a/client/src/assets/style.less b/client/src/assets/style.less index 2bb22992..e56b4ba2 100644 --- a/client/src/assets/style.less +++ b/client/src/assets/style.less @@ -64,4 +64,12 @@ .nav-padding { padding-top: 8vh; +} + +.replayButton { + cursor: pointer; +} + +.bold { + font-weight: bold; } \ No newline at end of file diff --git a/client/src/components/DayPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.js b/client/src/components/DayPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.js index 3547bea5..795ccf13 100644 --- a/client/src/components/DayPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.js +++ b/client/src/components/DayPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.js @@ -20,12 +20,15 @@ export default function BlocklyCanvasPanel(props) { const workspaceRef = useRef(null); const dayRef = useRef(null); + const replayRef = useRef([]); + const undoLength = useRef(0); const { SubMenu } = Menu; - const setWorkspace = () => + const setWorkspace = () => { workspaceRef.current = window.Blockly.inject('blockly-canvas', { toolbox: document.getElementById('toolbox') } ); + } const loadSave = selectedSave => { try { @@ -64,18 +67,31 @@ export default function BlocklyCanvasPanel(props) { // automatically save workspace every min setInterval(async () => { if (isStudent && workspaceRef.current && dayRef.current) { - const res = await handleSave(dayRef.current.id, workspaceRef); + const res = await handleSave(dayRef.current.id, workspaceRef, replayRef.current); if (res.data) { setLastAutoSave(res.data[0]); setLastSavedTime(getFormattedDate(res.data[0].updated_at)) } } }, 60000); + setInterval(async () => { + if (workspaceRef.current.undoStack_.length !== undoLength.current) { + undoLength.current = workspaceRef.current.undoStack_.length; + let xml = window.Blockly.Xml.workspaceToDom(workspaceRef.current); + let xml_text = window.Blockly.Xml.domToText(xml); + const replay = { + xml: xml_text, + timestamp: Date.now() + } + replayRef.current.push(replay); + console.log(replayRef.current); + } + }, 1000); // clean up - saves workspace and removes blockly div from DOM return async () => { if (isStudent && dayRef.current && workspaceRef.current) - await handleSave(dayRef.current.id, workspaceRef); + await handleSave(dayRef.current.id, workspaceRef, replayRef.current); if (workspaceRef.current) workspaceRef.current.dispose(); dayRef.current = null } @@ -102,6 +118,7 @@ export default function BlocklyCanvasPanel(props) { if (onLoadSave) { let xml = window.Blockly.Xml.textToDom(onLoadSave.workspace); window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current); + replayRef.current = onLoadSave.replay; setLastSavedTime(getFormattedDate(onLoadSave.updated_at)); } else if (day.template) { let xml = window.Blockly.Xml.textToDom(day.template); @@ -116,7 +133,7 @@ export default function BlocklyCanvasPanel(props) { const handleManualSave = async () => { // save workspace then update load save options - const res = await handleSave(day.id, workspaceRef); + const res = await handleSave(day.id, workspaceRef, replayRef.current); if (res.err) { message.error(res.err) } else { diff --git a/client/src/components/DayPanels/helpers.js b/client/src/components/DayPanels/helpers.js index 4fb56d4a..c7bedfa8 100644 --- a/client/src/components/DayPanels/helpers.js +++ b/client/src/components/DayPanels/helpers.js @@ -100,10 +100,10 @@ const flashArduino = async (response) => { } // save current workspace -export const handleSave = async (dayId, workspaceRef) => { +export const handleSave = async (dayId, workspaceRef, replay) => { let xml = window.Blockly.Xml.workspaceToDom(workspaceRef.current); let xml_text = window.Blockly.Xml.domToText(xml); - return await saveWorkspace(dayId, xml_text); + return await saveWorkspace(dayId, xml_text, replay); }; export const handleCreatorSaveDay = async (dayId, workspaceRef, blocksList) => { diff --git a/client/src/components/Replay/Replay.js b/client/src/components/Replay/Replay.js new file mode 100644 index 00000000..ee527f36 --- /dev/null +++ b/client/src/components/Replay/Replay.js @@ -0,0 +1,109 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Link } from 'react-router-dom' +import NavBar from '../NavBar/NavBar'; + +const Replay = () => { + const workspaceRef = useRef(null); + const [step, setStep] = useState(0); + let playback; + const setWorkspace = () => { + workspaceRef.current = window.Blockly.inject('blockly-canvas', + { toolbox: document.getElementById('toolbox') } + ); + } + const replay = [ + { + "xml": "WHILEitem", + "timestamp": 1618859201398 + }, + { + "xml": "WHILEitem", + "timestamp": 1618859202401 + }, + { + "xml": "WHILEitemAND", + "timestamp": 1618859203397 + }, + { + "xml": "WHILEitemAND", + "timestamp": 1618859204397 + }, + { + "xml": "WHILEitemROOTAND", + "timestamp": 1618859206397 + }, + { + "xml": "WHILEitemROOTAND", + "timestamp": 1618859209397 + }, + { + "xml": "WHILEitemWHILEROOTAND", + "timestamp": 1618859211400 + }, + { + "xml": "WHILEitemROOTANDWHILE", + "timestamp": 1618859214404 + }, + { + "xml": "WHILEitemROOTAND", + "timestamp": 1618859216400 + } + ]; + useEffect(() => { + workspaceRef.current ? workspaceRef.current.clear() : setWorkspace(); + const xml = window.Blockly.Xml.textToDom(replay[step].xml); + window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current); + }, [step]); + + const goBack = () => { + setStep(step - 1); + } + // const play = () => { + // playback = setInterval(() => { + // console.log('firing'); + // setStep(step + 1); + // console.log(step); + // }, 1000); + // } + const goForward = () => { + setStep(step + 1); + } + return ( +
+ +
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+

Code Replay

+
+
+
+
+
+

Logs

+
+ { replay.map((item, index) =>

{item.timestamp}

)} +
+
+
+
+ +
+ ) +}; + +export default Replay; \ No newline at end of file diff --git a/client/src/views/Classroom/Classroom.js b/client/src/views/Classroom/Classroom.js index 4b177546..cab62665 100644 --- a/client/src/views/Classroom/Classroom.js +++ b/client/src/views/Classroom/Classroom.js @@ -1,4 +1,5 @@ import React, {useEffect, useState} from "react" +import {Link} from 'react-router-dom'; import {message, Tabs} from 'antd'; import "./Classroom.less" @@ -27,7 +28,7 @@ export default function Classroom(props) { } }); }, [classroomId]); - + console.log(classroom); return (
@@ -47,6 +48,10 @@ export default function Classroom(props) { +

Student List

+
    + {classroom.students?.map(student =>
  • {student.name}
  • )} +
); diff --git a/client/src/views/ContentCreator/ContentCreator.js b/client/src/views/ContentCreator/ContentCreator.js index ab265cfe..17b88930 100644 --- a/client/src/views/ContentCreator/ContentCreator.js +++ b/client/src/views/ContentCreator/ContentCreator.js @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; - +import {useHistory} from 'react-router-dom'; import { Tabs, Table, Popconfirm, message } from 'antd' import Navbar from '../../components/NavBar/NavBar' import { QuestionCircleOutlined } from '@ant-design/icons'; @@ -12,11 +12,12 @@ import UnitEditor from './UnitEditor/UnitEditor' const { TabPane } = Tabs; -export default function ContentCreator(props) { +export default function ContentCreator() { const [dataSource, setDataSource] = useState([]) const [dataSourceMap, setDataSourceMap] = useState([]) const [gradeMenu, setGradeMenu] = useState([]) + const history = useHistory(); useEffect(() => { //console.log(getLearningStandardcount()) @@ -44,7 +45,7 @@ export default function ContentCreator(props) { width: '22.5%', align: 'left', render: (_, key) => ( - + ) }, { @@ -83,7 +84,7 @@ export default function ContentCreator(props) { // width: '10%', // align: 'right', // render: (_, key) => ( - // + // // ) // }, // { @@ -93,7 +94,7 @@ export default function ContentCreator(props) { // width: '10%', // align: 'right', // render: (_, key) => ( - // + // // ) // }, // { diff --git a/client/src/views/ContentCreator/LearningStandardDayCreator/DayEditor.js b/client/src/views/ContentCreator/LearningStandardDayCreator/DayEditor.js index 81d45de9..55b98aaa 100644 --- a/client/src/views/ContentCreator/LearningStandardDayCreator/DayEditor.js +++ b/client/src/views/ContentCreator/LearningStandardDayCreator/DayEditor.js @@ -1,4 +1,5 @@ import React, { useState } from 'react' +import {useHistory} from 'react-router-dom' import { Button, List, Card, Modal, Form, Input } from 'antd' import { createDay, deleteDay, getDayToolboxAll, getLearningStandard } from '../../../Utils/requests' @@ -13,13 +14,12 @@ export default function ContentCreator(props) { const [newDay, setNewDay] = useState(); const learningStandardId = props.learningStandardId const learningStandardName = props.learningStandardName - + const history = useHistory(); const handleCancel = () => { setVisible(false) }; - const showModal = () => { console.log("got days", props.days) setDay([...props.days]) @@ -27,7 +27,6 @@ export default function ContentCreator(props) { setVisible(true) }; - const addBasicDay = () => { const res = createDay(newDay, learningStandardId) res.then(function (a) { @@ -60,7 +59,7 @@ export default function ContentCreator(props) { day.toolbox = res.data.toolbox; localStorage.setItem("my-day", JSON.stringify(day)); - props.history.push('/day') + history.push('/day') }; //figure out how to set these up in the css file colors[] stuff causes problems diff --git a/client/src/views/ContentCreator/viewDayModal/viewDayModal.js b/client/src/views/ContentCreator/viewDayModal/viewDayModal.js index 624cb268..ef72a27e 100644 --- a/client/src/views/ContentCreator/viewDayModal/viewDayModal.js +++ b/client/src/views/ContentCreator/viewDayModal/viewDayModal.js @@ -1,5 +1,5 @@ import React, { useState } from 'react' - +import {useHistory} from 'react-router-dom' import { Modal } from 'antd'; import {CloseOutlined} from '@ant-design/icons' @@ -26,7 +26,7 @@ export default function ViewDayModal(props) { day.toolbox = res.data.toolbox; localStorage.setItem("my-day", JSON.stringify(day)); - props.history.push('/day') + history.push('/day') }; return ( diff --git a/client/src/views/Home/Home.js b/client/src/views/Home/Home.js index 060ed186..1b31f952 100644 --- a/client/src/views/Home/Home.js +++ b/client/src/views/Home/Home.js @@ -4,14 +4,13 @@ import Logo from "../../assets/casmm_logo.png" import HomeJoin from "./HomeJoin" import NavBar from "../../components/NavBar/NavBar"; -export default function Home(props) { - +export default function Home() { return(
- +
) diff --git a/client/src/views/Home/HomeJoin.js b/client/src/views/Home/HomeJoin.js index a5d49e28..a65b27ef 100644 --- a/client/src/views/Home/HomeJoin.js +++ b/client/src/views/Home/HomeJoin.js @@ -2,10 +2,12 @@ import React, {useState} from 'react' import {message} from 'antd' import './Home.less' import { getStudents } from "../../Utils/requests"; +import { useHistory } from 'react-router-dom' -export default function HomeJoin(props) { +export default function HomeJoin() { const [loading, setLoading] = useState(false); const [joinCode, setJoinCode] = useState(''); + const history = useHistory(); const handleLogin = () => { setLoading(true); @@ -14,7 +16,7 @@ export default function HomeJoin(props) { if(res.data){ setLoading(false); localStorage.setItem('join-code', joinCode); - props.history.push('/login'); + history.push('/login'); } else { setLoading(false); message.error('Join failed. Please input a valid join code.'); diff --git a/client/src/views/Student/Student.js b/client/src/views/Student/Student.js index 01ec694a..10705f5d 100644 --- a/client/src/views/Student/Student.js +++ b/client/src/views/Student/Student.js @@ -3,11 +3,12 @@ import {getStudentClassroom} from "../../Utils/requests" import './Student.less' import {message} from "antd"; import NavBar from "../../components/NavBar/NavBar"; +import {useHistory} from 'react-router-dom'; function Student(props) { const [learningStandard, setLearningStandard] = useState({}); const [selectedDay, setSelectedDay] = useState({}); - + const history = useHistory(); useEffect(() => { const fetchData = async () => { try { @@ -28,7 +29,7 @@ function Student(props) { setSelectedDay(day); localStorage.setItem("my-day", JSON.stringify(day)); - props.history.push("/workspace") + history.push("/workspace") }; return ( diff --git a/client/src/views/StudentDetail/StudentDetail.js b/client/src/views/StudentDetail/StudentDetail.js new file mode 100644 index 00000000..1d7cfabc --- /dev/null +++ b/client/src/views/StudentDetail/StudentDetail.js @@ -0,0 +1,18 @@ +import React, {useState, useEffect} from 'react' +import { useParams } from 'react-router-dom'; +import { getSaves } from '../../Utils/requests'; + +const StudentDetail = () => { + const {classroomId, studentId} = useParams(); + const [studentSaves, setStudentSaves] = useState(); + useEffect(() => { + const getStudentSaves = async () => await getSaves(1); + setStudentSaves(getStudentSaves); + }, []) + console.log(studentSaves); + return ( +

Student Detail

+ ) +} + +export default StudentDetail; \ No newline at end of file diff --git a/client/src/views/StudentLogin/StudentLogin.js b/client/src/views/StudentLogin/StudentLogin.js index b26fa8b9..3160a069 100644 --- a/client/src/views/StudentLogin/StudentLogin.js +++ b/client/src/views/StudentLogin/StudentLogin.js @@ -6,15 +6,17 @@ import StudentLoginForm from "./StudentLoginForm"; import {setUserSession} from "../../Utils/AuthRequests"; import {message} from "antd"; import NavBar from "../../components/NavBar/NavBar"; +import { useHistory } from 'react-router-dom'; -export default function StudentLogin(props) { +export default function StudentLogin() { const [studentList, setStudentList] = useState([]); const [animalList, setAnimalList] = useState([]) const [studentIds, setStudentIds] = useState([null, null, null]); const [studentAnimals, setStudentAnimals] = useState(['', '', '']); const [numForms, setNumForms] = useState(2); const joinCode = localStorage.getItem('join-code'); + const history = useHistory(); useEffect(() => { getStudents(joinCode).then(res => { @@ -32,7 +34,7 @@ export default function StudentLogin(props) { const res = await postJoin(joinCode, ids); if (res.data) { setUserSession(res.data.jwt, JSON.stringify(res.data.students)); - props.history.push('/student') + history.push('/student') } else { message.error(res.err); } diff --git a/client/src/views/TeacherLogin/TeacherLogin.js b/client/src/views/TeacherLogin/TeacherLogin.js index 0aae2bd8..df4a0964 100644 --- a/client/src/views/TeacherLogin/TeacherLogin.js +++ b/client/src/views/TeacherLogin/TeacherLogin.js @@ -1,6 +1,7 @@ import {message} from "antd"; import NavBar from "../../components/NavBar/NavBar"; import React, {useState} from "react"; +import {useHistory} from 'react-router-dom'; import {postUser, setUserSession} from "../../Utils/AuthRequests"; import "./TeacherLogin.less" @@ -8,7 +9,7 @@ export default function TeacherLogin(props) { const email = useFormInput(''); const password = useFormInput(''); const [loading, setLoading] = useState(false); - + const history = useHistory(); const handleLogin = () => { setLoading(true); let body = {identifier: email.value, password: password.value}; @@ -17,9 +18,9 @@ export default function TeacherLogin(props) { setUserSession(response.data.jwt, JSON.stringify(response.data.user)); setLoading(false); if (response.data.user.role.name === "Content Creator") { - props.history.push('/ccdashboard'); + history.push('/ccdashboard'); } else { - props.history.push('/dashboard'); + history.push('/dashboard'); } diff --git a/client/src/views/Workspace/Workspace.js b/client/src/views/Workspace/Workspace.js index 0d2e2c58..265794d5 100644 --- a/client/src/views/Workspace/Workspace.js +++ b/client/src/views/Workspace/Workspace.js @@ -3,11 +3,12 @@ import {getDayToolbox} from "../../Utils/requests.js" import BlocklyCanvasPanel from "../../components/DayPanels/BlocklyCanvasPanel/BlocklyCanvasPanel"; import {message} from "antd"; import NavBar from "../../components/NavBar/NavBar"; - +import { useHistory } from 'react-router-dom'; export default function Workspace(props) { const [day, setDay] = useState({}); - const {handleLogout, history} = props; + const {handleLogout} = props; + const history = useHistory(); useEffect(() => { const localDay = JSON.parse(localStorage.getItem("my-day")); diff --git a/server/api/save/controllers/save.js b/server/api/save/controllers/save.js index 3bb13b0c..6a9962d1 100644 --- a/server/api/save/controllers/save.js +++ b/server/api/save/controllers/save.js @@ -32,14 +32,14 @@ module.exports = { // ensure the request has the right number of params const params = Object.keys(ctx.request.body).length - if (params !== 2) return ctx.badRequest( + if (params !== 3) return ctx.badRequest( 'Invalid number of params!', { id: 'Save.create.body.invalid', error: 'ValidationError' } ) // validate the request - // at somept validate the xml...could lead to bad things... - const { day, workspace } = ctx.request.body + // TODO: at somept validate the xml...could lead to bad things... + const { day, workspace, replay } = ctx.request.body if (!strapi.services.validator.isInt(day) || !workspace) return ctx.badRequest( 'A day and workspace must be provided!', { id: 'Save.create.body.invalid', error: 'ValidationError' } @@ -66,10 +66,10 @@ module.exports = { return await Promise.all(ids.map(id => { // save exists, update const saveId = studentSaves[id] - if (saveId) return strapi.services.save.update({id: saveId}, {workspace: workspace}) + if (saveId) return strapi.services.save.update({id: saveId}, {workspace, replay}) // else, create a new save - return strapi.services.save.create({ student: id, day, workspace, session }) + return strapi.services.save.create({ student: id, day, workspace, session, replay }) })) }, } diff --git a/server/api/save/models/save.settings.json b/server/api/save/models/save.settings.json index a5f3111c..89d16355 100644 --- a/server/api/save/models/save.settings.json +++ b/server/api/save/models/save.settings.json @@ -21,6 +21,9 @@ "workspace": { "type": "text", "required": true + }, + "replay": { + "type": "json" } } }