├── .prettierrc ├── src ├── vite-env.d.ts ├── const.ts ├── questions │ ├── GridPosition.ts │ ├── v1 │ │ ├── 1.tsx │ │ ├── 26.tsx │ │ ├── 39.tsx │ │ ├── 53.tsx │ │ ├── tutorial │ │ │ ├── Tutorial.module.css │ │ │ ├── Congratulations.tsx │ │ │ ├── Tutorial1.tsx │ │ │ ├── Tutorial2.tsx │ │ │ └── Tutorial3.tsx │ │ ├── 2.tsx │ │ ├── 7.tsx │ │ ├── 8.tsx │ │ ├── 3.tsx │ │ ├── 4.tsx │ │ ├── 29.tsx │ │ ├── 5.tsx │ │ ├── 27.tsx │ │ ├── 9.tsx │ │ ├── 10.tsx │ │ ├── 6.tsx │ │ ├── 21.tsx │ │ ├── 17.tsx │ │ ├── 18.tsx │ │ ├── 20.tsx │ │ ├── 28.tsx │ │ ├── 24.tsx │ │ ├── 36.tsx │ │ ├── 23.tsx │ │ ├── 11.tsx │ │ ├── 16.tsx │ │ ├── 33.tsx │ │ ├── 19.tsx │ │ ├── 31.tsx │ │ ├── 12.tsx │ │ ├── 15.tsx │ │ ├── 13.tsx │ │ ├── 14.tsx │ │ ├── 25.tsx │ │ ├── 38.tsx │ │ ├── 22.tsx │ │ ├── 30.tsx │ │ ├── 37.tsx │ │ ├── 32.tsx │ │ ├── 34.tsx │ │ ├── 35.tsx │ │ ├── 40.tsx │ │ ├── 41.tsx │ │ ├── 42.tsx │ │ ├── 43.tsx │ │ ├── 47.tsx │ │ ├── 45.tsx │ │ ├── 44.tsx │ │ ├── 46.tsx │ │ ├── 49.tsx │ │ ├── 48.tsx │ │ ├── 50.tsx │ │ ├── 51.tsx │ │ └── 52.tsx │ ├── loadQuestion.ts │ └── QuestionData.ts ├── pages │ ├── TopPage │ │ ├── logo.png │ │ ├── TopPage.module.css │ │ └── index.tsx │ └── QuestionPage │ │ ├── components │ │ ├── assets │ │ │ └── plus.png │ │ ├── GridArea.module.css │ │ ├── GridAreaExtensionControl.module.css │ │ ├── GridAreaExtensionControl.tsx │ │ └── GridArea.tsx │ │ ├── hooks │ │ └── useNextPage.ts │ │ ├── index.tsx │ │ ├── logic │ │ ├── useGridExtension.ts │ │ ├── useGridItemSelection.ts │ │ └── useQuizPageLogic.tsx │ │ ├── TutorialPage │ │ └── index.tsx │ │ ├── QuestionPage.module.css │ │ └── QuizPage │ │ └── index.tsx ├── utils │ ├── indent.ts │ ├── range.ts │ ├── arrayShallowEqual.ts │ ├── i18n │ │ ├── language.ts │ │ ├── LanguageContext.tsx │ │ └── DefineLanguageRoute.tsx │ ├── simpleParseCss.ts │ └── hooks │ │ └── useStateReset.ts ├── App.tsx ├── main.tsx ├── index.css └── Routes.tsx ├── .gitignore ├── .editorconfig ├── public └── ogp.png ├── .vscode └── settings.json ├── vite.config.ts ├── tsconfig.json ├── package.json └── index.html /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | export const appUrl = "https://css-grid-mastery.uhyo.space"; 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space -------------------------------------------------------------------------------- /public/ogp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhyo/css-grid-quiz/HEAD/public/ogp.png -------------------------------------------------------------------------------- /src/questions/GridPosition.ts: -------------------------------------------------------------------------------- 1 | export type GridPosition = `${number},${number}`; 2 | -------------------------------------------------------------------------------- /src/pages/TopPage/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhyo/css-grid-quiz/HEAD/src/pages/TopPage/logo.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.organizeImports": true 4 | } 5 | } -------------------------------------------------------------------------------- /src/utils/indent.ts: -------------------------------------------------------------------------------- 1 | export function indent(str: string, space = " ") { 2 | return str.replace(/^(?!$)/gm, space); 3 | } 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Routes } from "./Routes"; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /src/utils/range.ts: -------------------------------------------------------------------------------- 1 | export function* range(start: number, end: number) { 2 | for (let i = start; i < end; i++) { 3 | yield i; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/QuestionPage/components/assets/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhyo/css-grid-quiz/HEAD/src/pages/QuestionPage/components/assets/plus.png -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /src/utils/arrayShallowEqual.ts: -------------------------------------------------------------------------------- 1 | export function arrayShallowEqual( 2 | a: readonly unknown[], 3 | b: readonly unknown[] 4 | ) { 5 | return a.length === b.length && a.every((v, i) => v === b[i]); 6 | } 7 | -------------------------------------------------------------------------------- /src/questions/v1/1.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | import { Tutorial1 } from "./tutorial/Tutorial1"; 3 | 4 | export const data: QuestionData = { 5 | type: "tutorial", 6 | contents: , 7 | }; 8 | -------------------------------------------------------------------------------- /src/questions/v1/26.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | import { Tutorial2 } from "./tutorial/Tutorial2"; 3 | 4 | export const data: QuestionData = { 5 | type: "tutorial", 6 | contents: , 7 | }; 8 | -------------------------------------------------------------------------------- /src/questions/v1/39.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | import { Tutorial3 } from "./tutorial/Tutorial3"; 3 | 4 | export const data: QuestionData = { 5 | type: "tutorial", 6 | contents: , 7 | }; 8 | -------------------------------------------------------------------------------- /src/questions/v1/53.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | import { Congratulations } from "./tutorial/Congratulations"; 3 | 4 | export const data: QuestionData = { 5 | type: "tutorial", 6 | contents: , 7 | noNext: true, 8 | }; 9 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ) 12 | -------------------------------------------------------------------------------- /src/utils/i18n/language.ts: -------------------------------------------------------------------------------- 1 | const languages = ["en", "ja"] as const; 2 | 3 | export type Language = typeof languages[number]; 4 | 5 | export const defaultLanguage: Language = "en"; 6 | 7 | export function isLanguage(l: unknown): l is Language { 8 | return (languages as readonly unknown[]).includes(l); 9 | } 10 | -------------------------------------------------------------------------------- /src/questions/v1/tutorial/Tutorial.module.css: -------------------------------------------------------------------------------- 1 | .tutorial h1 { 2 | font-size: 2em; 3 | } 4 | 5 | .sampleGridWrapper { 6 | width: 300px; 7 | } 8 | 9 | .shareOnTwitter { 10 | background-color: #00aced; 11 | color: #ffffff; 12 | font-size: 1.5em; 13 | font-weight: bold; 14 | padding: 0.5em; 15 | border-radius: 0.5em; 16 | text-decoration: none; 17 | } -------------------------------------------------------------------------------- /src/utils/simpleParseCss.ts: -------------------------------------------------------------------------------- 1 | export function simpleParseCss(css: string): Record { 2 | const matches = css.matchAll(/([\w-]+)\s*:\s*([^;]+);/g); 3 | return Object.fromEntries( 4 | Array.from(matches).map(([, key, value]) => [ 5 | key.replace(/-[a-z]/g, (match) => match[1].toUpperCase()), 6 | value, 7 | ]) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/questions/loadQuestion.ts: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "./QuestionData"; 2 | 3 | export async function loadQuestionV1(id: string): Promise { 4 | try { 5 | const ns = await import(`./v1/${id}.tsx`); 6 | if (ns.data) { 7 | return ns.data; 8 | } 9 | } catch (err) { 10 | console.error(err); 11 | } 12 | throw new Error(`Cannot load quiz for v1/${id}`); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/i18n/LanguageContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import { defaultLanguage, Language } from "./language"; 3 | 4 | const LanguageContext = createContext(defaultLanguage); 5 | 6 | export const LanguageProvider = LanguageContext.Provider; 7 | 8 | export function useI18n(values: { 9 | [K in Language]: T; 10 | }): T { 11 | const language = useContext(LanguageContext); 12 | return values[language]; 13 | } 14 | -------------------------------------------------------------------------------- /src/questions/v1/2.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: 3; 10 | grid-row: 2; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | gridStyle, 16 | itemStyle, 17 | gridDef: { 18 | rows: 4, 19 | columns: 4, 20 | }, 21 | answer: ["3,2"], 22 | }; 23 | -------------------------------------------------------------------------------- /src/questions/v1/7.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-area: 1 / 2 / 3 / 4; 10 | `.trim(); 11 | 12 | export const data: QuestionData = { 13 | type: "grid", 14 | gridStyle, 15 | itemStyle, 16 | gridDef: { 17 | rows: 4, 18 | columns: 4, 19 | }, 20 | answer: ["2,1", "3,1", "2,2", "3,2"], 21 | }; 22 | -------------------------------------------------------------------------------- /src/questions/v1/8.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: auto / 3; 10 | grid-row: 3 / auto; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | gridStyle, 16 | itemStyle, 17 | gridDef: { 18 | rows: 4, 19 | columns: 4, 20 | }, 21 | answer: ["2,3"], 22 | }; 23 | -------------------------------------------------------------------------------- /src/questions/v1/3.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: 1 / 3; 10 | grid-row: 2 / 3; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | gridStyle, 16 | itemStyle, 17 | gridDef: { 18 | rows: 4, 19 | columns: 4, 20 | }, 21 | answer: ["1,2", "2,2"], 22 | }; 23 | -------------------------------------------------------------------------------- /src/questions/v1/4.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: 1 / -2; 10 | grid-row: 4; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | gridStyle, 16 | itemStyle, 17 | gridDef: { 18 | rows: 4, 19 | columns: 4, 20 | }, 21 | answer: ["1,4", "2,4", "3,4"], 22 | }; 23 | -------------------------------------------------------------------------------- /src/questions/v1/29.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: 8; 10 | grid-row: 6; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | extensible: true, 16 | gridStyle, 17 | itemStyle, 18 | gridDef: { 19 | rows: 4, 20 | columns: 4, 21 | }, 22 | answer: ["8,6"], 23 | }; 24 | -------------------------------------------------------------------------------- /src/questions/v1/5.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: 2 / span 2; 10 | grid-row: 1 / 3; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | gridStyle, 16 | itemStyle, 17 | gridDef: { 18 | rows: 4, 19 | columns: 4, 20 | }, 21 | answer: ["2,1", "3,1", "2,2", "3,2"], 22 | }; 23 | -------------------------------------------------------------------------------- /src/questions/v1/27.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: 4 / 6; 10 | grid-row: 3; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | extensible: true, 16 | gridStyle, 17 | itemStyle, 18 | gridDef: { 19 | rows: 4, 20 | columns: 4, 21 | }, 22 | answer: ["4,3", "5,3"], 23 | }; 24 | -------------------------------------------------------------------------------- /src/questions/v1/9.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: auto; 10 | grid-row: auto / span 4; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | gridStyle, 16 | itemStyle, 17 | gridDef: { 18 | rows: 4, 19 | columns: 4, 20 | }, 21 | answer: ["1,1", "1,2", "1,3", "1,4"], 22 | }; 23 | -------------------------------------------------------------------------------- /src/questions/v1/10.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: 2 / span 2; 10 | grid-row: span 2 / auto; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | gridStyle, 16 | itemStyle, 17 | gridDef: { 18 | rows: 4, 19 | columns: 4, 20 | }, 21 | answer: ["2,1", "3,1", "2,2", "3,2"], 22 | }; 23 | -------------------------------------------------------------------------------- /src/questions/v1/6.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: span 2 / 4; 10 | grid-row: 1 / span 3; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | gridStyle, 16 | itemStyle, 17 | gridDef: { 18 | rows: 4, 19 | columns: 4, 20 | }, 21 | answer: ["2,1", "3,1", "2,2", "3,2", "2,3", "3,3"], 22 | }; 23 | -------------------------------------------------------------------------------- /src/questions/v1/21.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [a] 1fr [b] 1fr [a b]; 6 | grid-template-rows: 7 | [x] 1fr [y] 1fr [x] 1fr [y] 1fr [x y]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: a 2; 12 | grid-row: x / y; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | gridStyle, 18 | itemStyle, 19 | gridDef: { 20 | rows: 4, 21 | columns: 4, 22 | }, 23 | answer: ["3,1"], 24 | }; 25 | -------------------------------------------------------------------------------- /src/questions/v1/17.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [c] 1fr [d] 1fr [e]; 6 | grid-template-rows: 7 | [v] 1fr [w] 1fr [x] 1fr [y] 1fr [z]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: b / span 2; 12 | grid-row: x; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | gridStyle, 18 | itemStyle, 19 | gridDef: { 20 | rows: 4, 21 | columns: 4, 22 | }, 23 | answer: ["2,3", "3,3"], 24 | }; 25 | -------------------------------------------------------------------------------- /src/questions/v1/18.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [c] 1fr [d] 1fr [e]; 6 | grid-template-rows: 7 | [v] 1fr [w] 1fr [x] 1fr [y] 1fr [z]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: b / c; 12 | grid-row: v / y; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | gridStyle, 18 | itemStyle, 19 | gridDef: { 20 | rows: 4, 21 | columns: 4, 22 | }, 23 | answer: ["2,1", "2,2", "2,3"], 24 | }; 25 | -------------------------------------------------------------------------------- /src/questions/v1/20.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [a] 1fr [b] 1fr [a b]; 6 | grid-template-rows: 7 | [x] 1fr [y] 1fr [x] 1fr [y] 1fr [x y]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: a / span 2; 12 | grid-row: y; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | gridStyle, 18 | itemStyle, 19 | gridDef: { 20 | rows: 4, 21 | columns: 4, 22 | }, 23 | answer: ["1,2", "2,2"], 24 | }; 25 | -------------------------------------------------------------------------------- /src/questions/v1/28.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | `.trim(); 7 | 8 | const itemStyle = ` 9 | grid-column: span 3 / -4; 10 | grid-row: 2 / 4; 11 | `.trim(); 12 | 13 | export const data: QuestionData = { 14 | type: "grid", 15 | extensible: true, 16 | gridStyle, 17 | itemStyle, 18 | gridDef: { 19 | rows: 4, 20 | columns: 4, 21 | }, 22 | answer: ["-1,2", "0,2", "1,2", "-1,3", "0,3", "1,3"], 23 | }; 24 | -------------------------------------------------------------------------------- /src/questions/v1/24.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [a] 1fr [b] 1fr [a b]; 6 | grid-template-rows: 7 | [x] 1fr [y] 1fr [x] 1fr [y] 1fr [x y]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: 4 / 2; 12 | grid-row: 2 x / 2 y; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | gridStyle, 18 | itemStyle, 19 | gridDef: { 20 | rows: 4, 21 | columns: 4, 22 | }, 23 | answer: ["2,3", "3,3"], 24 | }; 25 | -------------------------------------------------------------------------------- /src/questions/v1/36.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: [a] 1fr [b] 1fr [c]; 5 | grid-template-rows: [a] 1fr [b] 1fr [c]; 6 | grid-template-areas: 7 | "c b" 8 | "x a"; 9 | `.trim(); 10 | 11 | const itemStyle = ` 12 | grid-column: a; 13 | grid-row: b; 14 | `.trim(); 15 | 16 | export const data: QuestionData = { 17 | type: "grid", 18 | extensible: true, 19 | gridStyle, 20 | itemStyle, 21 | gridDef: { 22 | rows: 2, 23 | columns: 2, 24 | }, 25 | answer: ["2,1"], 26 | }; 27 | -------------------------------------------------------------------------------- /src/questions/v1/23.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [a] 1fr [b] 1fr [a b]; 6 | grid-template-rows: 7 | [x] 1fr [y] 1fr [x] 1fr [y] 1fr [x y]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: a / b -2; 12 | grid-row: 2 / span x; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | gridStyle, 18 | itemStyle, 19 | gridDef: { 20 | rows: 4, 21 | columns: 4, 22 | }, 23 | answer: ["1,2", "2,2", "3,2"], 24 | }; 25 | -------------------------------------------------------------------------------- /src/questions/v1/11.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const itemStyle = ` 14 | grid-area: b; 15 | `.trim(); 16 | 17 | export const data: QuestionData = { 18 | type: "grid", 19 | gridStyle, 20 | itemStyle, 21 | gridDef: { 22 | rows: 4, 23 | columns: 4, 24 | }, 25 | answer: ["3,1", "4,1", "3,2", "4,2"], 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/i18n/DefineLanguageRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, useSearch } from "react-location"; 2 | import { defaultLanguage, isLanguage } from "./language"; 3 | import { LanguageProvider } from "./LanguageContext"; 4 | 5 | export const DefineLanguageRoute: React.VFC = () => { 6 | const { lang: rawLang } = useSearch<{ 7 | Search: { 8 | lang: string; 9 | }; 10 | }>(); 11 | const lang = isLanguage(rawLang) ? rawLang : defaultLanguage; 12 | 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/questions/v1/16.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const itemStyle = ` 14 | grid-column: 2 / a; 15 | grid-row: auto / b; 16 | `.trim(); 17 | 18 | export const data: QuestionData = { 19 | type: "grid", 20 | gridStyle, 21 | itemStyle, 22 | gridDef: { 23 | rows: 4, 24 | columns: 4, 25 | }, 26 | answer: ["2,2"], 27 | }; 28 | -------------------------------------------------------------------------------- /src/questions/v1/33.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [a] 1fr [b] 1fr [a b]; 6 | grid-template-rows: 7 | [x] 1fr [y] 1fr [x] 1fr [y] 1fr [x y]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: span a 2 / 2; 12 | grid-row: -2 z; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | extensible: true, 18 | gridStyle, 19 | itemStyle, 20 | gridDef: { 21 | rows: 4, 22 | columns: 4, 23 | }, 24 | answer: ["0,-1", "1,-1"], 25 | }; 26 | -------------------------------------------------------------------------------- /src/pages/QuestionPage/hooks/useNextPage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useNavigate } from "react-location"; 3 | 4 | export function useNextPage(id: string) { 5 | const navigate = useNavigate(); 6 | const idNum = Number(id); 7 | const nextPageUrl = `/quiz/v1/${idNum + 1}`; 8 | const goToNextPage = useCallback(() => { 9 | navigate({ 10 | to: nextPageUrl, 11 | search: (old) => ({ 12 | cheat: undefined, 13 | lang: old?.lang, 14 | }), 15 | }); 16 | }, [nextPageUrl]); 17 | return { 18 | goToNextPage, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/questions/v1/19.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [c] 1fr [d] 1fr [e]; 6 | grid-template-rows: 7 | [v] 1fr [w] 1fr [x] 1fr [y] 1fr [z]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: a / span d; 12 | grid-row: span v / 3; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | gridStyle, 18 | itemStyle, 19 | gridDef: { 20 | rows: 4, 21 | columns: 4, 22 | }, 23 | answer: ["1,1", "2,1", "3,1", "1,2", "2,2", "3,2"], 24 | }; 25 | -------------------------------------------------------------------------------- /src/questions/v1/31.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [a] 1fr [b] 1fr [a b]; 6 | grid-template-rows: 7 | [x] 1fr [y] 1fr [x] 1fr [y] 1fr [x y]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: a 2 / a 4; 12 | grid-row: x -5; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | extensible: true, 18 | gridStyle, 19 | itemStyle, 20 | gridDef: { 21 | rows: 4, 22 | columns: 4, 23 | }, 24 | answer: ["3,-1", "4,-1", "5,-1"], 25 | }; 26 | -------------------------------------------------------------------------------- /src/questions/v1/12.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const itemStyle = ` 14 | grid-column: b; 15 | grid-row: c; 16 | `.trim(); 17 | 18 | export const data: QuestionData = { 19 | type: "grid", 20 | gridStyle, 21 | itemStyle, 22 | gridDef: { 23 | rows: 4, 24 | columns: 4, 25 | }, 26 | answer: ["3,3", "4,3", "3,4", "4,4"], 27 | }; 28 | -------------------------------------------------------------------------------- /src/questions/v1/15.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const itemStyle = ` 14 | grid-column: span 1 / a; 15 | grid-row: c / -1; 16 | `.trim(); 17 | 18 | export const data: QuestionData = { 19 | type: "grid", 20 | gridStyle, 21 | itemStyle, 22 | gridDef: { 23 | rows: 4, 24 | columns: 4, 25 | }, 26 | answer: ["2,3", "2,4"], 27 | }; 28 | -------------------------------------------------------------------------------- /src/questions/v1/13.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const itemStyle = ` 14 | grid-column: 2; 15 | grid-row: b / c; 16 | `.trim(); 17 | 18 | export const data: QuestionData = { 19 | type: "grid", 20 | gridStyle, 21 | itemStyle, 22 | gridDef: { 23 | rows: 4, 24 | columns: 4, 25 | }, 26 | answer: ["2,1", "2,2", "2,3", "2,4"], 27 | }; 28 | -------------------------------------------------------------------------------- /src/questions/v1/14.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const itemStyle = ` 14 | grid-column: a / span 1; 15 | grid-row: b / span 2; 16 | `.trim(); 17 | 18 | export const data: QuestionData = { 19 | type: "grid", 20 | gridStyle, 21 | itemStyle, 22 | gridDef: { 23 | rows: 4, 24 | columns: 4, 25 | }, 26 | answer: ["1,1", "1,2"], 27 | }; 28 | -------------------------------------------------------------------------------- /src/questions/v1/25.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const itemStyle = ` 14 | grid-column: 2 / a; 15 | grid-row: b-end / span 2; 16 | `.trim(); 17 | 18 | export const data: QuestionData = { 19 | type: "grid", 20 | gridStyle, 21 | itemStyle, 22 | gridDef: { 23 | rows: 4, 24 | columns: 4, 25 | }, 26 | answer: ["2,3", "2,4"], 27 | }; 28 | -------------------------------------------------------------------------------- /src/questions/v1/38.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: [a] 1fr [b] 1fr [c]; 5 | grid-template-rows: [a] 1fr [b] 1fr [c]; 6 | grid-template-areas: 7 | "c b" 8 | "x a"; 9 | `.trim(); 10 | 11 | const itemStyle = ` 12 | grid-column: span x / b-end; 13 | grid-row: x-start; 14 | `.trim(); 15 | 16 | export const data: QuestionData = { 17 | type: "grid", 18 | extensible: true, 19 | gridStyle, 20 | itemStyle, 21 | gridDef: { 22 | rows: 2, 23 | columns: 2, 24 | }, 25 | answer: ["0,2", "1,2", "2,2"], 26 | }; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["./src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/questions/v1/22.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [a] 1fr [b] 1fr [a b]; 6 | grid-template-rows: 7 | [x] 1fr [y] 1fr [x] 1fr [y] 1fr [x y]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: a 2 / b 3; 12 | grid-row: x / span y 3; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | gridStyle, 18 | itemStyle, 19 | gridDef: { 20 | rows: 4, 21 | columns: 4, 22 | }, 23 | answer: ["3,1", "4,1", "3,2", "4,2", "3,3", "4,3", "3,4", "4,4"], 24 | }; 25 | -------------------------------------------------------------------------------- /src/questions/v1/30.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [a] 1fr [b] 1fr [a b]; 6 | grid-template-rows: 7 | [x] 1fr [y] 1fr [x] 1fr [y] 1fr [x y]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: span 2 / a; 12 | grid-row: y / span y; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | extensible: true, 18 | gridStyle, 19 | itemStyle, 20 | gridDef: { 21 | rows: 4, 22 | columns: 4, 23 | }, 24 | answer: ["-1,2", "0,2", "-1,3", "0,3"], 25 | }; 26 | -------------------------------------------------------------------------------- /src/questions/v1/37.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: [a] 1fr [b] 1fr [c]; 5 | grid-template-rows: [a] 1fr [b] 1fr [c]; 6 | grid-template-areas: 7 | "c b" 8 | "x a"; 9 | `.trim(); 10 | 11 | const itemStyle = ` 12 | grid-column: c / 1 c; 13 | grid-row: b 1 / span a; 14 | `.trim(); 15 | 16 | export const data: QuestionData = { 17 | type: "grid", 18 | extensible: true, 19 | gridStyle, 20 | itemStyle, 21 | gridDef: { 22 | rows: 2, 23 | columns: 2, 24 | }, 25 | answer: ["1,2", "2,2", "1,3", "2,3"], 26 | }; 27 | -------------------------------------------------------------------------------- /src/questions/v1/32.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [a] 1fr [b] 1fr [a b]; 6 | grid-template-rows: 7 | [x] 1fr [y] 1fr [x] 1fr [y] 1fr [x y]; 8 | `.trim(); 9 | 10 | const itemStyle = ` 11 | grid-column: c -1 / span c; 12 | grid-row: 2; 13 | `.trim(); 14 | 15 | export const data: QuestionData = { 16 | type: "grid", 17 | extensible: true, 18 | gridStyle, 19 | itemStyle, 20 | gridDef: { 21 | rows: 4, 22 | columns: 4, 23 | }, 24 | answer: ["0,2", "1,2", "2,2", "3,2", "4,2", "5,2"], 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-grid-quiz", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite --host 0.0.0.0", 6 | "build": "tsc && vite build && cp -r ./public ./dist/", 7 | "preview": "vite preview", 8 | "typecheck": "tsc --watch" 9 | }, 10 | "dependencies": { 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-location": "^3.3.3" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^17.0.33", 17 | "@types/react-dom": "^17.0.10", 18 | "@vitejs/plugin-react": "^1.0.7", 19 | "prettier": "^2.5.1", 20 | "typescript": "^4.5.4", 21 | "vite": "^2.7.2" 22 | } 23 | } -------------------------------------------------------------------------------- /src/questions/v1/34.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const itemStyle = ` 14 | grid-column: 2 / span a 2; 15 | grid-row: -2 b; 16 | `.trim(); 17 | 18 | export const data: QuestionData = { 19 | type: "grid", 20 | extensible: true, 21 | gridStyle, 22 | itemStyle, 23 | gridDef: { 24 | rows: 4, 25 | columns: 4, 26 | }, 27 | answer: ["2,-1", "3,-1", "4,-1", "5,-1", "6,-1"], 28 | }; 29 | -------------------------------------------------------------------------------- /src/questions/v1/35.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const itemStyle = ` 14 | grid-column: a-end / span a-start; 15 | grid-row: c 2 / c 4; 16 | `.trim(); 17 | 18 | export const data: QuestionData = { 19 | type: "grid", 20 | extensible: true, 21 | gridStyle, 22 | itemStyle, 23 | gridDef: { 24 | rows: 4, 25 | columns: 4, 26 | }, 27 | answer: ["3,7", "4,7", "5,7", "3,8", "4,8", "5,8"], 28 | }; 29 | -------------------------------------------------------------------------------- /src/questions/v1/40.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(5, 1fr); 5 | grid-template-rows: repeat(5, 1fr); 6 | `.trim(); 7 | 8 | const subgridStyle = ` 9 | grid-column: 2 / span 3; 10 | grid-row: 2 / span 3; 11 | grid-template-columns: subgrid; 12 | grid-template-rows: subgrid; 13 | `.trim(); 14 | 15 | const itemStyle = ` 16 | grid-column: 2; 17 | grid-row: 1 / 3; 18 | `.trim(); 19 | 20 | export const data: QuestionData = { 21 | type: "grid", 22 | gridStyle, 23 | subgridStyle, 24 | itemStyle, 25 | gridDef: { 26 | rows: 5, 27 | columns: 5, 28 | }, 29 | answer: ["3,2", "3,3"], 30 | }; 31 | -------------------------------------------------------------------------------- /src/questions/v1/41.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(5, 1fr); 5 | grid-template-rows: repeat(5, 1fr); 6 | `.trim(); 7 | 8 | const subgridStyle = ` 9 | grid-column: 2 / span 3; 10 | grid-row: 2 / span 3; 11 | grid-template-columns: subgrid; 12 | grid-template-rows: subgrid; 13 | `.trim(); 14 | 15 | const itemStyle = ` 16 | grid-column: 2 / span 3; 17 | grid-row: 2; 18 | `.trim(); 19 | 20 | export const data: QuestionData = { 21 | type: "grid", 22 | gridStyle, 23 | subgridStyle, 24 | itemStyle, 25 | gridDef: { 26 | rows: 5, 27 | columns: 5, 28 | }, 29 | answer: ["3,3", "4,3"], 30 | }; 31 | -------------------------------------------------------------------------------- /src/pages/TopPage/TopPage.module.css: -------------------------------------------------------------------------------- 1 | .topPage { 2 | width: max-content; 3 | max-width: calc(100% - 16px); 4 | margin: 0 auto; 5 | padding: 8px; 6 | } 7 | 8 | .topPage > header { 9 | text-align: center; 10 | } 11 | 12 | .logo { 13 | width: 500px; 14 | height: auto; 15 | max-width: 100%; 16 | } 17 | 18 | .tutorialLink { 19 | text-align: center; 20 | } 21 | 22 | .tutorialLink > a { 23 | display: inline-block; 24 | border-radius: 0.5em; 25 | padding: 0.5em; 26 | background-color: #777777; 27 | color: white; 28 | font-weight: bold; 29 | font-size: 1.5em; 30 | text-decoration: none; 31 | } 32 | 33 | .topPage hr { 34 | border: none; 35 | border-top: 1px solid #cccccc; 36 | } -------------------------------------------------------------------------------- /src/questions/v1/42.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(5, 1fr); 5 | grid-template-rows: repeat(5, 1fr); 6 | `.trim(); 7 | 8 | const subgridStyle = ` 9 | grid-column: 2 / span 3; 10 | grid-row: 2 / span 3; 11 | grid-template-columns: subgrid; 12 | grid-template-rows: subgrid; 13 | `.trim(); 14 | 15 | const itemStyle = ` 16 | grid-column: span 2 / -2; 17 | grid-row: -2 / -4; 18 | `.trim(); 19 | 20 | export const data: QuestionData = { 21 | type: "grid", 22 | gridStyle, 23 | subgridStyle, 24 | itemStyle, 25 | gridDef: { 26 | rows: 5, 27 | columns: 5, 28 | }, 29 | answer: ["2,2", "3,2", "2,3", "3,3"], 30 | }; 31 | -------------------------------------------------------------------------------- /src/pages/QuestionPage/components/GridArea.module.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: grid; 3 | gap: 1px; 4 | aspect-ratio: 1 / 1; 5 | padding: 1px; 6 | grid-auto-columns: 1fr; 7 | grid-auto-rows: 1fr; 8 | } 9 | 10 | .gridHasGrid { 11 | background-color: var(--grid-gap-color); 12 | } 13 | 14 | .gridHasGrid :where(.normalItem, .selectedItem) { 15 | display: flex; 16 | flex-flow: row nowrap; 17 | align-items: center; 18 | justify-content: center; 19 | background-color: var(--grid-bg-color); 20 | color: var(--grid-fg-color); 21 | font-size: var(--grid-font-size); 22 | overflow: hidden; 23 | } 24 | 25 | .selectedItem { 26 | background-color: var(--grid-active-bg-color); 27 | color: var(--grid-active-fg-color); 28 | } -------------------------------------------------------------------------------- /src/questions/v1/43.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(5, 1fr); 5 | grid-template-rows: repeat(5, 1fr); 6 | `.trim(); 7 | 8 | const subgridStyle = ` 9 | grid-column: 2 / span 3; 10 | grid-row: 2 / span 3; 11 | grid-template-columns: subgrid [a] [b] [c] [d]; 12 | grid-template-rows: subgrid [w] [x] [y] [z]; 13 | `.trim(); 14 | 15 | const itemStyle = ` 16 | grid-column: a / c; 17 | grid-row: x / z; 18 | `.trim(); 19 | 20 | export const data: QuestionData = { 21 | type: "grid", 22 | gridStyle, 23 | subgridStyle, 24 | itemStyle, 25 | gridDef: { 26 | rows: 5, 27 | columns: 5, 28 | }, 29 | answer: ["2,3", "3,3", "2,4", "3,4"], 30 | }; 31 | -------------------------------------------------------------------------------- /src/questions/v1/47.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [c] 1fr [d] 1fr [e]; 6 | grid-template-rows: 7 | [v] 1fr [w] 1fr [x] 1fr [y] 1fr [z]; 8 | `.trim(); 9 | 10 | const subgridStyle = ` 11 | grid-column: c / -1; 12 | grid-row: v / -2; 13 | grid-template-columns: subgrid [] [f]; 14 | grid-template-rows: subgrid [] [h] [i]; 15 | `.trim(); 16 | 17 | const itemStyle = ` 18 | grid-column: f; 19 | grid-row: h / i; 20 | `.trim(); 21 | 22 | export const data: QuestionData = { 23 | type: "grid", 24 | gridStyle, 25 | subgridStyle, 26 | itemStyle, 27 | gridDef: { 28 | rows: 4, 29 | columns: 4, 30 | }, 31 | answer: ["4,2"], 32 | }; 33 | -------------------------------------------------------------------------------- /src/questions/v1/45.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [c] 1fr [d] 1fr [e]; 6 | grid-template-rows: 7 | [v] 1fr [w] 1fr [x] 1fr [y] 1fr [z]; 8 | `.trim(); 9 | 10 | const subgridStyle = ` 11 | grid-column: b / e; 12 | grid-row: v / y; 13 | grid-template-columns: subgrid; 14 | grid-template-rows: subgrid; 15 | `.trim(); 16 | 17 | const itemStyle = ` 18 | grid-column: b / d; 19 | grid-row: z / w; 20 | `.trim(); 21 | 22 | export const data: QuestionData = { 23 | type: "grid", 24 | gridStyle, 25 | subgridStyle, 26 | itemStyle, 27 | gridDef: { 28 | rows: 4, 29 | columns: 4, 30 | }, 31 | answer: ["2,2", "3,2", "2,3", "3,3"], 32 | }; 33 | -------------------------------------------------------------------------------- /src/questions/v1/44.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(5, 1fr); 5 | grid-template-rows: repeat(5, 1fr); 6 | `.trim(); 7 | 8 | const subgridStyle = ` 9 | grid-column: 2 / span 3; 10 | grid-row: 2 / span 3; 11 | grid-template-columns: subgrid [a] [b] [c] [d]; 12 | grid-template-rows: subgrid [w] [x] [y] [z]; 13 | `.trim(); 14 | 15 | const itemStyle = ` 16 | grid-column: a / a 2; 17 | grid-row: span w / y; 18 | `.trim(); 19 | 20 | export const data: QuestionData = { 21 | type: "grid", 22 | gridStyle, 23 | subgridStyle, 24 | itemStyle, 25 | gridDef: { 26 | rows: 5, 27 | columns: 5, 28 | }, 29 | answer: ["2,2", "3,2", "4,2", "2,3", "3,3", "4,3"], 30 | }; 31 | -------------------------------------------------------------------------------- /src/questions/v1/46.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: 5 | [a] 1fr [b] 1fr [c] 1fr [d] 1fr [e]; 6 | grid-template-rows: 7 | [v] 1fr [w] 1fr [x] 1fr [y] 1fr [z]; 8 | `.trim(); 9 | 10 | const subgridStyle = ` 11 | grid-column: b / e; 12 | grid-row: v / y; 13 | grid-template-columns: subgrid [f] [] [g]; 14 | grid-template-rows: subgrid [] [h] [] [i]; 15 | `.trim(); 16 | 17 | const itemStyle = ` 18 | grid-column: c / g; 19 | grid-row: h / span 1 i; 20 | `.trim(); 21 | 22 | export const data: QuestionData = { 23 | type: "grid", 24 | gridStyle, 25 | subgridStyle, 26 | itemStyle, 27 | gridDef: { 28 | rows: 4, 29 | columns: 4, 30 | }, 31 | answer: ["3,2", "3,3"], 32 | }; 33 | -------------------------------------------------------------------------------- /src/questions/v1/49.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const subgridStyle = ` 14 | grid-column: 2 / span 2; 15 | grid-row: 2 / span 2; 16 | grid-template-columns: subgrid; 17 | grid-template-rows: subgrid; 18 | `.trim(); 19 | 20 | const itemStyle = ` 21 | grid-column: a; 22 | grid-row: b / c-end; 23 | `.trim(); 24 | 25 | export const data: QuestionData = { 26 | type: "grid", 27 | gridStyle, 28 | subgridStyle, 29 | itemStyle, 30 | gridDef: { 31 | rows: 4, 32 | columns: 4, 33 | }, 34 | answer: ["2,2", "2,3"], 35 | }; 36 | -------------------------------------------------------------------------------- /src/questions/v1/48.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const subgridStyle = ` 14 | grid-column: a; 15 | grid-row: a; 16 | grid-template-columns: subgrid; 17 | grid-template-rows: subgrid; 18 | `.trim(); 19 | 20 | const itemStyle = ` 21 | grid-column: a / b; 22 | grid-row: 2 / span 2; 23 | `.trim(); 24 | 25 | export const data: QuestionData = { 26 | type: "grid", 27 | gridStyle, 28 | subgridStyle, 29 | itemStyle, 30 | gridDef: { 31 | rows: 4, 32 | columns: 4, 33 | }, 34 | answer: ["1,2", "2,2", "1,3", "2,3"], 35 | }; 36 | -------------------------------------------------------------------------------- /src/questions/v1/50.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const subgridStyle = ` 14 | grid-column: 2 / span 2; 15 | grid-row: 2 / span 2; 16 | grid-template-columns: subgrid; 17 | grid-template-rows: subgrid; 18 | grid-template-areas: 19 | "b a" 20 | "b a"; 21 | `.trim(); 22 | 23 | const itemStyle = ` 24 | grid-column: a; 25 | grid-row: c; 26 | `.trim(); 27 | 28 | export const data: QuestionData = { 29 | type: "grid", 30 | gridStyle, 31 | subgridStyle, 32 | itemStyle, 33 | gridDef: { 34 | rows: 4, 35 | columns: 4, 36 | }, 37 | answer: ["2,3"], 38 | }; 39 | -------------------------------------------------------------------------------- /src/questions/v1/51.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const subgridStyle = ` 14 | grid-column: 2 / span 3; 15 | grid-row: 2 / span 3; 16 | grid-template-columns: subgrid; 17 | grid-template-rows: subgrid; 18 | grid-template-areas: 19 | "b a a" 20 | "b a a" 21 | "b a a; 22 | `.trim(); 23 | 24 | const itemStyle = ` 25 | grid-column: a-start 2; 26 | grid-row: span 2 / b-end -1; 27 | `.trim(); 28 | 29 | export const data: QuestionData = { 30 | type: "grid", 31 | gridStyle, 32 | subgridStyle, 33 | itemStyle, 34 | gridDef: { 35 | rows: 4, 36 | columns: 4, 37 | }, 38 | answer: ["3,3", "3,4"], 39 | }; 40 | -------------------------------------------------------------------------------- /src/questions/v1/52.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionData } from "../QuestionData"; 2 | 3 | const gridStyle = ` 4 | grid-template-columns: repeat(4, 1fr); 5 | grid-template-rows: repeat(4, 1fr); 6 | grid-template-areas: 7 | "a a b b" 8 | "a a b b" 9 | "a a c c" 10 | "a a c c"; 11 | `.trim(); 12 | 13 | const subgridStyle = ` 14 | grid-column: 2 / span 3; 15 | grid-row: 2 / span 3; 16 | grid-template-columns: subgrid [] [c-start]; 17 | grid-template-rows: subgrid [] [] [b-end]; 18 | grid-template-areas: 19 | "b a a" 20 | "b a a" 21 | "b a a; 22 | `.trim(); 23 | 24 | const itemStyle = ` 25 | grid-column: c-start 2; 26 | grid-row: 1 / b-end 2; 27 | `.trim(); 28 | 29 | export const data: QuestionData = { 30 | type: "grid", 31 | gridStyle, 32 | subgridStyle, 33 | itemStyle, 34 | gridDef: { 35 | rows: 4, 36 | columns: 4, 37 | }, 38 | answer: ["4,2", "4,3"], 39 | }; 40 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | 9 | --body-bg-color: white; 10 | --body-fg-color: black; 11 | background-color: var(--body-bg-color); 12 | color: var(--body-fg-color); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | body { 17 | --body-bg-color: #444444; 18 | --body-fg-color: white; 19 | } 20 | } 21 | 22 | pre { 23 | margin: 0; 24 | } 25 | 26 | button { 27 | appearance: none; 28 | margin: 0; 29 | padding: 0; 30 | border: none; 31 | background: none; 32 | font: inherit; 33 | font-size: calc(10px + 2vmin); 34 | } 35 | 36 | code { 37 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 38 | monospace; 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/QuestionPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useMatch, useSearch } from "react-location"; 3 | import { QuestionData } from "../../questions/QuestionData"; 4 | import { QuizPage } from "./QuizPage"; 5 | import { TutorialPage } from "./TutorialPage"; 6 | 7 | export const QuestionPage: React.VFC = () => { 8 | const { 9 | params: { id }, 10 | data: { quizData }, 11 | } = useMatch<{ 12 | LoaderData: { 13 | quizData: QuestionData; 14 | }; 15 | }>(); 16 | const { cheat } = useSearch<{ 17 | Search: { cheat?: string }; 18 | }>(); 19 | useEffect(() => { 20 | window.scrollTo({ 21 | top: 0, 22 | behavior: "smooth", 23 | }); 24 | }, [id]); 25 | if (quizData === undefined) { 26 | return null; 27 | } 28 | if (quizData.type === "tutorial") { 29 | return ; 30 | } 31 | return ; 32 | }; 33 | -------------------------------------------------------------------------------- /src/pages/QuestionPage/logic/useGridExtension.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useStateReset } from "../../../utils/hooks/useStateReset"; 3 | 4 | export type EdgeDirection = "top" | "right" | "bottom" | "left"; 5 | 6 | export type GridExtensionState = { 7 | readonly top: number; 8 | readonly right: number; 9 | readonly bottom: number; 10 | readonly left: number; 11 | }; 12 | 13 | export function useGridExtension(deps: readonly unknown[]) { 14 | const [extension, setExtension] = useStateReset( 15 | deps, 16 | () => ({ 17 | top: 0, 18 | right: 0, 19 | bottom: 0, 20 | left: 0, 21 | }) 22 | ); 23 | 24 | const extend = useCallback( 25 | (direction: EdgeDirection, amount = 1) => { 26 | setExtension((prev) => { 27 | return { ...prev, [direction]: prev[direction] + amount }; 28 | }); 29 | }, 30 | [setExtension] 31 | ); 32 | 33 | return { 34 | extension, 35 | extend, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/questions/QuestionData.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GridPosition } from "./GridPosition"; 3 | 4 | export type QuestionDataBase = { 5 | noNext?: true; 6 | }; 7 | 8 | export type Tutorial = QuestionDataBase & { 9 | type: "tutorial"; 10 | contents: React.ReactElement; 11 | }; 12 | 13 | export type GridQuestion = QuestionDataBase & { 14 | type: "grid"; 15 | /** 16 | * Style of grid containter. 17 | */ 18 | gridStyle: string; 19 | /** 20 | * Style of grid item. 21 | */ 22 | itemStyle: string; 23 | /** 24 | * Rendered size of grid. 25 | */ 26 | gridDef: { 27 | rows: number; 28 | columns: number; 29 | }; 30 | /** 31 | * Whether grid is extensible. 32 | */ 33 | extensible?: boolean; 34 | /** 35 | * Style for subgrid. 36 | */ 37 | subgridStyle?: string; 38 | /** 39 | * Correct answer. 40 | */ 41 | answer: readonly GridPosition[]; 42 | }; 43 | 44 | export type QuizData = GridQuestion; 45 | 46 | export type QuestionData = Tutorial | QuizData; 47 | -------------------------------------------------------------------------------- /src/pages/QuestionPage/TutorialPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tutorial } from "../../../questions/QuestionData"; 2 | import { useI18n } from "../../../utils/i18n/LanguageContext"; 3 | import { useNextPage } from "../hooks/useNextPage"; 4 | import classes from "../QuestionPage.module.css"; 5 | 6 | type Props = { 7 | id: string; 8 | tutorial: Tutorial; 9 | }; 10 | 11 | export const TutorialPage: React.VFC = ({ id, tutorial }) => { 12 | const { goToNextPage } = useNextPage(id); 13 | const langs = useI18n({ 14 | en: { 15 | proceed: "Proceed", 16 | }, 17 | ja: { 18 | proceed: "進む", 19 | }, 20 | }); 21 | return ( 22 |
23 |
{tutorial.contents}
24 |
25 | {!tutorial.noNext && ( 26 | 29 | )} 30 |
31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | CSS Grid Mastery Quiz 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/pages/QuestionPage/logic/useGridItemSelection.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { GridPosition } from "../../../questions/GridPosition"; 3 | import { useStateReset } from "../../../utils/hooks/useStateReset"; 4 | 5 | export function useGridItemSelection( 6 | deps: readonly unknown[], 7 | initialState?: readonly GridPosition[] 8 | ) { 9 | const [selectedItems, setSelectedItems] = useStateReset< 10 | readonly GridPosition[] 11 | >(deps, () => initialState ?? []); 12 | const toggleItem = useCallback( 13 | (column: number, row: number) => { 14 | setSelectedItems((selectedItems) => { 15 | const newSelectedItems = [...selectedItems]; 16 | const itemKey: GridPosition = `${column},${row}`; 17 | if (newSelectedItems.includes(itemKey)) { 18 | newSelectedItems.splice(newSelectedItems.indexOf(itemKey), 1); 19 | } else { 20 | newSelectedItems.push(itemKey); 21 | } 22 | return newSelectedItems; 23 | }); 24 | }, 25 | [setSelectedItems] 26 | ); 27 | 28 | return { 29 | selectedItems, 30 | toggleItem, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/QuestionPage/components/GridAreaExtensionControl.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: grid; 3 | grid-template-columns: auto 1fr auto; 4 | grid-template-rows: auto 1fr auto; 5 | } 6 | 7 | .main { 8 | grid-column: 2; 9 | grid-row: 2; 10 | } 11 | 12 | .button { 13 | display: flex; 14 | flex-flow: column nowrap; 15 | justify-content: center; 16 | align-items: center; 17 | margin: 4px; 18 | width: 32px; 19 | height: 32px; 20 | background-color: #ffffaa; 21 | border: 1px solid #aaaa22; 22 | border-radius: 4px; 23 | } 24 | 25 | .left { 26 | grid-column: 1; 27 | grid-row: 2; 28 | width: max-content; 29 | height: max-content; 30 | align-self: center; 31 | } 32 | 33 | .top { 34 | grid-column: 2; 35 | grid-row: 1; 36 | width: max-content; 37 | height: max-content; 38 | justify-self: center; 39 | } 40 | 41 | .right { 42 | grid-column: 3; 43 | grid-row: 2; 44 | width: max-content; 45 | height: max-content; 46 | align-self: center; 47 | } 48 | 49 | .bottom { 50 | grid-column: 2; 51 | grid-row: 3; 52 | width: max-content; 53 | height: max-content; 54 | justify-self: center; 55 | } -------------------------------------------------------------------------------- /src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import { ReactLocation, Route, Router } from "react-location"; 2 | import { QuestionPage } from "./pages/QuestionPage"; 3 | import { TopPage } from "./pages/TopPage"; 4 | import { loadQuestionV1 } from "./questions/loadQuestion"; 5 | import { DefineLanguageRoute } from "./utils/i18n/DefineLanguageRoute"; 6 | 7 | const routes: Route[] = [ 8 | { 9 | element: , 10 | children: [ 11 | { 12 | path: "/", 13 | element: , 14 | }, 15 | { 16 | path: "/quiz", 17 | children: [ 18 | { 19 | path: "/v1", 20 | children: [ 21 | { 22 | path: ":id", 23 | loader: async ({ params }) => { 24 | return { 25 | quizData: await loadQuestionV1(params.id), 26 | }; 27 | }, 28 | element: , 29 | }, 30 | ], 31 | }, 32 | ], 33 | }, 34 | ], 35 | }, 36 | ]; 37 | 38 | const location = new ReactLocation(); 39 | 40 | export const Routes: React.VFC = () => { 41 | return ; 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils/hooks/useStateReset.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from "react"; 2 | import { arrayShallowEqual } from "../arrayShallowEqual"; 3 | 4 | /** 5 | * useState, but reset when given deps has changed. 6 | */ 7 | export function useStateReset( 8 | deps: readonly unknown[], 9 | initialValue: () => T 10 | ): [state: T, update: (updater: T | ((current: T) => T)) => void] { 11 | type InternalState = { 12 | deps: readonly unknown[]; 13 | value: T; 14 | }; 15 | const [state, setStateInternal] = useState(() => ({ 16 | deps, 17 | value: initialValue(), 18 | })); 19 | 20 | const setState = (value: T | ((current: T) => T)) => { 21 | setStateInternal((current) => { 22 | if (typeof value !== "function") { 23 | return { 24 | deps, 25 | value, 26 | }; 27 | } 28 | const currentState = arrayShallowEqual(current.deps, deps) 29 | ? current.value 30 | : initialValue(); 31 | return { 32 | deps, 33 | value: (value as (current: T) => T)(currentState), 34 | }; 35 | }); 36 | }; 37 | 38 | const currentState = useMemo(() => { 39 | if (arrayShallowEqual(deps, state.deps)) { 40 | return state.value; 41 | } else { 42 | return initialValue(); 43 | } 44 | }, [state, deps]); 45 | 46 | return [currentState, setState]; 47 | } 48 | -------------------------------------------------------------------------------- /src/questions/v1/tutorial/Congratulations.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { appUrl } from "../../../const"; 3 | import { useI18n } from "../../../utils/i18n/LanguageContext"; 4 | import classes from "./Tutorial.module.css"; 5 | 6 | export const Congratulations: React.VFC = () => { 7 | const shareText = useI18n({ 8 | en: "I solved all the problems in CSS Grid Mastery Quiz!\n", 9 | ja: "CSS Grid Mastery Quizで全ての問題をクリアしました!\n", 10 | }); 11 | 12 | const twitterIntent = useMemo(() => { 13 | return `https://twitter.com/intent/tweet?text=${encodeURIComponent( 14 | shareText 15 | )}&url=${encodeURIComponent(appUrl)}`; 16 | }, [shareText]); 17 | const contents = useI18n({ 18 | en: ( 19 | <> 20 |

Congratulations!

21 |

You solved all the problems in CSS Grid Mastery Quiz.

22 |

23 | You are now CSS Grid Master! 24 |

25 |

26 | 32 | Share on Twitter 33 | 34 |

35 | 36 | ), 37 | ja: ( 38 | <> 39 |

完全制覇!

40 |

41 | おめでとうございます! CSS Grid Mastery 42 | Quizの全ての問題をクリアしました。 43 |

44 |

45 | あなたはCSS Gridマスターです! 46 |

47 |

48 | 54 | Twitterでシェアする 55 | 56 |

57 | 58 | ), 59 | }); 60 | return
{contents}
; 61 | }; 62 | -------------------------------------------------------------------------------- /src/pages/QuestionPage/components/GridAreaExtensionControl.tsx: -------------------------------------------------------------------------------- 1 | import { EdgeDirection } from "../logic/useGridExtension"; 2 | import plusImage from "./assets/plus.png"; 3 | import classes from "./GridAreaExtensionControl.module.css"; 4 | 5 | type Props = { 6 | children?: React.ReactNode; 7 | onExtend: (direction: EdgeDirection) => void; 8 | }; 9 | 10 | export const GridAreaExtensionControl: React.VFC = ({ 11 | children, 12 | onExtend, 13 | }) => { 14 | return ( 15 |
16 |
17 | 24 |
25 |
26 | 33 |
34 |
35 | 42 |
43 |
44 | 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 | 
14 |
15 |
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 |
62 |
63 |

64 | CSS Grid Mastery Quiz 71 |

72 |

73 | 74 | English 75 | {" "} 76 | |{" "} 77 | 84 | 日本語 85 | 86 |

87 |
88 | {langs.intro} 89 |

90 | 91 | {langs.proceed} 92 | 93 |

94 | {langs.progress} 95 |
96 |

97 | 98 | GitHub 99 | 100 |

101 |

102 | 103 | Share on Twitter 104 | 105 |

106 |

107 | 108 | This site is using{" "} 109 | 110 | Twemoji 111 | 112 | . 113 | 114 |

115 |
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 | 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 | 
14 |
15 |
16 |
17 |
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 |
90 |
91 |
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 | 191 | ) : ( 192 | 193 | )} 194 | 195 | {buttonState === "check" ? ( 196 | 199 | ) : buttonState === "correct" ? ( 200 | 203 | ) : buttonState === "wrong" ? ( 204 | 207 | ) : null} 208 | {isCheat && isSubgrid ? ( 209 |

{langs.subgridCheatNote}

210 | ) : null} 211 |
212 | ); 213 | }); 214 | --------------------------------------------------------------------------------