You solved all the problems in CSS Grid Mastery Quiz.
22 |
41 | おめでとうございます! CSS Grid Mastery
42 | Quizの全ての問題をクリアしました。
43 |
44 | = ({
11 | children,
12 | onExtend,
13 | }) => {
14 | return (
15 |
16 |
17 |
onExtend("top")}
21 | >
22 |
23 |
24 |
25 |
26 |
onExtend("right")}
30 | >
31 |
32 |
33 |
34 |
35 |
onExtend("bottom")}
39 | >
40 |
41 |
42 |
43 |
44 |
onExtend("left")}
48 | >
49 |
50 |
51 |
52 |
{children}
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/questions/v1/tutorial/Tutorial1.tsx:
--------------------------------------------------------------------------------
1 | import { GridArea } from "../../../pages/QuestionPage/components/GridArea";
2 | import { useGridItemSelection } from "../../../pages/QuestionPage/logic/useGridItemSelection";
3 | import { useI18n } from "../../../utils/i18n/LanguageContext";
4 | import classes from "./Tutorial.module.css";
5 |
6 | export const Tutorial1: React.VFC = () => {
7 | const { gridDef, selectedItems, toggleItem } = useSampleGrid();
8 |
9 | const code1 = (
10 |
11 |
12 | {`
13 |
16 | `.trim()}
17 |
18 |
19 | );
20 | const code2 = (
21 |
22 |
23 | {`
24 | .grid-container {
25 | display: grid;
26 | grid-template-columns: 1fr 1fr;
27 | grid-template-rows: 1fr 1fr;
28 | }
29 | .grid-item {
30 | grid-column: 1;
31 | grid-row: 1;
32 | }
33 | `.trim()}
34 |
35 |
36 | );
37 | const sample = (
38 |
39 |
45 |
46 | );
47 |
48 | const contents = useI18n({
49 | en: (
50 | <>
51 | Tutorial #1
52 | Consider the following HTML structure:
53 | {code1}
54 | You are given style definitions for above elements. Example:
55 | {code2}
56 |
57 | Click/tap all grid cells occupied by the .grid-item{" "}
58 | element. Press “Check” button to check your answer.
59 |
60 | Sample:
61 | {sample}
62 | After making three mistakes, a “Cheat” button appears.
63 | >
64 | ),
65 | ja: (
66 | <>
67 | チュートリアル #1
68 | ここでは、次のHTML構造を考えます。
69 | {code1}
70 | これに対して、次の例のようにスタイル定義が与えられます。
71 | {code2}
72 |
73 | .grid-item
74 | 要素が占めるすべてのグリッドセルをクリック/タップしてください。
75 | その後“Check”ボタンを押して答えが合っているか確かめましょう。
76 |
77 | 例:
78 | {sample}
79 | 3回間違えると“Cheat”ボタンが出現します。
80 | >
81 | ),
82 | });
83 |
84 | return {contents} ;
85 | };
86 |
87 | function useSampleGrid() {
88 | const gridDef = {
89 | columns: 2,
90 | rows: 2,
91 | };
92 | const { selectedItems, toggleItem } = useGridItemSelection([], ["1,1"]);
93 |
94 | return {
95 | gridDef,
96 | selectedItems,
97 | toggleItem,
98 | };
99 | }
100 |
--------------------------------------------------------------------------------
/src/questions/v1/tutorial/Tutorial2.tsx:
--------------------------------------------------------------------------------
1 | import { GridArea } from "../../../pages/QuestionPage/components/GridArea";
2 | import { GridAreaExtensionControl } from "../../../pages/QuestionPage/components/GridAreaExtensionControl";
3 | import { useGridExtension } from "../../../pages/QuestionPage/logic/useGridExtension";
4 | import { useGridItemSelection } from "../../../pages/QuestionPage/logic/useGridItemSelection";
5 | import { useI18n } from "../../../utils/i18n/LanguageContext";
6 | import classes from "./Tutorial.module.css";
7 |
8 | export const Tutorial2: React.VFC = () => {
9 | const { gridDef, selectedItems, extension, toggleItem, extend } =
10 | useSampleGrid();
11 |
12 | const sample = (
13 |
14 |
15 |
26 |
27 |
28 | );
29 |
30 | const contents = useI18n({
31 | en: (
32 | <>
33 | Tutorial #2
34 |
35 | When grid items are placed outside of the grid area explicitly deined
36 | by grid-template-columns and{" "}
37 | grid-template-rows,implicit grid tracks and{" "}
38 | implicit grid lines are generated outside of explicit grid
39 | tracks.
40 |
41 |
42 | From now on, the correct answer may enter those implicit grid cells.
43 |
44 |
45 | To specify such cells, you need to extend the displayed grid
46 | by pressing the plus buttons placed at each edge.
47 |
48 |
49 | To remove extended cells, you need to reset the state of the grid by
50 | pressing “Reset” button.
51 |
52 | Sample:
53 | {sample}
54 | >
55 | ),
56 | ja: (
57 | <>
58 | チュートリアル #2
59 |
60 | グリッドアイテムがgrid-template-columnsおよび
61 | grid-template-rows
62 | によって明示的に定義されたグリッドエリアの外に配置された場合、
63 | 明示的なグリッドエリアの外側に暗黙のグリッドトラック
64 | が生成されます。
65 |
66 |
67 | これからは、正しい答えが初期表示されているグリッドの範囲を超えて暗黙のグリッドトラックにまたがる可能性があります。
68 |
69 |
70 | そのような回答をするためには、表示されているグリッドを、各辺に配置されたプラスボタンを押すことで拡張する必要があります。
71 |
72 |
73 | 拡張したグリッドセルを元に戻すには、“Reset”ボタンを押して画面を初期化する必要があります。
74 |
75 | 拡張できるグリッドセルのサンプル:
76 | {sample}
77 | >
78 | ),
79 | });
80 | return {contents} ;
81 | };
82 |
83 | function useSampleGrid() {
84 | const gridDef = {
85 | columns: 2,
86 | rows: 2,
87 | };
88 | const { selectedItems, toggleItem } = useGridItemSelection([]);
89 | const { extension, extend } = useGridExtension([]);
90 |
91 | return {
92 | gridDef,
93 | selectedItems,
94 | extension,
95 | toggleItem,
96 | extend,
97 | };
98 | }
99 |
--------------------------------------------------------------------------------
/src/pages/TopPage/index.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { Link } from "react-location";
3 | import { appUrl } from "../../const";
4 | import { useI18n } from "../../utils/i18n/LanguageContext";
5 | import logoImage from "./logo.png";
6 | import classes from "./TopPage.module.css";
7 |
8 | export const TopPage: React.VFC = () => {
9 | const shareOnTwitterlink = useMemo(() => {
10 | return `https://twitter.com/intent/tweet?text=${encodeURIComponent(
11 | "CSS Grid Mastery Quiz\n"
12 | )}&url=${encodeURIComponent(appUrl)}`;
13 | }, []);
14 |
15 | const langs = useI18n({
16 | en: {
17 | intro: (
18 | <>
19 |
20 | Learn how CSS Grid works through a number of CSS Grid questions!
21 |
22 |
23 | With the current version, you can learn how grid placement
24 | properties work.
25 |
26 | >
27 | ),
28 | proceed: "Proceed to Tutorial",
29 | progress: (
30 | <>
31 | How do I save my progress?
32 |
33 | To save your progress, just save current URL. To continue, open that
34 | URL and go on!
35 |
36 | >
37 | ),
38 | },
39 | ja: {
40 | intro: (
41 | <>
42 | 数々のCSS Gridの問題を解いてCSS Gridの仕組みを学習しよう!
43 |
44 | 現在のバージョンでは、グリッドアイテムの配置にかかわるプロパティの挙動を学ぶことができます。
45 |
46 | >
47 | ),
48 | proceed: "チュートリアルに進む",
49 | progress: (
50 | <>
51 | 進捗を保存する方法
52 |
53 | 現在の進捗を保存するには、現在のURLを保存しておくだけで構いません。そのURLを開けば再開できます。
54 |
55 | >
56 | ),
57 | },
58 | });
59 |
60 | return (
61 |
116 | );
117 | };
118 |
--------------------------------------------------------------------------------
/src/pages/QuestionPage/components/GridArea.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 | import { GridPosition } from "../../../questions/GridPosition";
3 | import { range } from "../../../utils/range";
4 | import { GridExtensionState } from "../logic/useGridExtension";
5 | import classes from "./GridArea.module.css";
6 |
7 | type GridDef = { rows: number; columns: number };
8 |
9 | type SubgridDef = {
10 | style: React.CSSProperties;
11 | };
12 |
13 | type PropsBase = {
14 | gridDef: GridDef;
15 | className?: string;
16 | style?: React.CSSProperties;
17 | children?: React.ReactNode;
18 | };
19 |
20 | type PropsHasGrid = PropsBase & {
21 | hasGrid: true;
22 | toggleItem: (column: number, row: number) => void;
23 | selectedItems: readonly GridPosition[];
24 | extension?: GridExtensionState;
25 | };
26 |
27 | type PropsNoGrid = PropsBase & {
28 | hasGrid?: false;
29 | };
30 |
31 | type Props = PropsHasGrid | PropsNoGrid;
32 |
33 | export const GridArea: React.VFC = (props) => {
34 | const { hasGrid, gridDef, className, style, children } = props;
35 |
36 | const extension = (hasGrid && props.extension) || {
37 | top: 0,
38 | right: 0,
39 | bottom: 0,
40 | left: 0,
41 | };
42 | const gridContents = hasGrid ? (
43 | <>
44 | {Array.from(
45 | range(1 - extension.top, gridDef.rows + extension.bottom + 1)
46 | ).map((row) => (
47 |
48 | {Array.from(
49 | range(1 - extension.left, gridDef.columns + extension.right + 1)
50 | ).map((column) => {
51 | const isSelected = props.selectedItems.includes(`${column},${row}`);
52 | return (
53 | props.toggleItem(column, row)}
62 | aria-label={`(${column}, ${row})`}
63 | >
64 | {isInGrid(column, row, gridDef) ? `(${column}, ${row})` : null}
65 |
66 | );
67 | })}
68 |
69 | ))}
70 | >
71 | ) : null;
72 |
73 | return (
74 |
82 | {gridContents}
83 | {children}
84 |
85 | );
86 | };
87 |
88 | function isInGrid(column: number, row: number, gridDef: GridDef) {
89 | return (
90 | column >= 1 && column <= gridDef.columns && row >= 1 && row <= gridDef.rows
91 | );
92 | }
93 |
94 | function getGridPlacelement(
95 | column: number,
96 | row: number,
97 | gridDef: GridDef
98 | ): React.CSSProperties {
99 | const gridRow =
100 | 1 <= row
101 | ? `${row}`
102 | : // 0 -> -(gridDef.rows+2), -1 -> -(gridDef.rows+3) ...
103 | `${-gridDef.rows + row - 2}`;
104 |
105 | const gridColumn =
106 | 1 <= column ? `${column}` : `${-gridDef.columns + column - 2}`;
107 | return {
108 | gridRow,
109 | gridColumn,
110 | };
111 | }
112 |
--------------------------------------------------------------------------------
/src/questions/v1/tutorial/Tutorial3.tsx:
--------------------------------------------------------------------------------
1 | import { GridArea } from "../../../pages/QuestionPage/components/GridArea";
2 | import { useGridItemSelection } from "../../../pages/QuestionPage/logic/useGridItemSelection";
3 | import { useI18n } from "../../../utils/i18n/LanguageContext";
4 | import classes from "./Tutorial.module.css";
5 |
6 | export const Tutorial3: React.VFC = () => {
7 | const { gridDef, selectedItems, toggleItem } = useSampleGrid();
8 |
9 | const code1 = (
10 |
11 |
12 | {`
13 |
18 | `.trim()}
19 |
20 |
21 | );
22 |
23 | const code2 = (
24 |
25 |
26 | {`
27 | .grid-container {
28 | display: grid;
29 | grid-template-columns: 1fr 1fr 1fr;
30 | grid-template-rows: 1fr 1fr 1fr;
31 | }
32 | .subgrid {
33 | grid-column: 2 / 4;
34 | grid-rows: 1 / 3;
35 | display: grid;
36 | grid-template-columns: subgrid;
37 | grid-template-rows: subgrid;
38 | }
39 | .grid-item {
40 | grid-column: 1;
41 | grid-row: 1;
42 | }
43 | `.trim()}
44 |
45 |
46 | );
47 |
48 | const sample = (
49 |
50 |
60 |
61 | );
62 |
63 | const contents = useI18n({
64 | en: (
65 | <>
66 | Tutorial #3
67 |
68 | A recent version of CSS Grid has the concept of subgrid . A
69 | subgrid itself is a grid item of the parent grid container and also
70 | has its own grid in it. An axis declared as subgrid inherit grid lines
71 | from the parent grid, still maintaining its own coordinate system.
72 |
73 | From now on, let's consider the following HTML structure:
74 | {code1}
75 |
76 | Now you are given style definitions for these three elements. Example:
77 |
78 | {code2}
79 |
80 | You still need to answer the grid cells occupied by the element at the
81 | bottom of tree, namely .grid-item one. Note that the
82 | coordinates displayed in the answering grid is ones of the parent grid
83 | container (.grid-container).
84 |
85 | Sample (answer for the above style):
86 | {sample}
87 | >
88 | ),
89 | ja: (
90 | <>
91 | チュートリアル #3
92 |
93 | 最近のバージョンのCSS Gridには、サブグリッド
94 | という概念があります。サブグリッドは、親のグリッドに
95 | 属するグリッドアイテムでありつつ、自身もグリッドコンテナです。
96 | サブグリッドとして宣言された軸は、親のグリッドトラックをそのまま継承しますが、独自の座標軸を持っています。
97 |
98 | 今後の問題では、次のようなHTML構造を考えます。
99 | {code1}
100 |
101 | そして、次の例のように、これら3つの要素に対するスタイルが与えられます。
102 |
103 | {code2}
104 |
105 | これまで通り、ツリーの末端にある.grid-item
106 | 要素が占めるグリッドセルを解答してください。解答用のグリッドに表示されている座標は親グリッド(
107 | .grid-container)のものである点に注意してください。
108 |
109 | 例(上記の例に対する解答):
110 | {sample}
111 | >
112 | ),
113 | });
114 |
115 | return {contents} ;
116 | };
117 |
118 | function useSampleGrid() {
119 | const gridDef = {
120 | columns: 3,
121 | rows: 3,
122 | };
123 | const { selectedItems, toggleItem } = useGridItemSelection([], ["2,1"]);
124 |
125 | return {
126 | gridDef,
127 | selectedItems,
128 | toggleItem,
129 | };
130 | }
131 |
--------------------------------------------------------------------------------
/src/pages/QuestionPage/QuestionPage.module.css:
--------------------------------------------------------------------------------
1 | .page {
2 | max-width: 1024px;
3 | margin: 0 auto;
4 | display: grid;
5 | grid-template:
6 | "main title" auto
7 | "main code" 1fr
8 | "cont cont" 80px / 1fr auto;
9 |
10 | --grid-bg-color: #ffffff;
11 | --grid-gap-color: #222222;
12 | --grid-fg-color: #666666;
13 | --code-bg-color: #d3d3d3;
14 | --code-fg-color: black;
15 | --cont-bg-color: #e8e8e8;
16 | --grid-active-bg-color: #88ddff;
17 | --grid-active-fg-color: #2233ff;
18 |
19 | --grid-font-size: 24px;
20 | }
21 |
22 | @media (max-width: 700px) {
23 | .page {
24 | grid-template:
25 | "title" auto
26 | "code" auto
27 | "main" auto
28 | "cont" 80px / 1fr;
29 | }
30 | .page code {
31 | font-size: 1em;
32 | }
33 | }
34 |
35 | @media (max-width: 750px), (max-height: 460px) {
36 | .page {
37 | --grid-font-size: 18px;
38 | }
39 | }
40 |
41 | @media (prefers-color-scheme: dark) {
42 | .page {
43 | --grid-bg-color: #222222;
44 | --grid-gap-color: #ffffff;
45 | --grid-fg-color: #aaaaaa;
46 | --cont-bg-color: #292929;
47 | --code-bg-color: #494949;
48 | --code-fg-color: white;
49 | --grid-active-bg-color: #086f98;
50 | --grid-active-fg-color: #dddeeb;
51 | }
52 | }
53 |
54 | .titleArea {
55 | grid-area: title;
56 | margin: 0;
57 | height: 40px;
58 | padding: 0 0.5em;
59 | display: flex;
60 | flex-flow: column nowrap;
61 | justify-content: center;
62 | background-color: #eeffee;
63 | color: black;
64 | font-size: 1.2em;
65 | }
66 |
67 | .mainArea {
68 | grid-area: main;
69 | }
70 |
71 | .mainGridContainer {
72 | max-width: calc(100vh - 80px);
73 | max-height: calc(100vh - 80px);
74 | margin: 0 0 0 auto;
75 | display: grid;
76 | grid-template: auto 1fr / 1fr;
77 | z-index: 0;
78 | }
79 |
80 | .mainGrid {
81 | grid-area: 1 / 1;
82 | }
83 |
84 | .cheatGrid {
85 | grid-area: 1 / 1;
86 | z-index: 1;
87 | pointer-events: none;
88 | }
89 |
90 | .defs {
91 | max-height: calc(100vh - 120px);
92 | grid-area: code;
93 | background-color: var(--code-bg-color);
94 | color: var(--code-fg-color);
95 | overflow: auto;
96 | scrollbar-gutter: stable;
97 | }
98 |
99 | .eachDef {
100 | padding: 0.5em;
101 | }
102 |
103 | .normalItem, .selectedItem {
104 | display: flex;
105 | flex-flow: row nowrap;
106 | align-items: center;
107 | justify-content: center;
108 | background-color: var(--grid-bg-color);
109 | color: var(--grid-fg-color);
110 | font-size: var(--grid-font-size);
111 | }
112 |
113 | .cheatItem {
114 | background-color: #ffff44;
115 | opacity: 0.5;
116 | }
117 |
118 | .cheatSubgrid {
119 | display: grid;
120 | background-color: #ffbbbb;
121 | opacity: 0.2;
122 | }
123 |
124 | .controlGrid {
125 | grid-area: cont;
126 | background-size: auto auto;
127 | background-color: var(--body-bg-color);
128 | background-image: repeating-linear-gradient(120deg, transparent, transparent 25px, var(--cont-bg-color) 25px, var(--cont-bg-color) 40px );
129 | display: grid;
130 | grid-template-columns: 1fr auto auto auto;
131 | align-items: center;
132 | padding: 0 1em;
133 | gap: 1em;
134 | }
135 |
136 | .controlGrid.hasCheatNote {
137 | grid-template-rows: auto auto;
138 | }
139 |
140 | .controlGrid :is(a, button) {
141 | display: inline-block;
142 | width: max-content;
143 | border-radius: 0.75em;
144 | padding: 0.25em 0.5em;
145 | background-color: #888888;
146 | color: white;
147 | font-weight: bold;
148 | text-decoration: none;
149 | font-size: calc(10px + 2vmin);
150 | }
151 |
152 | .controlGrid button.goToTop {
153 | justify-self: start;
154 | }
155 |
156 | .controlGrid button.check {
157 | background-color: #0cb826;
158 | }
159 |
160 | .controlGrid button.cheat {
161 | background-color: #eebc18;
162 | color: black;
163 | }
164 |
165 | .controlGrid button.wrong {
166 | background-color: #1d6fb6;
167 | }
168 |
169 | .controlGrid .cheatNotice {
170 | grid-column: 1 / -1;
171 | grid-row: 2;
172 | margin: 0;
173 | font-size: 0.9em;
174 | }
175 |
176 | .tutorialArea {
177 | grid-row: main;
178 | grid-column: 1 / -1;
179 | padding: 1em;
180 | }
--------------------------------------------------------------------------------
/src/pages/QuestionPage/logic/useQuizPageLogic.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo, useReducer } from "react";
2 | import { useNavigate } from "react-location";
3 | import { GridPosition } from "../../../questions/GridPosition";
4 | import { QuizData } from "../../../questions/QuestionData";
5 | import { useStateReset } from "../../../utils/hooks/useStateReset";
6 | import { useNextPage } from "../hooks/useNextPage";
7 | import {
8 | EdgeDirection,
9 | GridExtensionState,
10 | useGridExtension,
11 | } from "./useGridExtension";
12 | import { useGridItemSelection } from "./useGridItemSelection";
13 |
14 | export type ButtonState = "check" | "correct" | "wrong";
15 |
16 | export function useQuizPageLogic(
17 | quizId: string,
18 | data: QuizData,
19 | isCheat: boolean
20 | ) {
21 | const { goToNextPage } = useNextPage(quizId);
22 | const navigate = useNavigate();
23 | const [resetCount, reset] = useReducer((c: number) => c + 1, 0);
24 | const [wrongCount, addWrong] = useStateReset([quizId], () => 0);
25 | const { selectedItems, toggleItem } = useGridItemSelection([
26 | quizId,
27 | resetCount,
28 | ]);
29 | const { extension, extend: extendGrid } = useGridExtension([
30 | quizId,
31 | resetCount,
32 | ]);
33 |
34 | // if cheat is enabled but extension is not enough, extend
35 | useAutoExtension(extension, data.gridDef, extendGrid, data.answer, isCheat);
36 |
37 | const [buttonState, setButtonState] = useStateReset(
38 | [quizId],
39 | () => "check"
40 | );
41 |
42 | const check = useCallback(() => {
43 | if (checkAnswer(selectedItems, data.answer)) {
44 | setButtonState("correct");
45 | } else {
46 | setButtonState("wrong");
47 | }
48 | }, [selectedItems, data]);
49 |
50 | useEffect(() => {
51 | if (buttonState === "correct") {
52 | const timer = setTimeout(() => {
53 | goToNextPage();
54 | }, 300);
55 | return () => clearTimeout(timer);
56 | }
57 | if (buttonState === "wrong") {
58 | addWrong((c) => c + 1);
59 | const timer = setTimeout(() => {
60 | setButtonState("check");
61 | }, 2500);
62 | return () => clearTimeout(timer);
63 | }
64 | return undefined;
65 | }, [buttonState]);
66 |
67 | const getCheat = useMemo(() => {
68 | if (wrongCount < 3) {
69 | // no cheat available yet
70 | return undefined;
71 | }
72 | return () => {
73 | navigate({
74 | search: (old) =>
75 | isCheat
76 | ? { lang: old?.lang }
77 | : {
78 | cheat: "1",
79 | lang: old?.lang,
80 | },
81 | });
82 | };
83 | }, [wrongCount, isCheat]);
84 |
85 | return {
86 | selectedItems,
87 | toggleItem,
88 | extension,
89 | extendGrid,
90 | buttonState,
91 | check,
92 | reset,
93 | getCheat,
94 | };
95 | }
96 |
97 | function checkAnswer(
98 | selectedItems: readonly GridPosition[],
99 | answer: readonly GridPosition[]
100 | ) {
101 | // TODO: O(N^2)
102 | return (
103 | selectedItems.length === answer.length &&
104 | selectedItems.every((item) => answer.includes(item))
105 | );
106 | }
107 |
108 | function useAutoExtension(
109 | extension: GridExtensionState,
110 | gridDef: { rows: number; columns: number },
111 | extendGrid: (direction: EdgeDirection, amount: number) => void,
112 | answer: readonly GridPosition[],
113 | isCheat: boolean
114 | ) {
115 | useEffect(() => {
116 | if (!isCheat) {
117 | return;
118 | }
119 | let topEdge = 1;
120 | let rightEdge = gridDef.columns;
121 | let bottomEdge = gridDef.rows;
122 | let leftEdge = 1;
123 |
124 | for (const item of answer) {
125 | const [column, row] = item.split(",").map(Number);
126 | if (column < leftEdge) {
127 | leftEdge = column;
128 | }
129 | if (column > rightEdge) {
130 | rightEdge = column;
131 | }
132 | if (row < topEdge) {
133 | topEdge = row;
134 | }
135 | if (row > bottomEdge) {
136 | bottomEdge = row;
137 | }
138 | }
139 | const leftExtension = 1 - leftEdge;
140 | if (extension.left < leftExtension) {
141 | extendGrid("left", leftExtension - extension.left);
142 | }
143 | const rightExtension = rightEdge - gridDef.columns;
144 | if (extension.right < rightExtension) {
145 | extendGrid("right", rightExtension - extension.right);
146 | }
147 | const topExtension = 1 - topEdge;
148 | if (extension.top < topExtension) {
149 | extendGrid("top", topExtension - extension.top);
150 | }
151 | const bottomExtension = bottomEdge - gridDef.rows;
152 | if (extension.bottom < bottomExtension) {
153 | extendGrid("bottom", bottomExtension - extension.bottom);
154 | }
155 | }, [isCheat, answer, extension, gridDef]);
156 | }
157 |
--------------------------------------------------------------------------------
/src/pages/QuestionPage/QuizPage/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useEffect, useMemo, useRef } from "react";
2 | import { Link } from "react-location";
3 | import { QuizData } from "../../../questions/QuestionData";
4 | import { useI18n } from "../../../utils/i18n/LanguageContext";
5 | import { indent } from "../../../utils/indent";
6 | import { simpleParseCss } from "../../../utils/simpleParseCss";
7 | import { GridArea } from "../components/GridArea";
8 | import { GridAreaExtensionControl } from "../components/GridAreaExtensionControl";
9 | import { ButtonState, useQuizPageLogic } from "../logic/useQuizPageLogic";
10 | import classes from "../QuestionPage.module.css";
11 |
12 | type Props = {
13 | quizId: string;
14 | quizData: QuizData;
15 | cheat: boolean;
16 | };
17 |
18 | export const QuizPage: React.VFC = ({ quizId, quizData, cheat }) => {
19 | const { gridStyle, subgridStyle, itemStyle, gridDef, extensible } = quizData;
20 | const pageTitleRef = useRef(null);
21 |
22 | useEffect(() => {
23 | // a11y
24 | pageTitleRef.current?.focus();
25 | }, [quizId]);
26 |
27 | const { gridStyleDisp, gridStyleObj } = useMemo(
28 | () => ({
29 | gridStyleDisp: `.grid-container {
30 | display: grid;
31 | ${indent(gridStyle)}
32 | }`,
33 | gridStyleObj: simpleParseCss(gridStyle),
34 | }),
35 | [gridStyle]
36 | );
37 | const { subgridStyleDisp, subgridStyleObj } = useMemo(() => {
38 | if (!subgridStyle) {
39 | return { subgridStyleDisp: undefined, subgridStyleObj: undefined };
40 | }
41 | return {
42 | subgridStyleDisp: `.subgrid {
43 | display: grid;
44 | ${indent(subgridStyle)}
45 | }`,
46 | subgridStyleObj: simpleParseCss(subgridStyle),
47 | };
48 | }, [subgridStyle]);
49 | const { itemStyleDisp, itemStyleObj } = useMemo(
50 | () => ({
51 | itemStyleDisp: `.grid-item {
52 | ${indent(itemStyle)}
53 | }`,
54 | itemStyleObj: simpleParseCss(itemStyle),
55 | }),
56 | [itemStyle]
57 | );
58 |
59 | const {
60 | selectedItems,
61 | buttonState,
62 | extension,
63 | toggleItem,
64 | extendGrid,
65 | check,
66 | reset,
67 | getCheat,
68 | } = useQuizPageLogic(quizId, quizData, cheat);
69 |
70 | const mainGrid = (
71 |
72 |
81 | {cheat && (
82 |
88 | {subgridStyleObj ? (
89 |
92 | ) : (
93 |
94 | )}
95 |
96 | )}
97 |
98 | );
99 |
100 | return (
101 |
102 |
108 | Page {quizId}
109 |
110 |
115 |
116 | {extensible ? (
117 |
118 | {mainGrid}
119 |
120 | ) : (
121 | mainGrid
122 | )}
123 |
124 |
132 |
133 | );
134 | };
135 |
136 | const GridDefs: React.VFC<{
137 | gridStyleDisp: string;
138 | subgridStyleDisp: string | undefined;
139 | itemStyleDisp: string;
140 | }> = memo(({ gridStyleDisp, subgridStyleDisp, itemStyleDisp }) => {
141 | return (
142 |
143 |
144 | {gridStyleDisp}
145 |
146 | {subgridStyleDisp ? (
147 |
148 | {subgridStyleDisp}
149 |
150 | ) : null}
151 |
152 | {itemStyleDisp}
153 |
154 |
155 | );
156 | });
157 |
158 | const ControlGrid: React.VFC<{
159 | buttonState: ButtonState;
160 | isCheat: boolean;
161 | isSubgrid: boolean;
162 | reset: () => void;
163 | check: () => void;
164 | getCheat: (() => void) | undefined;
165 | }> = memo(({ buttonState, isCheat, isSubgrid, reset, check, getCheat }) => {
166 | const langs = useI18n({
167 | en: {
168 | goToTop: "Go to Top",
169 | correct: "Correct!",
170 | wrong: "Wrong…",
171 | subgridCheatNote:
172 | "Note: cheat for subgrids only works for browsers that support subgrid.",
173 | },
174 | ja: {
175 | goToTop: "トップへ",
176 | correct: "正解!",
177 | wrong: "不正解…",
178 | subgridCheatNote:
179 | "注意:subgridに対するチートはブラウザがsubgridをサポートしている場合のみ正常に動作します。",
180 | },
181 | });
182 | return (
183 |
184 |
185 | {langs.goToTop}
186 |
187 | {getCheat !== undefined ? (
188 |
189 | Cheat
190 |
191 | ) : (
192 |
193 | )}
194 |
Reset
195 | {buttonState === "check" ? (
196 |
197 | Check
198 |
199 | ) : buttonState === "correct" ? (
200 |
201 | {langs.correct}
202 |
203 | ) : buttonState === "wrong" ? (
204 |
205 | {langs.wrong}
206 |
207 | ) : null}
208 | {isCheat && isSubgrid ? (
209 |
{langs.subgridCheatNote}
210 | ) : null}
211 |
212 | );
213 | });
214 |
--------------------------------------------------------------------------------