Skip to content

Commit

Permalink
Add telemetry and back button to quiz. Add version watermark
Browse files Browse the repository at this point in the history
  • Loading branch information
willcrichton committed Dec 18, 2024
1 parent 1f842a2 commit dc51d88
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 55 deletions.
4 changes: 3 additions & 1 deletion js/packages/repo-quest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"@wcrichto/quiz": "^0.3.8",
"highlight.js": "^11.11.0",
"jsdom": "^25.0.0",
"lodash": "^4.17.21",
"marked": "^15.0.3",
Expand All @@ -24,6 +25,7 @@
"normalize.css": "^8.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.78.0"
"sass": "^1.78.0",
"uuid": "^11.0.3"
}
}
109 changes: 84 additions & 25 deletions js/packages/repo-quest/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { observer } from "mobx-react";
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import ReactDOM from "react-dom/client";
import * as uuid from "uuid";

import guideMd from "../../../../GUIDE.md?raw";
import {
Expand All @@ -21,6 +22,48 @@ import {
commands
} from "./bindings/backend";

declare global {
var VERSION: string;
var COMMIT_HASH: string;
var TELEMETRY_URL: string;
}

function getSessionId() {
const SESSION_STORAGE_KEY = "__repo_quest_telemetry_session";
if (localStorage.getItem(SESSION_STORAGE_KEY) === null) {
localStorage.setItem(SESSION_STORAGE_KEY, uuid.v4());
}
return localStorage.getItem(SESSION_STORAGE_KEY)!;
}

class Telemetry {
private sessionId: string;

constructor() {
this.sessionId = getSessionId();
}

// biome-ignore lint/suspicious/noExplicitAny: payload can be anything
log(_endpoint: string, payload: any) {
let log = {
sessionId: this.sessionId,
commitHash: COMMIT_HASH,
version: VERSION,
timestamp: new Date().getTime(),
payload
};

let fullUrl = `${TELEMETRY_URL}/rq_answers`;
fetch(fullUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(log)
});
}
}

let useWindowListener = <K extends keyof WindowEventMap>(
event: K,
listener: (this: Window, ev: WindowEventMap[K]) => void
Expand Down Expand Up @@ -318,44 +361,57 @@ let NewQuest = () => {
);
};

let QuizPage: React.FC<{ quest: QuestConfig }> = ({ quest }) => {
let QuizPage: React.FC<{ quest: QuestConfig; goBack: () => void }> = ({
quest,
goBack
}) => {
let quiz = quest.final as
/* biome-ignore lint/suspicious/noExplicitAny: backend guarantees that this satisfies Quiz */
any as Quiz;
return (
<QuizView
name={quest.title}
quiz={quiz}
cacheAnswers={true}
autoStart={true}
allowRetry={true}
/>
<div>
<div>
<button type="button" onClick={goBack}>
Back to main page
</button>
</div>
<QuizView
name={quest.title}
quiz={quiz}
// cacheAnswers={true}
autoStart={true}
allowRetry={true}
/>
</div>
);
};

let QuestView: React.FC<{
quest: QuestConfig;
initialState: StateDescriptor;
}> = ({ quest, initialState }) => {
console.debug(quest);

let loader = useContext(Loader.context)!;
let [state, setState] = useState<StateDescriptor | undefined>(initialState);
let [showQuiz, setShowQuiz] = useState(false);
let setTitle = useContext(TitleContext)!;
useEffect(() => setTitle(quest.title), [quest.title]);

useEffect(() => {
events.stateEvent.listen(e => setState(e.payload));
console.debug("QuestConfig", quest);
events.stateEvent.listen(e => {
if (!_.isEqual(e.payload, state)) setState(e.payload);
});
}, []);

console.debug("State", state);

let cur_stage =
state && state.state.type === "Ongoing"
? state.state.stage
: quest.stages.length - 1;

if (showQuiz) {
return <QuizPage quest={quest} />;
return <QuizPage quest={quest} goBack={() => setShowQuiz(false)} />;
}

return (
Expand All @@ -365,6 +421,12 @@ let QuestView: React.FC<{
<>
<div className="quest-dir">
<strong>Quest directory:</strong> <code>{state.dir}</code>
<button
type="button"
onClick={() => navigator.clipboard.writeText(state!.dir)}
>
📋
</button>
</div>
{state.behind_origin && (
<div className="behind-origin-warning">
Expand All @@ -382,9 +444,13 @@ let QuestView: React.FC<{
/>
))}
{state.state.type === "Completed" && quest.final && (
<li>
<li className="quiz-bullet">
<div>
<span className="stage-title">Quiz</span>{" "}
</div>
<div>
<span className="stage-title">Quiz</span>
Check your conceptual understanding of the material by
taking this quiz.
</div>
<div>
<button type="button" onClick={() => setShowQuiz(true)}>
Expand All @@ -409,17 +475,6 @@ let QuestView: React.FC<{
</button>
</div>

<div>
<button
type="button"
onClick={() => {
if (state) navigator.clipboard.writeText(state.dir);
}}
>
Copy directory to 📋
</button>
</div>

{initialState.can_skip && (
<div>
<select
Expand Down Expand Up @@ -597,6 +652,7 @@ let App = () => {
undefined
);
let [loader] = useState(() => new Loader());

return (
<Loader.context.Provider value={loader}>
<ErrorContext.Provider value={setErrorMessage}>
Expand All @@ -621,12 +677,15 @@ let App = () => {
<GithubLoader />
)}
</div>
<div id="version-watermark">v{VERSION}</div>
</TitleContext.Provider>
</ErrorContext.Provider>
</Loader.context.Provider>
);
};

window.telemetry = new Telemetry();

ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

/* @ts-ignore */
Expand Down
28 changes: 27 additions & 1 deletion js/packages/repo-quest/styles/index.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import "normalize.css/normalize.css";
@import "@wcrichto/quiz/lib.scss";
@import "highlight.js/styles/default.css";
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap');

:root {
Expand Down Expand Up @@ -50,13 +51,21 @@ h1 {

.quest-dir {
margin-bottom: 1rem;

button {
margin-left: 0.5rem;
}
}

.stages {
margin: 0;

li {
margin-bottom: 1rem;

&.quiz-bullet {
list-style-type: none;
}
}
}

Expand Down Expand Up @@ -255,7 +264,7 @@ dialog {

.guide-button {
position: absolute;
top: 2rem;
top: 1.5rem;
right: 1rem;
}

Expand All @@ -264,4 +273,21 @@ dialog {
padding: 0.5rem;
margin: 1rem 0;
background-color: rgb(255, 251, 167);
}

#version-watermark {
position: fixed;
bottom: 0.5rem;
right: 0.5rem;
color: #ccc;
font-size: 60%;
font-family: var(--code-font);
}

.mdbook-quiz .answer .answer-row > div {
.correct, .incorrect {
&::before {
font-size: 1rem !important;
}
}
}
9 changes: 8 additions & 1 deletion js/packages/repo-quest/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as cp from "node:child_process";
import path from "node:path";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import packageJson from "./package.json";

let commitHash = cp.execSync("git rev-parse HEAD").toString("utf-8").trim();

let alias = {
"@wcrichto/rust-editor/dist/lib.css": path.resolve(
Expand All @@ -13,7 +17,10 @@ let alias = {
export default defineConfig(({ mode }) => ({
base: "./",
define: {
"process.env.NODE_ENV": JSON.stringify(mode)
"process.env.NODE_ENV": JSON.stringify(mode),
VERSION: JSON.stringify(packageJson.version),
COMMIT_HASH: JSON.stringify(commitHash),
TELEMETRY_URL: JSON.stringify("https://rust-book.willcrichton.net/logs")
},
plugins: [react()],
resolve: { alias },
Expand Down
18 changes: 18 additions & 0 deletions js/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit dc51d88

Please sign in to comment.