Skip to content

Commit

Permalink
Merge pull request #379 from codekids-vt/loops-book-components
Browse files Browse the repository at this point in the history
Loops book components
  • Loading branch information
scout517 committed Sep 8, 2024
2 parents a1dda75 + 482919e commit 430ee0a
Show file tree
Hide file tree
Showing 12 changed files with 902 additions and 64 deletions.
392 changes: 382 additions & 10 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"prettier": "^3.2.5",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-code-blocks": "^0.1.6",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-error-boundary": "^4.0.13",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/BookImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { ClothingActivity } from "./ClothingActivity";
import BookRushHour from "./BookRushHour";
import { ImageQuestion } from "./ImageQuestion";
import { DragMultiChoice } from "./DragMultiChoice";
import CodeStepFlowchart from "./CodeStepFlowchart";
import FillInTheBlank from "./FillInTheBlank";

export function BookImage({
image,
Expand Down Expand Up @@ -141,6 +143,12 @@ export function BookImage({
{image === "DragMultiChoice" && (
<DragMultiChoice props={page?.props} setAllowNext={setAllowNext} />
)}
{image === "CodeStepFlowchart" && (
<CodeStepFlowchart props={page?.props} setAllowNext={setAllowNext} />
)}
{image === "FillInTheBlank" && (
<FillInTheBlank props={page?.props} setAllowNext={setAllowNext} />
)}
</div>
);
}
193 changes: 139 additions & 54 deletions frontend/src/components/CodeStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import React, { useState } from "react";
import { CodeBlock } from "react-code-blocks";

/*
Code is delimited by \n
Expand All @@ -11,11 +12,27 @@ export interface ICodeStepProps {
code: string;
skipLines: number[];
enableNext: boolean;
getLine: () => {};
getLine: (lineNumber: number) => void;
}

export function CodeStep({ props }: { props: any | ICodeStepProps }) {
export interface ILoop {
exists: boolean;
lines?: number[];
times?: number;
getIteration?: (iterationNumber: number) => void;
}

export function CodeStep({
props,
loop,
useCodeBlock,
}: {
props: ICodeStepProps;
loop: ILoop;
useCodeBlock?: boolean;
}) {
const [currentLine, setCurrentLine] = useState(0);
const [loopIteration, setLoopIteration] = useState(0);

const code: string[] = props.code.split("\n");

Expand All @@ -24,47 +41,140 @@ export function CodeStep({ props }: { props: any | ICodeStepProps }) {
return "none";
}

function getButtonStyle() {
if (props.enableNext) return button_style;
return disabled_button_style;
function nextButton() {
if (!loop.exists) {
if (currentLine === code.length - 1) return;
var nextLine = currentLine + 1;
while (props.skipLines.includes(nextLine) && nextLine !== code.length - 1)
nextLine++;
setCurrentLine(nextLine);
props.getLine(nextLine);
} else {
nextLoop();
}
}

function nextButton() {
if (currentLine === code.length - 1) return;
var nextLine = currentLine + 1;
while (props.skipLines.includes(nextLine) && nextLine !== code.length - 1)
nextLine++;
setCurrentLine(nextLine);
props.getLine(nextLine);
function nextLoop() {
if (
loop.lines !== undefined &&
loop.times !== undefined &&
loop.getIteration !== undefined
) {
var nextLine = currentLine + 1;
if (
!loop.lines.includes(nextLine) &&
currentLine !== loop.lines[loop.lines.length - 1]
) {
// nextLine is not within loop
if (currentLine === code.length - 1) return;
while (
props.skipLines.includes(nextLine) &&
nextLine !== code.length - 1
)
nextLine++;
} else {
// nextLine is within loop
if (currentLine === loop.lines[0] && loopIteration === loop.times) {
// Loop executed loop.times
nextLine = loop.lines[loop.lines.length - 1] + 1;
while (
props.skipLines.includes(nextLine) &&
nextLine !== code.length - 1
)
nextLine++;
} else if (nextLine === loop.lines[1]) {
// Next line is the first line within the loop
setLoopIteration(loopIteration + 1);
loop.getIteration(loopIteration + 1);
} else if (currentLine === loop.lines[loop.lines.length - 1]) {
// Current line is end of loop
nextLine = loop.lines[0];
}
}
setCurrentLine(nextLine);
props.getLine(nextLine);
}
}

function backButton() {
if (currentLine === 0) return;
var nextLine = currentLine - 1;
while (props.skipLines.includes(nextLine) && nextLine !== 0) nextLine--;
setCurrentLine(nextLine);
props.getLine(nextLine);
if (!loop.exists) {
if (currentLine === 0) return;
var nextLine = currentLine - 1;
while (props.skipLines.includes(nextLine) && nextLine !== 0) nextLine--;
setCurrentLine(nextLine);
props.getLine(nextLine);
} else {
backLoop();
}
}

function backLoop() {
if (
loop.lines !== undefined &&
loop.times !== undefined &&
loop.getIteration !== undefined
) {
var nextLine = currentLine - 1;
if (!loop.lines.includes(nextLine) && currentLine !== loop.lines[0]) {
// nextLine is not within loop
if (currentLine === 0) return;
while (props.skipLines.includes(nextLine) && nextLine !== 0) nextLine--;

if (nextLine === loop.lines[loop.lines.length - 1]) {
nextLine = loop.lines[0];
}
} else {
if (currentLine === loop.lines[0] && loopIteration > 0) {
nextLine = loop.lines[loop.lines.length - 1];
} else if (currentLine === loop.lines[0]) {
if (currentLine === 0) return;
while (props.skipLines.includes(nextLine) && nextLine !== 0)
nextLine--;
} else if (nextLine === loop.lines[0]) {
setLoopIteration(loopIteration - 1);
loop.getIteration(loopIteration - 1);
}
}
setCurrentLine(nextLine);
props.getLine(nextLine);
}
}

return (
<React.Fragment>
<div className=" flex flex-col ml-auto mr-auto px-10 py-5 w-fit bg-zinc-200 ">
{code.map((line, index) => (
<div
className="px-5 whitespace-pre-wrap text-left"
key={index}
style={{ background: checkCurrentLine(index, currentLine) }}
>
{line}
</div>
))}
<div className=" flex flex-col ml-auto mr-auto px-10 py-5 w-fit bg-zinc-200 text-start">
{useCodeBlock ? (
<CodeBlock
text={props.code}
language="python"
highlight={
currentLine === 0
? ""
: `${code[0].includes("#") ? "1," : ""}${(currentLine + 1).toString()}`
}
/>
) : (
code.map((line, index) => (
<div
className="px-5 whitespace-pre-wrap text-left"
key={line + index}
style={{ background: checkCurrentLine(index, currentLine) }}
>
{line}
</div>
))
)}
</div>
<div className="inline-flex mt-5 rounded-md gap-10 justify-center">
<button style={button_style} type="button" onClick={() => backButton()}>
<button
className="bg-gray-300 text-xl border-black border-2 rounded-3xl px-5 py-2 m-1 whitespace-pre-wrap"
type="button"
onClick={() => backButton()}
>
Back
</button>
<button
style={getButtonStyle()}
className={`${!props.enableNext ? "opacity-50" : ""} bg-gray-300 text-xl border-black border-2 rounded-3xl px-5 m-1 whitespace-pre-wrap`}
disabled={!props.enableNext}
type="button"
onClick={() => nextButton()}
Expand All @@ -75,28 +185,3 @@ export function CodeStep({ props }: { props: any | ICodeStepProps }) {
</React.Fragment>
);
}

const button_style = {
backgroundColor: "#D1D5DB",
color: "black",
fontSize: "20px",
border: "1px solid grey",
borderRadius: "30px",
padding: "5px 20px",
margin: ".5%",
cursor: "pointer",
whiteSpace: "pre-wrap" as "pre-wrap",
};

const disabled_button_style = {
backgroundColor: "#D1D5DB",
color: "black",
fontSize: "20px",
border: "1px solid grey",
borderRadius: "30px",
padding: "5px 20px",
margin: ".5%",
cursor: "pointer",
whiteSpace: "pre-wrap" as "pre-wrap",
opacity: "50%",
};
109 changes: 109 additions & 0 deletions frontend/src/components/CodeStepFlowchart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { Dispatch, SetStateAction, useState } from "react";
import { CodeStep } from "./CodeStep";
import { Reader } from "./Reader";

function CodeStepFlowchart({
props,
setAllowNext,
}: {
props: any;
setAllowNext: Dispatch<SetStateAction<boolean>>;
}) {
const [currentLine, setCurrentLine] = useState(0);
const [currentImage, setCurrentImage] = useState("");
const [loopIteration, setLoopIteration] = useState("");
const [walkthroughText, setWalkthroughText] = useState("");
const [iteratorVariable, setIteratorVariable] = useState(0);
const [prevLine, setPrevLine] = useState(0);

const startLine = Math.min(...props.loop.lines);
const endLine = Math.max(...props.loop.lines);

const numOfLines = props.code.split("\n").length - 1;

React.useEffect(() => {
if (
currentLine === startLine &&
loopIteration !== "" &&
prevLine > currentLine
) {
setIteratorVariable(parseInt(loopIteration) + 1);
} else if (currentLine === endLine && prevLine === startLine) {
setIteratorVariable(iteratorVariable - 1);
}
}, [
loopIteration,
currentLine,
endLine,
iteratorVariable,
prevLine,
startLine,
setAllowNext,
]);

React.useEffect(() => {
if (props.flowChart[currentLine] === undefined) return;

var { image, text } = props.flowChart[currentLine];

// f is indication of first iteration
if (`${currentLine}f` in props.flowChart && loopIteration === "") {
image = props.flowChart[`${currentLine}f`].image;
text = props.flowChart[`${currentLine}f`].text;
}

setCurrentImage(image);
setWalkthroughText(text);

setAllowNext(currentLine === numOfLines);
}, [currentLine, loopIteration, numOfLines, props.flowChart, setAllowNext]);

const getLine = (lineNumber: number) => {
setPrevLine(currentLine);
setCurrentLine(lineNumber);
};

const getIteration = (iteration: number) => {
if (iteration - 1 === -1) {
setLoopIteration("");
return;
}
setLoopIteration((iteration - 1).toString());
};

return (
<div className="w-full h-full flex flex-col items-center justify-center">
<div className="flex gap-5 w-full">
<div className="flex flex-col w-1/2 justify-center">
<CodeStep
props={{
code: props.code.replace(
"%N",
props.loop.display === "iteration"
? loopIteration
: iteratorVariable,
),
skipLines: props.skipLines,
enableNext: true,
getLine: getLine,
}}
loop={{
exists: props.loop.exists,
lines: props.loop.lines,
times: props.loop.iterations,
getIteration: getIteration,
}}
/>
</div>
<div className="flex w-1/2 justify-center">
{currentImage !== "" && (
<img src={currentImage} alt="Current flowchart." />
)}
</div>
</div>
<Reader text={walkthroughText} />
</div>
);
}

export default CodeStepFlowchart;
Loading

0 comments on commit 430ee0a

Please sign in to comment.