Skip to content

Commit 052803e

Browse files
committed
feat: handle solution inside exercises
1 parent 82a0a23 commit 052803e

File tree

5 files changed

+126
-0
lines changed

5 files changed

+126
-0
lines changed

gatsby/lib/exercise-solution.mjs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { visit } from 'unist-util-visit';
2+
3+
export default function rehypeExerciseSolution() {
4+
return (tree) => {
5+
visit(tree, { tagName: 'div' }, (node) => {
6+
if (node.properties.dataType !== 'exercise') {
7+
return;
8+
}
9+
10+
let hasSolution = false;
11+
let p5EditorUrl = null;
12+
13+
visit(node, { tagName: 'div' }, (solutionNode, _index, _parent) => {
14+
// Look for solution node with a class of 'solution'
15+
if (
16+
solutionNode.properties?.className &&
17+
solutionNode.properties.className.includes('solution')
18+
) {
19+
hasSolution = true;
20+
21+
visit(solutionNode, { tagName: 'embed-example' }, (embedNode) => {
22+
if (embedNode.properties?.dataP5Editor) {
23+
p5EditorUrl = embedNode.properties.dataP5Editor;
24+
// add a class to the solution node to indicate that it is a p5 solution
25+
solutionNode.properties.className.push('p5-solution');
26+
27+
return false;
28+
}
29+
});
30+
}
31+
});
32+
33+
// If we found a solution, render it differently
34+
if (hasSolution) {
35+
node.tagName = 'exercise-with-solution';
36+
37+
// If we found a p5 editor URL, add it to the node's properties
38+
if (p5EditorUrl) {
39+
node.properties.p5EditorUrl = p5EditorUrl;
40+
}
41+
}
42+
});
43+
};
44+
}

gatsby/lib/parse-content.mjs

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { rehypeCodesplit } from './codesplit.mjs';
1313
import { preserveCustomSpans, restoreCustomSpans } from './blank-span.mjs';
1414
import { rehypeVideoLink } from './video-link.mjs';
1515
import rehypeReplaceBrWithSpace from './br-space.mjs';
16+
import rehypeExerciseSolution from './exercise-solution.mjs';
1617

1718
export function parseContent(html) {
1819
const replaceMedia = () => (tree) => {
@@ -191,6 +192,7 @@ export function parseContent(html) {
191192
})
192193
.use(replaceMedia)
193194
.use(rehypeVideoLink)
195+
.use(rehypeExerciseSolution)
194196
.use(externalLinkInNewTab)
195197
.use(rehypeReplaceBrWithSpace)
196198
.use(rehypeCodesplit)
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import { HiOutlineEye, HiOutlineEyeOff } from 'react-icons/hi';
3+
import { HiOutlineLink } from 'react-icons/hi';
4+
5+
const ExerciseWithSolution = (props) => {
6+
const childrenWithoutP5Solution = props.children.filter(
7+
(child) => !child.props?.className?.includes('p5-solution'),
8+
);
9+
const solutions = childrenWithoutP5Solution.filter((child) => {
10+
return child.props?.className?.includes('solution');
11+
});
12+
13+
const [isAnswerVisible, setIsAnswerVisible] = React.useState(false);
14+
15+
const toggleAnswerHiddenStatus = () => {
16+
setIsAnswerVisible((lastState) => !lastState);
17+
};
18+
19+
return (
20+
<div
21+
className={`callout relative ${isAnswerVisible ? 'is-answer-visible' : ''}`}
22+
>
23+
{/* bottons group on the top right */}
24+
<div className="absolute right-0 top-0 hidden sm:flex">
25+
{solutions.length && (
26+
<button
27+
className="flex items-center rounded px-2.5 py-1.5 text-xs font-semibold hover:bg-gray-300"
28+
onClick={toggleAnswerHiddenStatus}
29+
>
30+
{isAnswerVisible ? (
31+
<>
32+
<HiOutlineEyeOff className="h-4 w-4" />
33+
<span className="ml-1">Hide Answer</span>
34+
</>
35+
) : (
36+
<>
37+
<HiOutlineEye className="h-4 w-4" />
38+
<span className="ml-1">Reveal Answer</span>
39+
</>
40+
)}
41+
</button>
42+
)}
43+
44+
{props.p5EditorUrl && (
45+
<a
46+
className="flex items-center rounded px-2.5 py-1.5 text-xs font-semibold text-gray-700 no-underline hover:bg-gray-300"
47+
target="_blank"
48+
rel="noopener noreferrer"
49+
href={props.p5EditorUrl}
50+
>
51+
<HiOutlineLink className="h-4 w-4" />
52+
<span className="ml-1">Suggested Answer</span>
53+
</a>
54+
)}
55+
</div>
56+
57+
{childrenWithoutP5Solution}
58+
</div>
59+
);
60+
};
61+
62+
export default ExerciseWithSolution;

src/layouts/ChapterLayout.js

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Image from '../components/Image';
1010
import Example from '../components/Example';
1111
import VideoLink from '../components/VideoLink';
1212
import Codesplit from '../components/Codesplit';
13+
import ExerciseWithSolution from '../components/ExerciseWithSolution';
1314

1415
const renderAst = ({ ast, images }) => {
1516
visit(ast, { tagName: 'img' }, (node) => {
@@ -33,6 +34,7 @@ const renderAst = ({ ast, images }) => {
3334
'embed-example': Example,
3435
'video-link': VideoLink,
3536
'codesplit': Codesplit,
37+
'exercise-with-solution': ExerciseWithSolution,
3638
},
3739
});
3840

src/styles/callout.css

+16
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@
2323
@apply my-0;
2424
}
2525

26+
.callout > .solution {
27+
@apply my-4 rounded bg-white px-6 py-4 text-transparent;
28+
29+
:first-child {
30+
@apply mt-0;
31+
}
32+
33+
:last-child {
34+
@apply mb-0;
35+
}
36+
}
37+
38+
.callout.is-answer-visible > .solution {
39+
@apply text-inherit;
40+
}
41+
2642
.highlight {
2743
@apply block text-center text-lg font-semibold;
2844
}

0 commit comments

Comments
 (0)