Skip to content
Closed
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
9 changes: 9 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Ignore `requireLogin` in the book's metadata
NEXT_PUBLIC_IGNORE_LOGIN=true

# Publish all books, including unpublished ones
SHOW_UNPUBLISHED=true

# Access token for the books API
# Define this in .env.local, not here!
# NEXT_PUBLIC_ACCESS_TOKEN=
165 changes: 165 additions & 0 deletions components/Book/Book.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React, { useContext, useState } from "react";
import Image from "../Image";
/*
import { getBookId, QuizContextProvider } from "../../contexts/QuizContext";
import QuizProgress from "../Quiz/QuizProgress";
import { UserContext } from "../../contexts/UserContext";
import BookLogin from "../BookLogin/BookLogin";
import { SubmitQuiz } from "./SubmitQuiz";
import { useMutation } from "@tanstack/react-query";
*/
import { Chapter } from "./Chapter";
import { ContentIndexControl } from "./ContentIndex";
import { MdxContent } from "./MdxContent";
import { IntlContextProvider } from "../../i18n";
import Layout from "../layout";

const ignoreLogin = process && process.env.NEXT_PUBLIC_IGNORE_LOGIN === "true";

export const Book = ({ frontmatter, content, chapters, slug }) => {
const [isChapterIndexVisible, setIsChapterIndexVisible] = useState({});
/* const [quizState, setQuizState] = useState(null);
const [quizStateFetched, setQuizStateFetched] = useState(false);

const { user } = useContext(UserContext);
*/ const relativePath = React.useMemo(() => `/${slug}`, [slug]);
const mdxContent = React.useMemo(
/* hideQuestions is irrelevant because introduction can't contain them
(and MdxContent throws an exceptions if it does) */
() => <MdxContent content={content} />,
[content]
);
/*
const { mutate: fetchBookState } = useMutation({
mutationFn: () =>
fetch(
`${process.env.NEXT_PUBLIC_API_URL}/state?book_id=${getBookId(
frontmatter.title,
slug
)}`,
{
headers: {
"access-token": user?.access_token,
},
}
).then((res) => res.json()),
onError: () => {
setQuizStateFetched(true);
},
onSuccess: (data) => {
setQuizState(data);
setQuizStateFetched(true);
},
});
*/
const chapterNumbers = React.useMemo(() => {
let counter = 1;
let _chapterNumbers = {};

chapters.forEach((c, index: number) => {
if (c.frontmatter.omitAsChapter) {
return;
}

_chapterNumbers[index] = counter;
counter = counter + 1;
});

return _chapterNumbers;
}, [chapters]);
/*
React.useEffect(() => {
if (!user?.access_token || !frontmatter.requireLogin || ignoreLogin) {
return;
}

fetchBookState();
}, [frontmatter.requireLogin, fetchBookState, user?.access_token]);

React.useEffect(() => {
if (frontmatter.hideQuestions && frontmatter.showQuizProgress) {
throw new Error(
"Incompatible options: hideQuestions: true and showQuizProgress: true"
);
}
}, [frontmatter.hideQuestions, frontmatter.showQuizProgress]);

if (frontmatter.requireLogin && !(user?.access_token || ignoreLogin)) {
return (
<BookLogin
emailContent={frontmatter.email}
title={frontmatter.title}
loginSubtitle={frontmatter.loginSubtitle}
/>
);
}
if (!ignoreLogin && frontmatter.requireLogin && frontmatter.logQuizzes && !quizStateFetched) {
return null;
}
*/
return (
<IntlContextProvider lang={frontmatter.language}>
{/* <QuizContextProvider
title={frontmatter.title}
quizState={quizState}
slug={slug}
submissionEmail={frontmatter.submissionEmail}
quizThreshold={frontmatter.quizThreshold || 0.8}
logQuizzes={frontmatter.logQuizzes}
> */ }
<Layout title={frontmatter.title}
showHome={!frontmatter.tocInHeader}
chapters={frontmatter.tocInHeader && chapters}
isChapterIndexVisible={frontmatter.tocInHeader && isChapterIndexVisible}>
<div className="prose mx-auto book">
{frontmatter.coverImg && (
<div className="book-cover-img">
<Image
width={650}
height={650}
layout={"responsive"}
alt={"cover image"}
src={`${relativePath}/${frontmatter.coverImg}`}
/>
</div>
)}

{ /* frontmatter.showQuizProgress && <QuizProgress /> */}

<h1 className="max-w-sm mb-0 font-medium">{frontmatter.title}</h1>
<p className="subtitle">{frontmatter.subTitle}</p>

{mdxContent}

{!frontmatter.tocInHeader &&
<ContentIndexControl
chapters={chapters}
isChapterIndexVisible={isChapterIndexVisible}
showQuizProgress={!!frontmatter.showQuizProgress}
startSmall={!!frontmatter.indexInitiallyClosed}
/>
}

{ /* frontmatter.requireLogin && frontmatter.logQuizzes && (
<SubmitQuiz submitText={frontmatter.submitQuizText} />
) */}

{chapters.map(
({ frontmatter: chapterFrontmatter, content }, index) => (
<Chapter
key={chapterFrontmatter.title}
frontmatter={chapterFrontmatter}
content={content}
index={index}
setIsChapterIndexVisible={setIsChapterIndexVisible}
hideQuestions={!!frontmatter.hideQuestions}
chapterNumber={chapterNumbers[index]}
/>
)
)}
</div>
</Layout>
{ /* </QuizContextProvider> */ }
</IntlContextProvider>
);
};
6 changes: 6 additions & 0 deletions components/Book/CcByNcNd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Image from "../Image";
import image from "../../public/icons/cc-by-nc-nd.png";

export default function CcByNcNd() {
return <Image {...image} alt="" />;
}
51 changes: 51 additions & 0 deletions components/Book/Chapter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react";
import { useOnScreen } from "../../hooks/useOnScreen";
import { useIntl } from "../../i18n";
import slugify from "slugify";
import { MdxContent } from "./MdxContent";
/* import { QuizContext } from "../../contexts/QuizContext"; */

export const Chapter = ({
frontmatter,
content,
index,
setIsChapterIndexVisible,
hideQuestions,
chapterNumber,
}) => {
const ref = React.useRef<HTMLDivElement | null>(null);
const isVisible = useOnScreen(ref);
/* const { quizState } = React.useContext(QuizContext); */
const { t } = useIntl();

React.useEffect(() => {
setIsChapterIndexVisible((val) => ({ ...val, ...{ [index]: isVisible } }));
}, [isVisible, setIsChapterIndexVisible, index]);

const mdxContent = React.useMemo(() => {
return (
<MdxContent
chapterIndex={index}
content={content}
hideQuestions={false /* hideQuestions */}
showQuiz={false /* quizState.activeChapters.includes(index)} */}
chapterTitle={frontmatter.title}
/>
);
}, [content, frontmatter.title, hideQuestions, index, /* quizState.activeChapters */]);

return (
<div ref={ref} className="flex-container">
<div className="right-column">
<div className="prose mx-auto mt-8 chapter">
<h2 className="chapter-title" id={slugify(frontmatter.title)}>
{chapterNumber && `${t("book.chapter")} ${chapterNumber}: `}
{frontmatter.title}
</h2>

{mdxContent}
</div>
</div>
</div>
);
};
138 changes: 138 additions & 0 deletions components/Book/ContentIndex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from "react";
import { ImShrink2, ImEnlarge2 } from "react-icons/im";
import {
RiCheckboxBlankCircleLine,
RiCheckboxCircleFill,
RiRecordCircleLine,
} from "react-icons/ri";
import Link from "next/link";
import slugify from "slugify";
import { useIntl } from "../../i18n";

/* import { QuizContext } from "../../contexts/QuizContext";

const QuizState = ({ chapterIndex }: { chapterIndex: number }) => {
const { quizState, allCompletedChapters, chaptersWithMandatoryQuestions } =
React.useContext(QuizContext);

if (!chaptersWithMandatoryQuestions.includes(chapterIndex)) {
return null;
}

if (allCompletedChapters.includes(chapterIndex)) {
return <RiCheckboxCircleFill className="active" />;
}
if (quizState.activeChapters.includes(chapterIndex)) {
return <RiRecordCircleLine className="active" />;
}
if (quizState.chaptersWithQuiz.includes(chapterIndex)) {
return <RiCheckboxBlankCircleLine className="inactive" />;
}

return null;
};

const QuizProgress = () => {
const { availablePoints, achievedPoints, quizState } =
React.useContext(QuizContext);

const { width, color } = React.useMemo(() => {
return {
width: (achievedPoints / availablePoints) * 100,
color:
achievedPoints / availablePoints >= quizState.quizThreshold
? "green"
: "red",
};
}, [achievedPoints, availablePoints, quizState.quizThreshold]);

return (
<>
<div className="quiz-progress-indicator-wrapper">
<div
className="quiz-progress-indicator-bar"
style={{ width: `${width}%`, backgroundColor: color }}
>
<div
className="quiz-progress-indicator-bar-threshold-line"
style={{ left: `${quizState.quizThreshold * 100}%` }}
/>
</div>
</div>
<div
className="quiz-progress-indicator-threshold"
style={{ left: `${quizState.quizThreshold * 100}%` }}
>
{quizState.quizThreshold * 100}%
</div>
</>
);
};
*/
interface ContentIndexProps {
contentTitle?: string;
chapters: any[];
isChapterIndexVisible: Record<number, boolean>;
showQuizProgress?: boolean;
className?: string;
}

export const ContentIndex = (
{ chapters, isChapterIndexVisible, showQuizProgress, className, contentTitle }: ContentIndexProps) =>
{
const highestVisibleIndex = React.useMemo(() => {
let highestIndex = 0;

Object.keys(isChapterIndexVisible).forEach((c) => {
const chapterIndex = parseInt(c);
if (isChapterIndexVisible[chapterIndex] && chapterIndex > highestIndex) {
highestIndex = chapterIndex;
}
});

return highestIndex;
}, [isChapterIndexVisible]);

return (<>
<div className={`content ${className || ""}`}>
{ contentTitle && <h2>{contentTitle}</h2> }

<ul>
{chapters.map(({frontmatter}, index) => (
<li className="content-index-chapter" key={index}>
<Link legacyBehavior href={"#" + slugify(frontmatter.title)}>
<a className={highestVisibleIndex === index ? "active" : ""}>
{frontmatter.title}
</a>
</Link>

{/*showQuizProgress && <QuizState chapterIndex={index}/> */}
</li>
))}
</ul>
</div>

{/*showQuizProgress && <QuizProgress/>*/}
</>
);
}

export const ContentIndexControl = ({ chapters, isChapterIndexVisible, showQuizProgress, startSmall }) => {
const [small, setSmall] = React.useState(startSmall);
const { t } = useIntl();

return (
<div className={small ? "small content-index" : "content-index"}>
<div className="toolbar">
<button className="icon-button" onClick={() => setSmall(!small)}>
{small ? <ImEnlarge2 /> : <ImShrink2 />}
</button>
</div>
<ContentIndex
contentTitle={t("book.chapters")}
chapters={chapters}
showQuizProgress={showQuizProgress}
isChapterIndexVisible={isChapterIndexVisible}/>
</div>
);
};
Loading