Skip to content

Commit

Permalink
Add an option to enable the bug reporter (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
willcrichton authored Aug 28, 2024
1 parent 99c4ea2 commit b3106ab
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 131 deletions.
7 changes: 7 additions & 0 deletions crates/mdbook-quiz/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ struct QuizConfig {
/// Path to a .dic file containing words to include in the spellcheck dictionary.
more_words: Option<PathBuf>,

/// If true (and telemetry is enabled) then allow users to report bugs in the frontend.
show_bug_reporter: Option<bool>,

dev_mode: bool,
}

Expand Down Expand Up @@ -170,6 +173,9 @@ impl QuizPreprocessor {
if let Some(lang) = &self.config.default_language {
add_data("quiz-default-language", lang)?;
}
if let Some(true) = self.config.show_bug_reporter {
add_data("quiz-show-bug-reporter", "")?;
}

html.push_str("></div>");

Expand Down Expand Up @@ -199,6 +205,7 @@ impl SimplePreprocessor for QuizPreprocessor {
.get("more-words")
.map(|value| value.as_str().unwrap().into()),
spellcheck: parse_bool("spellcheck"),
show_bug_reporter: parse_bool("show-bug-reporter"),
dev_mode,
};

Expand Down
3 changes: 2 additions & 1 deletion example/mdbook/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ src = "src"
title = "example"

[preprocessor.quiz]
fullscreen = true
fullscreen = true
show-bug-reporter = true
2 changes: 2 additions & 0 deletions js/packages/quiz-embed/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ let initQuizzes = () => {
let root = ReactDOM.createRoot(el);
let fullscreen = divEl.dataset.quizFullscreen !== undefined;
let cacheAnswers = divEl.dataset.quizCacheAnswers !== undefined;
let showBugReporter = divEl.dataset.quizShowBugReporter !== undefined;
root.render(
<ErrorBoundary FallbackComponent={onError}>
<QuizView
name={name}
quiz={quiz}
fullscreen={fullscreen}
cacheAnswers={cacheAnswers}
showBugReporter={showBugReporter}
allowRetry
/>
</ErrorBoundary>
Expand Down
5 changes: 4 additions & 1 deletion js/packages/quiz/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
".": {
"default": "./dist/lib.js"
},
"./*": {
"./*.js": {
"default": "./dist/*.js"
},
"./*.scss": {
"default": "./dist/*.scss"
}
},
"type": "module",
Expand Down
131 changes: 70 additions & 61 deletions js/packages/quiz/src/components/quiz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { action, toJS } from "mobx";
import { observer, useLocalObservable } from "mobx-react";
import hash from "object-hash";
import React, {
useContext,
useEffect,
useLayoutEffect,
useMemo,
Expand Down Expand Up @@ -152,55 +153,54 @@ let loadState = ({
};

interface HeaderProps {
quiz: Quiz;
state: QuizState;
ended: boolean;
}

let Header = observer(({ quiz, state, ended }: HeaderProps) => (
<header>
<h3>Quiz</h3>
<div className="counter">
{state.started ? (
!ended && (
let Header = observer(({ state, ended }: HeaderProps) => {
let { quiz } = useContext(QuizConfigContext)!;
return (
<header>
<h3>Quiz</h3>
<div className="counter">
{state.started ? (
!ended && (
<>
Question{" "}
{(state.attempt === 0
? state.index
: state.wrongAnswers!.indexOf(state.index)) + 1}{" "}
/{" "}
{state.attempt === 0
? quiz.questions.length
: state.wrongAnswers!.length}
</>
)
) : (
<>
Question{" "}
{(state.attempt === 0
? state.index
: state.wrongAnswers!.indexOf(state.index)) + 1}{" "}
/{" "}
{state.attempt === 0
? quiz.questions.length
: state.wrongAnswers!.length}
{quiz.questions.length} question
{quiz.questions.length > 1 && "s"}
</>
)
) : (
<>
{quiz.questions.length} question
{quiz.questions.length > 1 && "s"}
</>
)}
</div>
</header>
));
)}
</div>
</header>
);
});

interface AnswerReviewProps {
quiz: Quiz;
state: QuizState;
name: string;
nCorrect: number;
onRetry: () => void;
onGiveUp: () => void;
}

let AnswerReview = ({
quiz,
state,
name,
nCorrect,
onRetry,
onGiveUp
}: AnswerReviewProps) => {
let { quiz, name } = useContext(QuizConfigContext)!;
let confirm = !state.confirmedDone && (
<p style={{ marginBottom: "1em" }}>
You can either{" "}
Expand Down Expand Up @@ -294,15 +294,21 @@ export let useCaptureMdbookShortcuts = (capture: boolean) => {
}, [capture]);
};

export interface QuizViewProps {
export interface QuizViewConfig {
name: string;
quiz: Quiz;
fullscreen?: boolean;
cacheAnswers?: boolean;
allowRetry?: boolean;
onFinish?: (answers: TaggedAnswer[]) => void;
showBugReporter?: boolean;
}

export type QuizViewProps = QuizViewConfig & {
onFinish?: (answers: TaggedAnswer[]) => void;
};

export let QuizConfigContext = React.createContext<QuizViewConfig | null>(null);

let aCode = "a".charCodeAt(0);
export let generateQuestionTitles = (quiz: Quiz): string[] => {
let groups: Question[][] = [];
Expand Down Expand Up @@ -333,23 +339,27 @@ export let generateQuestionTitles = (quiz: Quiz): string[] => {
};

export let QuizView: React.FC<QuizViewProps> = observer(
({ quiz, name, fullscreen, cacheAnswers, allowRetry, onFinish }) => {
let [quizHash] = useState(() => hash.MD5(quiz));
let answerStorage = new AnswerStorage(name, quizHash);
({ onFinish, ...config }) => {
let [quizHash] = useState(() => hash.MD5(config.quiz));
let answerStorage = new AnswerStorage(config.name, quizHash);
let questionStates = useMemo(
() =>
quiz.questions.map(q => {
config.quiz.questions.map(q => {
let methods = getQuestionMethods(q.type);
return methods.questionState?.(q.prompt, q.answer);
}),
[quiz]
[config.quiz]
);
let state = useLocalObservable(() =>
loadState({ quiz, answerStorage, cacheAnswers })
loadState({
quiz: config.quiz,
answerStorage,
cacheAnswers: config.cacheAnswers
})
);

let saveToCache = () => {
if (cacheAnswers)
if (config.cacheAnswers)
answerStorage.save(
state.answers,
state.confirmedDone,
Expand All @@ -360,18 +370,18 @@ export let QuizView: React.FC<QuizViewProps> = observer(

// Don't allow any keyboard inputs to reach external listeners
// while the quiz is active (e.g. to avoid using the search box).
let ended = state.index === quiz.questions.length;
let ended = state.index === config.quiz.questions.length;
let inProgress = state.started && !ended;
useCaptureMdbookShortcuts(inProgress);

// Restore the user's scroll position after leaving fullscreen mode
let [lastTop, setLastTop] = useState<number | undefined>();
let showFullscreen = inProgress && (fullscreen ?? false);
let showFullscreen = inProgress && (config.fullscreen ?? false);
useLayoutEffect(() => {
document.body.style.overflowY = showFullscreen ? "hidden" : "auto";
if (showFullscreen) {
setLastTop(window.scrollY + 100);
} else if (fullscreen && lastTop !== undefined) {
} else if (config.fullscreen && lastTop !== undefined) {
window.scrollTo(0, lastTop);
}
}, [showFullscreen]);
Expand All @@ -389,7 +399,7 @@ export let QuizView: React.FC<QuizViewProps> = observer(
n => n === state.index
);
if (wrongAnswerIdx === state.wrongAnswers!.length - 1)
state.index = quiz.questions.length;
state.index = config.quiz.questions.length;
else state.index = state.wrongAnswers![wrongAnswerIdx + 1];
}

Expand All @@ -400,11 +410,11 @@ export let QuizView: React.FC<QuizViewProps> = observer(
attempt: state.attempt
});

if (state.index === quiz.questions.length) {
if (state.index === config.quiz.questions.length) {
let wrongAnswers = state.answers
.map((a, i) => ({ a, i }))
.filter(({ a }) => !a.correct);
if (wrongAnswers.length === 0 || !allowRetry) {
if (wrongAnswers.length === 0 || !config.allowRetry) {
state.confirmedDone = true;
} else {
state.wrongAnswers = wrongAnswers.map(({ i }) => i);
Expand All @@ -421,15 +431,13 @@ export let QuizView: React.FC<QuizViewProps> = observer(
// on first render...
state.confirmedDone;

let questionTitles = generateQuestionTitles(quiz);
let questionTitles = generateQuestionTitles(config.quiz);

let body = (
<section>
{state.started ? (
ended ? (
<AnswerReview
quiz={quiz}
name={name}
state={state}
nCorrect={nCorrect}
onRetry={action(() => {
Expand All @@ -444,12 +452,11 @@ export let QuizView: React.FC<QuizViewProps> = observer(
) : (
<QuestionView
key={state.index}
quizName={name}
multipart={quiz.multipart}
multipart={config.quiz.multipart}
index={state.index}
title={questionTitles[state.index]}
attempt={state.attempt}
question={quiz.questions[state.index]}
question={config.quiz.questions[state.index]}
questionState={questionStates[state.index]}
onSubmit={onSubmit}
/>
Expand Down Expand Up @@ -484,18 +491,20 @@ export let QuizView: React.FC<QuizViewProps> = observer(
let wrapperRef = useRef<HTMLDivElement | undefined>();

return (
<div ref={wrapperRef} className={wrapperClass}>
<div className="mdbook-quiz">
{showFullscreen && (
<>
{exitButton}
<ExitExplanation wrapperRef={wrapperRef} />
</>
)}
<Header quiz={quiz} state={state} ended={ended} />
{body}
<QuizConfigContext.Provider value={config}>
<div ref={wrapperRef} className={wrapperClass}>
<div className="mdbook-quiz">
{showFullscreen && (
<>
{exitButton}
<ExitExplanation wrapperRef={wrapperRef} />
</>
)}
<Header state={state} ended={ended} />
{body}
</div>
</div>
</div>
</QuizConfigContext.Provider>
);
}
);
13 changes: 6 additions & 7 deletions js/packages/quiz/src/questions/mod.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import classNames from "classnames";
import _ from "lodash";
import React, { useId, useMemo, useRef, useState } from "react";
import React, { useContext, useId, useMemo, useRef, useState } from "react";
import { type RegisterOptions, useForm } from "react-hook-form";

import type { Question } from "../bindings/Question";
import type { Quiz } from "../bindings/Quiz";
import { MarkdownView } from "../components/markdown";
import { MoreInfo } from "../components/more-info";
import { useCaptureMdbookShortcuts } from "../lib";
import { QuizConfigContext, useCaptureMdbookShortcuts } from "../lib";
import { MultipleChoiceMethods } from "./multiple-choice";
import { ShortAnswerMethods } from "./short-answer";
import { TracingMethods } from "./tracing";
Expand Down Expand Up @@ -113,7 +113,6 @@ we can better improve the surrounding text.
`.trim();

interface QuestionViewProps {
quizName: string;
multipart: Quiz["multipart"];
question: Question;
index: number;
Expand Down Expand Up @@ -146,7 +145,6 @@ let MultipartContext = ({
);

export let QuestionView: React.FC<QuestionViewProps> = ({
quizName,
multipart,
question,
index,
Expand All @@ -155,6 +153,7 @@ export let QuestionView: React.FC<QuestionViewProps> = ({
questionState,
onSubmit
}) => {
let { name: quizName, showBugReporter } = useContext(QuizConfigContext)!;
let start = useMemo(now, [quizName, question, index]);
let ref = useRef<HTMLFormElement>(null);
let [showExplanation, setShowExplanation] = useState(false);
Expand Down Expand Up @@ -209,7 +208,7 @@ export let QuestionView: React.FC<QuestionViewProps> = ({
/>
)}
<methods.PromptView prompt={question.prompt} />
{window.telemetry && (
{window.telemetry && showBugReporter && (
<BugReporter quizName={quizName} question={index} />
)}
</div>
Expand Down Expand Up @@ -266,7 +265,6 @@ interface AnswerViewProps {
}

export let AnswerView: React.FC<AnswerViewProps> = ({
quizName,
multipart,
question,
index,
Expand All @@ -275,6 +273,7 @@ export let AnswerView: React.FC<AnswerViewProps> = ({
correct,
showCorrect
}) => {
let { name: quizName, showBugReporter } = useContext(QuizConfigContext)!;
let methods = getQuestionMethods(question.type);
let questionClass = questionNameToCssClass(question.type);

Expand Down Expand Up @@ -309,7 +308,7 @@ export let AnswerView: React.FC<AnswerViewProps> = ({
<h4>Question {title}</h4>
{multipartView}
<methods.PromptView prompt={question.prompt} />
{window.telemetry && (
{window.telemetry && showBugReporter && (
<BugReporter quizName={quizName} question={index} />
)}
</div>
Expand Down
Loading

0 comments on commit b3106ab

Please sign in to comment.