5 | ```
6 |
--------------------------------------------------------------------------------
/src/questions/question-13/inquiry.mdx:
--------------------------------------------------------------------------------
1 | What will be the output of the following code?
2 |
3 | ```html-derivative
4 |
5 | ```
6 |
--------------------------------------------------------------------------------
/src/app/review/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "../_components/layout";
2 |
3 | export default function Page({ children }: React.PropsWithChildren) {
4 | return {children};
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/mdx/code-block.ts:
--------------------------------------------------------------------------------
1 | import dedent from "dedent";
2 |
3 | export const codeBlock = (s: TemplateStringsArray) => {
4 | return `\`\`\`html-derivative\n${dedent`${s.join("\n")}`}\n\`\`\``;
5 | };
6 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "*.{md,json,yml,yaml,html}": [
3 | "prettier --write",
4 | "cspell --no-must-find-files"
5 | ],
6 | "*.{ts,tsx}": ["eslint --fix", "cspell --no-must-find-files"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/quiz/[step]/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "../../_components/layout";
2 |
3 | export default function Page({ children }: React.PropsWithChildren) {
4 | return {children};
5 | }
6 |
--------------------------------------------------------------------------------
/src/questions/question-6/inquiry.mdx:
--------------------------------------------------------------------------------
1 | What is the correct HTML to make the following text semantically accurate?
2 |
3 | ```html-derivative
4 |
The old price was $49.99, but now it's just $29.99!
5 | ```
6 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Supabase (can get them from your local Supabase instance)
2 | NEXT_PUBLIC_SUPABASE_URL=
3 | NEXT_PUBLIC_SUPABASE_ANON_KEY=
4 |
5 | # Posthog
6 | NEXT_PUBLIC_POSTHOG_HOST=
7 | NEXT_PUBLIC_POSTHOG_KEY=
8 |
--------------------------------------------------------------------------------
/scripts/generate-supabase-types:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | OUT=./src/lib/supabase/Database.ts
5 |
6 | npx --no-install supabase gen types --local >$OUT
7 | echo -e "/* eslint-disable */\n$(cat $OUT)" >$OUT
8 |
--------------------------------------------------------------------------------
/src/questions/question-2/inquiry.mdx:
--------------------------------------------------------------------------------
1 | What is the color of the "FullstacksJS" text in the following code?
2 |
3 | ```html
4 |
5 |
6 | FullstacksJS
7 |
8 | ```
9 |
--------------------------------------------------------------------------------
/src/questions/question-16/inquiry.mdx:
--------------------------------------------------------------------------------
1 | What will be the output of the following code?
2 |
3 | ```html
4 |
5 |
6 |
9 | ```
10 |
--------------------------------------------------------------------------------
/src/lib/cn.tsx:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from "clsx";
2 |
3 | import { clsx } from "clsx";
4 | import { twMerge } from "tailwind-merge";
5 |
6 | export const cn = (...inputs: ClassValue[]) => {
7 | return twMerge(clsx(inputs));
8 | };
9 |
--------------------------------------------------------------------------------
/src/questions/question-3/inquiry.mdx:
--------------------------------------------------------------------------------
1 | Which of the following is the best way to link an accessible error message to a form field?
2 |
3 | ```html
4 |
5 |
6 | Error: Enter a valid email address
7 |
8 | ```
9 |
--------------------------------------------------------------------------------
/src/questions/question-14/inquiry.mdx:
--------------------------------------------------------------------------------
1 | What is the color of the "FullstacksJS" text in the following HTML?
2 |
3 | ```html
4 |
5 |
6 | FullstacksJS
7 |
8 |
9 | ```
10 |
--------------------------------------------------------------------------------
/src/questions/question-10/inquiry.mdx:
--------------------------------------------------------------------------------
1 | What is the color of the "FullstacksJS" text in the following code?
2 |
3 | ```html
4 |
5 |
6 |
FullstacksJS
7 |
8 |
9 | ```
10 |
--------------------------------------------------------------------------------
/src/questions/question-9/inquiry.mdx:
--------------------------------------------------------------------------------
1 | What is the color of the "FullstacksJS" text in the following code?
2 |
3 | ```html
4 |
5 |
6 |
FullstacksJS
7 |
8 |
9 | ```
10 |
--------------------------------------------------------------------------------
/src/app/summary/layout.tsx:
--------------------------------------------------------------------------------
1 | export default function SummaryLayout({ children }: React.PropsWithChildren) {
2 | return (
3 |
4 | {children}
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/src/questions/question-11/inquiry.mdx:
--------------------------------------------------------------------------------
1 | What is the color of the "FullstacksJS" text in the following code?
2 |
3 | ```html
4 |
8 |
9 |
FullstacksJS
10 |
11 | ```
12 |
--------------------------------------------------------------------------------
/src/lib/mdx/mdx-options.ts:
--------------------------------------------------------------------------------
1 | import type { NextMDXOptions } from "@next/mdx";
2 |
3 | import rehypeShiki from "@shikijs/rehype";
4 |
5 | import { shikiOptions } from "./shiki-options";
6 |
7 | export const mdxOptions: NextMDXOptions["options"] = {
8 | rehypePlugins: [[rehypeShiki, shikiOptions]],
9 | };
10 |
--------------------------------------------------------------------------------
/src/questions/question-12/inquiry.mdx:
--------------------------------------------------------------------------------
1 | How many columns are there in the following table?
2 |
3 | ```html
4 |
22 | ))}
23 | >
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/questions/question-15/index.tsx:
--------------------------------------------------------------------------------
1 | import { compileMDX } from "@app/mdx/compileMdx";
2 |
3 | import type { Question } from "../Question";
4 |
5 | import { IDontKnow } from "../shared-options";
6 | import explanation from "./explanation.mdx";
7 |
8 | export default {
9 | id: 15,
10 | inquiry: await compileMDX("What does the `inert` attribute do in HTML?"),
11 | options: [
12 | { id: 1, text: () => "To prevent elements from being interactive." },
13 | {
14 | id: 2,
15 | text: () =>
16 | "It disables all styles applied to an element and its children.",
17 | },
18 | {
19 | id: 3,
20 | text: () => "It pauses animations and transitions for an element.",
21 | },
22 | IDontKnow,
23 | ],
24 | explanation,
25 | correctAnswerId: 1,
26 | } satisfies Question;
27 |
--------------------------------------------------------------------------------
/src/state/useAnswers.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { useSessionStorage } from "usehooks-ts";
3 |
4 | export type UserAnswers = Record;
5 |
6 | const initialAnswers: UserAnswers = {};
7 |
8 | export function useResetUserAnswers() {
9 | return useSessionStorage("answers", initialAnswers)[2];
10 | }
11 |
12 | export function useUserAnswers() {
13 | return useSessionStorage("answers", initialAnswers)[0];
14 | }
15 |
16 | export function useStoreAnswer(questionId: number) {
17 | const [, setAnswers] = useSessionStorage(
18 | "answers",
19 | initialAnswers,
20 | );
21 |
22 | return useCallback(
23 | (answerId: number) => {
24 | setAnswers((answers) => ({ ...answers, [questionId]: answerId }));
25 | },
26 | [questionId, setAnswers],
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/questions/question-5/explanation.mdx:
--------------------------------------------------------------------------------
1 | > The term being defined is identified following these rules: [Reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dfn#usage_notes)
2 | >
3 | > 1. If the {""} element has a title attribute, the value of the title attribute is considered to be the term being defined. The element must still have text within it, but that text may be an abbreviation (perhaps using {""}) or another form of the term.
4 | > 2. If the {""} contains a single child element and does not have any text content of its own, and the child element is an {""} element with a title attribute itself, then the exact value of the {""} element's title is the term being defined.
5 | > 3. Otherwise, the text content of the {""} element is the term being defined. This is shown in the first example below.
6 |
--------------------------------------------------------------------------------
/src/questions/question-14/explanation.mdx:
--------------------------------------------------------------------------------
1 | > The caption must be used as a child of a table element. [Spec Reference](https://html.spec.whatwg.org/multipage/tables.html#the-caption-element)
2 |
3 | Therefore, The `caption` element is considered valid only when the parser is in the ["in table" insertion mode](https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intable).
4 | Otherwise, the parser must ignore the `caption` element.
5 |
6 | For in-body parsing mode:
7 |
8 | > A start tag whose tag name is one of: `caption`, `col`, `colgroup`, `frame`, `head`, `tbody`, `td`, `tfoot`, `th`, `thead`, `tr`
9 | >
10 | > Parse error. Ignore the token.
11 | > [Spec Reference](https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody)
12 |
13 | Therefore, the code is equivalent to:
14 |
15 | ```html
16 |
17 | FullstacksJS
18 |
19 | ```
20 |
--------------------------------------------------------------------------------
/src/app/quiz/_actions/submit-answer.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import {
4 | isDuplicationError,
5 | isPermissionError,
6 | } from "@app/supabase/supabase-error";
7 | import { redirect } from "next/navigation";
8 |
9 | import { saveUserAnswers } from "./save-answers.action";
10 |
11 | interface Args {
12 | answers: Record;
13 | }
14 |
15 | export const submitAnswer = async (
16 | _prevState: { error?: string },
17 | { answers }: Args,
18 | ): Promise<{ error?: string }> => {
19 | try {
20 | await saveUserAnswers(answers);
21 | redirect("/summary");
22 | } catch (e) {
23 | if (isPermissionError(e)) {
24 | return { error: "Not permitted! The project might be misconfigured." };
25 | }
26 | if (isDuplicationError(e)) {
27 | return { error: "You have already submitted your answers." };
28 | }
29 |
30 | throw e;
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/src/questions/question-4/index.tsx:
--------------------------------------------------------------------------------
1 | import { compileMDX } from "@app/mdx/compileMdx";
2 |
3 | import type { Question } from "../Question";
4 |
5 | import { IDontKnow } from "../shared-options";
6 | import explanation from "./explanation.mdx";
7 | import inquiry from "./inquiry.mdx";
8 |
9 | export default {
10 | id: 4,
11 | inquiry,
12 | options: [
13 | { id: 1, text: () => "It does nothing." },
14 | {
15 | id: 2,
16 | text: await compileMDX(
17 | `It creates a new standalone custom element called {""}`,
18 | ),
19 | },
20 | {
21 | id: 3,
22 | text: await compileMDX(
23 | `It can extend the {" element with a custom behavior defined in the counter-button class.`,
24 | ),
25 | },
26 | IDontKnow,
27 | ],
28 | explanation,
29 | correctAnswerId: 3,
30 | } satisfies Question;
31 |
--------------------------------------------------------------------------------
/src/questions/question-15/explanation.mdx:
--------------------------------------------------------------------------------
1 | > The `inert` global attribute is a Boolean attribute indicating that the element and all of its flat tree descendants become inert.
2 | >
3 | > ### Inert does the following:
4 | > * Prevents the `click` event from being fired when the user clicks on the element.
5 | > * Prevents the `focus` event from being raised by preventing the element from gaining focus.
6 | > * Prevents any contents of the element from being found/matched during any use of the browser's find-in-page feature.
7 | > * Prevents users from selecting text within the element — akin to using the CSS property `user-select` to disable text selection.
8 | > * Prevents users from editing any contents of the element that are otherwise editable.
9 | > * Hides the element and its content from assistive technologies by excluding them from the accessibility tree.
10 | >
11 | > [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inert)
12 |
13 |
--------------------------------------------------------------------------------
/src/app/_components/button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@app/cn";
2 | import { Slot } from "@radix-ui/react-slot";
3 |
4 | export interface ButtonProps extends React.ButtonHTMLAttributes {
5 | children: React.ReactNode;
6 | className?: string;
7 | variant?: "contained" | "secondary";
8 | size?: "md" | "sm";
9 | asChild?: boolean;
10 | }
11 |
12 | export const Button = ({
13 | variant = "contained",
14 | className,
15 | asChild,
16 | size = "md",
17 | ...props
18 | }: ButtonProps) => {
19 | const Comp = asChild ? Slot : "button";
20 |
21 | return (
22 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/lib/supabase/createClient.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from "@supabase/ssr";
2 | import { cookies } from "next/headers";
3 |
4 | import type { Database } from "./Database";
5 |
6 | import { config } from "./config";
7 |
8 | export async function createSupabaseClient() {
9 | const cookieStore = await cookies();
10 |
11 | return createServerClient(
12 | config.get("url"),
13 | config.get("anonKey"),
14 | {
15 | cookies: {
16 | getAll() {
17 | return cookieStore.getAll();
18 | },
19 | setAll(cookiesToSet) {
20 | try {
21 | cookiesToSet.forEach(({ name, value, options }) =>
22 | cookieStore.set(name, value, options),
23 | );
24 | } catch {
25 | // The `setAll` method was called from a Server Component.
26 | // This can be ignored if you have middleware refreshing
27 | // user sessions.
28 | }
29 | },
30 | },
31 | },
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/questions/question-1/explanation.mdx:
--------------------------------------------------------------------------------
1 | > The {''} HTML element displays its contents styled in a fashion intended to indicate that the text is a short fragment of computer code. [MDN reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code)
2 |
3 | ```html
4 | echo "Hello, FullstacksJS"
5 | ```
6 |
7 | This represents a command-line instruction, which is code that a user would type into a terminal. That's exactly what the {''} element is for—marking up a snippet of code inline.
8 |
9 | ---
10 |
11 | > The {''} HTML element is used to enclose inline text which represents sample (or quoted) output from a computer program. [MDN reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/samp)
12 |
13 | ```html
14 | Hello, FullstacksJS
15 | ```
16 |
17 | This is the output you get after running the code. The {''} tag semantically represents what the computer outputs, often used in documentation or tutorials.
18 |
--------------------------------------------------------------------------------
/src/app/summary/_components/correct-answer-count.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { Question } from "@app/questions/Question";
4 |
5 | import { useUserAnswers } from "@app/state/useAnswers";
6 | import { isNull } from "@fullstacksjs/toolbox";
7 | import { useEffect, useState } from "react";
8 |
9 | import { Skeleton } from "./skeleton";
10 |
11 | interface Props {
12 | questions: Pick[];
13 | }
14 |
15 | export const CorrectAnswerCount = ({ questions }: Props) => {
16 | const answers = useUserAnswers();
17 | const [correctAnswers, setCorrectAnswers] = useState();
18 |
19 | useEffect(() => {
20 | // eslint-disable-next-line react-hooks/set-state-in-effect, @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
21 | setCorrectAnswers(
22 | questions.filter((q) => answers[q.id] === q.correctAnswerId).length,
23 | );
24 | }, [answers, questions]);
25 |
26 | if (isNull(correctAnswers)) return ;
27 | return correctAnswers.toString().padStart(2, "0");
28 | };
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "noUncheckedIndexedAccess": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "react-jsx",
16 | "incremental": true,
17 | "plugins": [{ "name": "next" }],
18 | "paths": {
19 | "@app/state/*": ["./src/state/*"],
20 | "@app/questions/*": ["./src/questions/*"],
21 | "@app/supabase/*": ["./src/lib/supabase/*"],
22 | "@app/posthog/*": ["./src/lib/posthog/*"],
23 | "@app/cn": ["./src/lib/cn"],
24 | "@app/mdx/*": ["./src/lib/mdx/*"]
25 | }
26 | },
27 | "include": [
28 | "next-env.d.ts",
29 | "**/*.ts",
30 | "**/*.tsx",
31 | ".next/types/**/*.ts",
32 | ".next/dev/types/**/*.ts"
33 | ],
34 | "exclude": ["node_modules"]
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { RestartButton } from "./_components/restart-button";
2 |
3 | export default function HomePage() {
4 | return (
5 |
6 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/questions/question-10/explanation.mdx:
--------------------------------------------------------------------------------
1 | > A `p` element's end tag can be omitted if the `p` element is immediately followed by
2 | > an `address`, `article`, `aside`, `blockquote`, `details`, `dialog`, `div`, `dl`,
3 | > `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1`, `h2`, `h3`, `h4`, `h5`,
4 | > `h6`, `header`, `hgroup`, `hr`, `main`, `menu`, `nav`, `ol`, `p`, `pre`, `search`,
5 | > `section`, `table`, or `ul` element,
6 | > or if there is no more content in the parent element and the parent element is an HTML element that is not
7 | > an `a`, `audio`, `del`, `ins`, `map`, `noscript`, or `video` element, or an autonomous custom element.
8 | > [Spec Reference](https://html.spec.whatwg.org/multipage/grouping-content.html#the-p-element)
9 |
10 | This mean the parser will automatically close the `p` tag when it encounters the next start `p` tag.
11 |
12 | Therefore the code will be equivalent to:
13 |
14 | ```html
15 |
16 |
17 |
FullstacksJS
18 |
19 |
20 | ```
21 |
22 | And the color of the text "FullstacksJS" is red.
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 FullstacksJS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/questions/question-2/explanation.mdx:
--------------------------------------------------------------------------------
1 | To understand why FullstacksJS inherits the red color, we need to understand two facts about HTML syntax:
2 |
3 | > Self-closing tags ({''}) do not exist in HTML. If a trailing slash character is present in the start tag of an HTML element, HTML parsers **ignore** that slash character. [MDN Reference](https://developer.mozilla.org/en-US/docs/Glossary/Void_element#self-closing_tags)
4 |
5 | > A `p` element's end tag may be omitted if the p element is immediately followed by an `div`, `dl`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `header`, `hgroup`, `hr`, `main`, `menu`, `nav`, `ol`, `p`, `pre`, `search`, `section`, `table`, or `ul` element, or if there is no more content in the parent element and the parent element is an HTML element that is not an `a`, `audio`, `del`, `ins`, `map`, `noscript`, or `video` element, or an autonomous custom element. [Spec Reference](https://html.spec.whatwg.org/multipage/syntax.html#optional-tags)
6 |
7 | So the code is equivalent to:
8 |
9 | ```html
10 |
39 | `),
40 | },
41 | IDontKnow,
42 | ],
43 | explanation,
44 | correctAnswerId: 1,
45 | } satisfies Question;
46 |
--------------------------------------------------------------------------------
/src/questions/question-16/explanation.mdx:
--------------------------------------------------------------------------------
1 | The parser automatically closes the first `p` tag when facing the next opening `p` tag.
2 |
3 | ### Why?
4 | There's a rule for that in the spec:
5 |
6 | > A `p` element's end tag may be omitted if the `p` element is immediately followed by an
7 | > `address`, `article`, `p`, `aside`, [...] or if there is no more content in the parent element and the parent element is an HTML element that is not an
8 | > `a`, `audio`, `del`, [...], `ins` element, or an autonomous custom element. [Spec reference](https://html.spec.whatwg.org/multipage/syntax.html#normal-elements)
9 |
10 | Then the parser will create the second `p` element for ``.
11 |
12 | And finally the parser will create the third `p` element when it encounters the closing `` tag.
13 |
14 | ### Why?
15 |
16 | When the parser is in [in body insertion mode](https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody), and encounters a token for closing `p` tag, it must follow this algorithm:
17 |
18 | > **An end tag whose tag name is `p`**
19 | >
20 | > If the stack of open elements does not have a `p` element in button scope, then this is a parse error;
21 | > insert an HTML element for a `p` start tag token with no attributes.
22 | >
23 | > Close a `p` element. ([I don't recommend opening this link](https://html.spec.whatwg.org/multipage/parsing.html#close-a-p-element))
24 |
25 |
--------------------------------------------------------------------------------
/src/app/quiz/[step]/page.tsx:
--------------------------------------------------------------------------------
1 | import { questions } from "@app/questions/questions";
2 | import { notFound } from "next/navigation";
3 |
4 | import { Question } from "../_components/question/question";
5 | import { QuestionProgressbar } from "../_components/question/question-progressbar";
6 | import { QuizHeader } from "../../_components/quiz-header";
7 |
8 | export function generateStaticParams() {
9 | return questions.map((_, index) => ({ step: String(index + 1) }));
10 | }
11 |
12 | interface Props {
13 | params: Promise<{ step: string }>;
14 | }
15 |
16 | export default async function QuizPage({ params }: Props) {
17 | const step = Number((await params).step);
18 | const currentStep = step - 1;
19 | const currentQuestion = questions[currentStep];
20 |
21 | if (!currentQuestion) return notFound();
22 |
23 | return (
24 | <>
25 | (s === 0 ? "/" : `/quiz/${s}`)}
27 | getLabel={(s) => `Question ${s}/${questions.length}`}
28 | step={step}
29 | />
30 |
31 | }
34 | step={step}
35 | isLastQuestion={step === questions.length}
36 | options={currentQuestion.options.map((option) => ({
37 | id: option.id,
38 | text: ,
39 | }))}
40 | />
41 | >
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/questions/question-6/index.tsx:
--------------------------------------------------------------------------------
1 | import { codeBlock } from "@app/mdx/code-block";
2 | import { compileMDX } from "@app/mdx/compileMdx";
3 |
4 | import type { Question } from "../Question";
5 |
6 | import { IDontKnow } from "../shared-options";
7 | import explanation from "./explanation.mdx";
8 | import inquiry from "./inquiry.mdx";
9 |
10 | export default {
11 | id: 6,
12 | inquiry,
13 | options: [
14 | {
15 | id: 1,
16 | text: await compileMDX(
17 | codeBlock`
18 |
19 | The old price was $49.99, but now it's just $29.99
20 |
31 | ),
32 | hr: () => ,
33 | H: ({ children }) => {
34 | return {children};
35 | },
36 | code: (props) => ,
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 |
3 | import type { Metadata, Viewport } from "next";
4 |
5 | import { PostHogProvider } from "@app/posthog/PostHogProvider";
6 | import { Fira_Mono as FiraMono, Ubuntu } from "next/font/google";
7 |
8 | const inter = Ubuntu({
9 | variable: "--font-inter",
10 | subsets: ["latin"],
11 | weight: ["400", "700"],
12 | style: ["italic", "normal"],
13 | });
14 |
15 | const firaMono = FiraMono({
16 | variable: "--font-fira-mono",
17 | subsets: ["latin"],
18 | weight: ["500"],
19 | });
20 |
21 | const title = "You don't know html";
22 | const description = "If you think you know HTML, think again.";
23 | const images = { url: "/og.png", alt: title };
24 |
25 | export const metadata: Metadata = {
26 | title,
27 | description,
28 | keywords: ["html", "quiz", "challenge"],
29 | metadataBase: new URL("https://youdontknowhtml.com"),
30 | openGraph: {
31 | description,
32 | images,
33 | title,
34 | },
35 | twitter: {
36 | images,
37 | title,
38 | card: "summary_large_image",
39 | },
40 | };
41 |
42 | export const viewport: Viewport = {
43 | themeColor: "#23252e",
44 | colorScheme: "dark",
45 | };
46 |
47 | export default function RootLayout({ children }: React.PropsWithChildren) {
48 | return (
49 |
53 |
54 | {children}
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/questions/question-12/explanation.mdx:
--------------------------------------------------------------------------------
1 | > Certain tags can be omitted.[Spec Reference](https://html.spec.whatwg.org/multipage/syntax.html#optional-tags)
2 | >
3 | > - A `caption` element's end tag may be omitted if the caption element is not immediately followed by ASCII whitespace or a comment.
4 | > - A `colgroup` element's end tag may be omitted if the colgroup element is not immediately followed by ASCII whitespace or a comment.
5 | > - A `tr` element's end tag may be omitted if the tr element is immediately followed by another `tr` element, or if there is no more content in the parent element.
6 | > - A `th` element's end tag may be omitted if the th element is immediately followed by a `td` or `th` element, or if there is no more content in the parent element.
7 | > - A `td` element's end tag may be omitted if the td element is immediately followed by a `td` or `th` element, or if there is no more content in the parent element.
8 |
9 | > `col` is a void element, so it doesn't need a closing tag. [Spec Reference](https://html.spec.whatwg.org/multipage/tables.html#the-col-element)
10 |
11 | So the code is a valid HTML syntax and is equivalent to:
12 |
13 | ```html
14 |
54 | {children}
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |
4 | # You don't know HTML
5 |
6 | Think you know HTML? [Take the test](https://youdontknowhtml.com)! This interactive quiz challenges your
7 | understanding of HTML language.
8 |
9 | ## Contributing
10 |
11 | ### Prerequisites
12 |
13 | - Node.js (version specified in package.json)
14 | - pnpm (version specified in package.json)
15 | - Docker
16 |
17 | ### Development
18 |
19 | 1. Clone the repository:
20 | 2. Install dependencies:
21 | 3. Start the Supabase server
22 | ```bash
23 | npm run supabase:start
24 | ```
25 | 4. Set up environment variables:
26 | ```bash
27 | cp .env.example .env
28 | ```
29 | 5. Set variables for your local Supabase instance.
30 | 6. Start the development server:
31 | ```bash
32 | pnpm dev
33 | ```
34 |
35 | ## Making new changes
36 |
37 | Contributions are welcome! Whether it's adding new questions, improving
38 | explanations, or fixing bugs, please feel free to submit a pull request.
39 |
40 | 1. Fork the repository
41 | 2. Create your feature branch (`git checkout -b feature/amazing-question`)
42 | 3. Commit your changes (`git commit -m 'feat: add a new question'`)
43 | 4. Open a Pull Request
44 |
45 | ## Tech Stack
46 |
47 | - [Next.js](https://nextjs.org/) - React framework
48 | - [Tailwind CSS](https://tailwindcss.com/) - Styling
49 | - [MDX](https://mdxjs.com/) - Content
50 | - [Supabase](https://supabase.com/) - Backend
51 |
--------------------------------------------------------------------------------
/src/app/summary/_components/answers-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { Question } from "@app/questions/Question";
4 |
5 | import { useUserAnswers } from "@app/state/useAnswers";
6 | import { isNull } from "@fullstacksjs/toolbox";
7 | import { createContext, useEffect, useMemo, useState } from "react";
8 | import { useIsClient } from "usehooks-ts";
9 |
10 | export const AnswersContext = createContext<{
11 | correctAnswers: number;
12 | loading: boolean;
13 | total: number;
14 | }>({
15 | correctAnswers: 0,
16 | total: 0,
17 | loading: true,
18 | });
19 | AnswersContext.displayName = "AnswersContext";
20 |
21 | export const AnswersProvider = ({
22 | children,
23 | questions,
24 | }: {
25 | children: React.ReactNode;
26 | questions: Pick[];
27 | }) => {
28 | const isClient = useIsClient();
29 | const answers = useUserAnswers();
30 | const [correctAnswers, setCorrectAnswers] = useState();
31 | const loading = !isClient || isNull(correctAnswers);
32 |
33 | useEffect(() => {
34 | // eslint-disable-next-line react-hooks/set-state-in-effect, @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
35 | setCorrectAnswers(
36 | questions.filter((q) => answers[q.id] === q.correctAnswerId).length,
37 | );
38 | }, [answers, questions]);
39 |
40 | const value = useMemo(
41 | () => ({
42 | correctAnswers: correctAnswers ?? 0,
43 | loading,
44 | total: questions.length,
45 | }),
46 | [correctAnswers, loading, questions.length],
47 | );
48 |
49 | return {children};
50 | };
51 |
--------------------------------------------------------------------------------
/src/questions/question-9/explanation.mdx:
--------------------------------------------------------------------------------
1 | This one is a bit tricky, but here's the breakdown:
2 |
3 | ### Understanding “normal elements” and end tag omission
4 |
5 | In HTML, elements like {'
'} are considered normal elements. According to the spec:
6 |
7 | > The start and end tags of certain normal elements can be omitted. [Spec reference](https://html.spec.whatwg.org/multipage/syntax.html#normal-elements)
8 |
9 | However, for {'
'}, the element definition clearly states:
10 |
11 | > Tag omission in text/html:
12 | >
13 | > Neither tag is omissible. [Spec reference](https://html.spec.whatwg.org/multipage/grouping-content.html#the-div-element)
14 |
15 | So, semantically valid HTML must include both {'
'} and {'
'}.
16 |
17 | ### But why does it still work in browsers?
18 |
19 | This is where the HTML parser spec comes in — particularly the ["in body" insertion mode](https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody). This defines how HTML parsers should behave.
20 |
21 | From the spec:
22 |
23 | > An end tag whose tag name is one of: `div`, `section`, `article` ...
24 | >
25 | > **If there is no matching open element on the stack**
26 | >
27 | > It's a parse error and the token is ignored.
28 | >
29 | > **Otherwise:**
30 | >
31 | > Generate implied end tags.
32 |
33 | In short, the parser will generate implied end tags for any unclosed normal elements.
34 |
35 | ```html
36 |