Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an option to enable the bug reporter #46

Merged
merged 1 commit into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading