feat(frontend): show the rules
A How to play page with every rule and a before/after board computed by the engine, a help button in the header, and the rules in a side panel over the game page so a game can go on while a rule is looked up.
This commit is contained in:
parent
3a46833e86
commit
8511e0b3f2
@ -14,6 +14,7 @@ import { RTMTWS, ConnectionState, AuthUser } from '@mancala/core';
|
|||||||
import { Theme } from '@mancala/core';
|
import { Theme } from '@mancala/core';
|
||||||
import LobyPage from './routes/LobyPage';
|
import LobyPage from './routes/LobyPage';
|
||||||
import PrivacyPage from './routes/PrivacyPage';
|
import PrivacyPage from './routes/PrivacyPage';
|
||||||
|
import RulesPage from './routes/RulesPage';
|
||||||
import swal from 'sweetalert';
|
import swal from 'sweetalert';
|
||||||
import Util from './util/Util';
|
import Util from './util/Util';
|
||||||
import { server } from './const/config';
|
import { server } from './const/config';
|
||||||
@ -130,6 +131,7 @@ const MancalaApp: FunctionComponent = () => {
|
|||||||
</Route>
|
</Route>
|
||||||
<Route path="privacy" element={<PrivacyPage context={context} theme={theme} userKey={userKey} />}>
|
<Route path="privacy" element={<PrivacyPage context={context} theme={theme} userKey={userKey} />}>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="rules" element={<RulesPage context={context} theme={theme} />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>}
|
</BrowserRouter>}
|
||||||
|
|||||||
@ -71,3 +71,7 @@ export const PauseIcon = ({ size = 22 }: IconProps) => (
|
|||||||
export const BotIcon = ({ size = 20 }: IconProps) => (
|
export const BotIcon = ({ size = 20 }: IconProps) => (
|
||||||
<svg {...stroke(size, 2)}><rect x="4" y="8" width="16" height="12" rx="4" /><path d="M12 4v4" /><circle cx="12" cy="3.5" r="1" /><path d="M9 13v2M15 13v2" /></svg>
|
<svg {...stroke(size, 2)}><rect x="4" y="8" width="16" height="12" rx="4" /><path d="M12 4v4" /><circle cx="12" cy="3.5" r="1" /><path d="M9 13v2M15 13v2" /></svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const HelpIcon = ({ size = 20 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2)}><circle cx="12" cy="12" r="9" /><path d="M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.7.3-1 .9-1 1.7v.5" /><path d="M12 17.5h.01" /></svg>
|
||||||
|
);
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import { Link } from "react-router-dom";
|
|||||||
import { AuthUser } from "@mancala/core";
|
import { AuthUser } from "@mancala/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Context } from "../../context/context";
|
import { Context } from "../../context/context";
|
||||||
import { LogoIcon } from "../Icons";
|
import { HelpIcon, LogoIcon } from "../Icons";
|
||||||
import AccountMenu from "./AccountMenu";
|
import AccountMenu from "./AccountMenu";
|
||||||
import ThemeSwatches from "./ThemeSwatches";
|
import ThemeSwatches from "./ThemeSwatches";
|
||||||
|
|
||||||
// The header of every page: logo and name, theme swatches, the account menu when a user is given, and the page's
|
// The header of every page: logo and name, theme swatches, how to play, the account menu when a user is given, and
|
||||||
// main action (children).
|
// the page's main action (children). How to play opens the rules page, or calls `onHelp` (the game page shows the
|
||||||
const AppHeader: FunctionComponent<{ context: Context, authUser?: AuthUser }> = ({ context, authUser, children }) => {
|
// rules over the game instead).
|
||||||
|
const AppHeader: FunctionComponent<{ context: Context, authUser?: AuthUser, onHelp?: () => void }> = ({ context, authUser, onHelp, children }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<header className={"hdr" + (authUser ? " has-account" : "")}>
|
<header className={"hdr" + (authUser ? " has-account" : "")}>
|
||||||
@ -19,6 +20,9 @@ const AppHeader: FunctionComponent<{ context: Context, authUser?: AuthUser }> =
|
|||||||
<span>{t("Mancala")}</span>
|
<span>{t("Mancala")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<ThemeSwatches context={context} />
|
<ThemeSwatches context={context} />
|
||||||
|
{onHelp
|
||||||
|
? <button className="btn icon ghost" aria-label={t("HowToPlay")} title={t("HowToPlay")} onClick={onHelp}><HelpIcon /></button>
|
||||||
|
: <Link className="btn icon ghost" to="/rules" aria-label={t("HowToPlay")} title={t("HowToPlay")}><HelpIcon /></Link>}
|
||||||
{authUser && <AccountMenu context={context} authUser={authUser} />}
|
{authUser && <AccountMenu context={context} authUser={authUser} />}
|
||||||
{children}
|
{children}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -47,24 +47,10 @@
|
|||||||
.meta .pill { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
.meta .pill { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.meta > span:last-child { text-align: right; }
|
.meta > span:last-child { text-align: right; }
|
||||||
|
|
||||||
/* Mini board: the top player's store on the left, the bottom player's on the right */
|
|
||||||
|
|
||||||
.mini { display: grid; grid-template-columns: 1.15fr repeat(6, minmax(0, 1fr)) 1.15fr; gap: 5px; padding: 8px; border-radius: 16px; background: var(--miniBg); }
|
|
||||||
.mini .mp { aspect-ratio: 1; border-radius: 50%; background: var(--miniPit); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; color: var(--ink); box-shadow: inset 0 2px 4px rgba(0, 0, 0, .35); }
|
|
||||||
.mini .ms { grid-row: 1 / 3; border-radius: 999px; background: var(--miniPit); display: flex; align-items: flex-end; justify-content: center; padding-bottom: 8px; font-size: 13px; font-weight: 800; color: var(--ink); box-shadow: inset 0 2px 4px rgba(0, 0, 0, .35); }
|
|
||||||
.mini .ms.l { grid-column: 1; }
|
|
||||||
.mini .ms.r { grid-column: 8; color: var(--accent); }
|
|
||||||
.mini.lg { gap: 9px; padding: 16px; border-radius: 26px; }
|
|
||||||
.mini.lg .mp { font-size: 16px; }
|
|
||||||
.mini.lg .ms { font-size: 20px; padding-bottom: 14px; }
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.hero { grid-template-columns: 1fr; padding: 20px; gap: 20px; border-radius: 26px; }
|
.hero { grid-template-columns: 1fr; padding: 20px; gap: 20px; border-radius: 26px; }
|
||||||
.hero h1 { font-size: 25px; }
|
.hero h1 { font-size: 25px; }
|
||||||
.hero .btn { flex: 1; }
|
.hero .btn { flex: 1; }
|
||||||
.mini.lg { gap: 6px; padding: 10px; border-radius: 20px; }
|
|
||||||
.mini.lg .mp { font-size: 13px; }
|
|
||||||
.mini.lg .ms { font-size: 15px; padding-bottom: 10px; }
|
|
||||||
.grid { grid-template-columns: 1fr; gap: 14px; }
|
.grid { grid-template-columns: 1fr; gap: 14px; }
|
||||||
.sec { margin: 12px 0 -4px; }
|
.sec { margin: 12px 0 -4px; }
|
||||||
}
|
}
|
||||||
|
|||||||
19
apps/frontend/src/components/home/MiniBoard.css
Normal file
19
apps/frontend/src/components/home/MiniBoard.css
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/* Mini board: the top player's store on the left, the bottom player's on the right */
|
||||||
|
|
||||||
|
.mini { display: grid; grid-template-columns: 1.15fr repeat(6, minmax(0, 1fr)) 1.15fr; gap: 5px; padding: 8px; border-radius: 16px; background: var(--miniBg); }
|
||||||
|
.mini .mp { aspect-ratio: 1; border-radius: 50%; background: var(--miniPit); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; color: var(--ink); box-shadow: inset 0 2px 4px rgba(0, 0, 0, .35); }
|
||||||
|
.mini .ms { grid-row: 1 / 3; border-radius: 999px; background: var(--miniPit); display: flex; align-items: flex-end; justify-content: center; padding-bottom: 8px; font-size: 13px; font-weight: 800; color: var(--ink); box-shadow: inset 0 2px 4px rgba(0, 0, 0, .35); }
|
||||||
|
.mini .ms.l { grid-column: 1; }
|
||||||
|
.mini .ms.r { grid-column: 8; color: var(--accent); }
|
||||||
|
.mini.lg { gap: 9px; padding: 16px; border-radius: 26px; }
|
||||||
|
.mini.lg .mp { font-size: 16px; }
|
||||||
|
.mini.lg .ms { font-size: 20px; padding-bottom: 14px; }
|
||||||
|
/* A move shown on the board: the pit played and where its last stone landed */
|
||||||
|
.mini .played { box-shadow: inset 0 0 0 2px var(--accent); }
|
||||||
|
.mini .land, .mini .ms.r.land { background: var(--accent); color: var(--onAccent); }
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.mini.lg { gap: 6px; padding: 10px; border-radius: 20px; }
|
||||||
|
.mini.lg .mp { font-size: 13px; }
|
||||||
|
.mini.lg .ms { font-size: 15px; padding-bottom: 10px; }
|
||||||
|
}
|
||||||
@ -1,22 +1,27 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
|
import "./MiniBoard.css";
|
||||||
|
|
||||||
const PLAYER1_PITS = [0, 1, 2, 3, 4, 5];
|
const PLAYER1_PITS = [0, 1, 2, 3, 4, 5];
|
||||||
const PLAYER2_PITS = [7, 8, 9, 10, 11, 12];
|
const PLAYER2_PITS = [7, 8, 9, 10, 11, 12];
|
||||||
const PLAYER1_STORE = 6;
|
const PLAYER1_STORE = 6;
|
||||||
const PLAYER2_STORE = 13;
|
const PLAYER2_STORE = 13;
|
||||||
|
|
||||||
// Stone counts on a small board, as the bottom player sees it. Empty pits show no number.
|
// Stone counts on a small board, as the bottom player sees it. Empty pits show no number. `played` and `landed` mark
|
||||||
const MiniBoard: FunctionComponent<{ board: number[], revert?: boolean, large?: boolean }> = ({ board, revert, large }) => {
|
// a move: the pit played and where its last stone landed.
|
||||||
|
const MiniBoard: FunctionComponent<{
|
||||||
|
board: number[], revert?: boolean, large?: boolean, played?: number, landed?: number,
|
||||||
|
}> = ({ board, revert, large, played, landed }) => {
|
||||||
const [topPits, topStore, bottomPits, bottomStore] = revert
|
const [topPits, topStore, bottomPits, bottomStore] = revert
|
||||||
? [PLAYER1_PITS, PLAYER1_STORE, PLAYER2_PITS, PLAYER2_STORE]
|
? [PLAYER1_PITS, PLAYER1_STORE, PLAYER2_PITS, PLAYER2_STORE]
|
||||||
: [PLAYER2_PITS, PLAYER2_STORE, PLAYER1_PITS, PLAYER1_STORE];
|
: [PLAYER2_PITS, PLAYER2_STORE, PLAYER1_PITS, PLAYER1_STORE];
|
||||||
const pit = (index: number) => <div key={index} className="mp">{board[index] || ""}</div>;
|
const marks = (index: number) => (index === played ? " played" : "") + (index === landed ? " land" : "");
|
||||||
|
const pit = (index: number) => <div key={index} className={"mp" + marks(index)}>{board[index] || ""}</div>;
|
||||||
return (
|
return (
|
||||||
<div className={"mini" + (large ? " lg" : "")} aria-hidden="true">
|
<div className={"mini" + (large ? " lg" : "")} aria-hidden="true">
|
||||||
<div className="ms l">{board[topStore]}</div>
|
<div className={"ms l" + marks(topStore)}>{board[topStore]}</div>
|
||||||
{[...topPits].reverse().map(pit)}
|
{[...topPits].reverse().map(pit)}
|
||||||
<div className="ms r">{board[bottomStore]}</div>
|
<div className={"ms r" + marks(bottomStore)}>{board[bottomStore]}</div>
|
||||||
{bottomPits.map(pit)}
|
{bottomPits.map(pit)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
44
apps/frontend/src/components/rules/RuleBoard.tsx
Normal file
44
apps/frontend/src/components/rules/RuleBoard.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { getMoveOutcome, RuleExample } from "@mancala/core";
|
||||||
|
import MiniBoard from "../home/MiniBoard";
|
||||||
|
import { NextIcon } from "../Icons";
|
||||||
|
|
||||||
|
// A rule's example: the board before the move with the pit played marked, and after it with the landing pit marked.
|
||||||
|
// The board after the move comes from the engine.
|
||||||
|
const RuleBoard: FunctionComponent<{ example: RuleExample }> = ({ example }) => {
|
||||||
|
if (example.pitIndex === undefined) {
|
||||||
|
return <div className="rboard single"><MiniBoard board={example.board} /></div>;
|
||||||
|
}
|
||||||
|
const outcome = getMoveOutcome(example.board, example.pitIndex);
|
||||||
|
return (
|
||||||
|
<div className="rboard">
|
||||||
|
<MiniBoard board={example.board} played={example.pitIndex} />
|
||||||
|
<span className="arrow"><NextIcon /></span>
|
||||||
|
<MiniBoard board={outcome.board} landed={outcome.path[outcome.path.length - 1]} />
|
||||||
|
<style jsx>{`
|
||||||
|
.rboard {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.arrow {
|
||||||
|
color: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.rboard {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
.arrow {
|
||||||
|
justify-content: center;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RuleBoard;
|
||||||
52
apps/frontend/src/components/rules/RulesPanel.tsx
Normal file
52
apps/frontend/src/components/rules/RulesPanel.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CloseIcon } from "../Icons";
|
||||||
|
import RulesView from "./RulesView";
|
||||||
|
|
||||||
|
// The rules over the game page, so a player can look a rule up without leaving the game (R1.4).
|
||||||
|
const RulesPanel: FunctionComponent<{ onClose: () => void }> = ({ onClose }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="rpanel-back" onClick={onClose}>
|
||||||
|
<aside className="rpanel" role="dialog" aria-label={t("HowToPlay")} onClick={(event) => event.stopPropagation()}>
|
||||||
|
<div className="rhead">
|
||||||
|
<h1>{t("HowToPlay")}</h1>
|
||||||
|
<button className="btn icon ghost" aria-label={t("Close")} onClick={onClose}><CloseIcon /></button>
|
||||||
|
</div>
|
||||||
|
<RulesView />
|
||||||
|
</aside>
|
||||||
|
<style jsx>{`
|
||||||
|
.rpanel-back {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background: rgba(0, 0, 0, .45);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.rpanel {
|
||||||
|
width: min(480px, 100%);
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg);
|
||||||
|
box-shadow: -12px 0 30px rgba(0, 0, 0, .3);
|
||||||
|
}
|
||||||
|
.rhead {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RulesPanel;
|
||||||
58
apps/frontend/src/components/rules/RulesView.tsx
Normal file
58
apps/frontend/src/components/rules/RulesView.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { RULES } from "@mancala/core";
|
||||||
|
import RuleBoard from "./RuleBoard";
|
||||||
|
|
||||||
|
// Every rule in order with its text and example (docs/specs/tutorial, R1). `showLessons` adds a link to each lesson;
|
||||||
|
// left out where following it would leave a game.
|
||||||
|
const RulesView: FunctionComponent<{ showLessons?: boolean }> = ({ showLessons }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<ol className="rules">
|
||||||
|
{RULES.map((rule) => (
|
||||||
|
<li key={rule.id} id={rule.id} className="rule panel">
|
||||||
|
<h2>{t(rule.titleKey)}</h2>
|
||||||
|
{rule.textKeys.map((key) => <p key={key}>{t(key)}</p>)}
|
||||||
|
{rule.example && <RuleBoard example={rule.example} />}
|
||||||
|
{showLessons && rule.lesson && (
|
||||||
|
<Link className="btn ghost try" to={`/learn/${rule.id}`}>{t("TryIt")}</Link>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<style jsx>{`
|
||||||
|
.rules {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.rule {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
letter-spacing: -.01em;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.rules :global(.try) {
|
||||||
|
align-self: flex-start;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RulesView;
|
||||||
@ -28,6 +28,7 @@ import GameClockView from '../components/GameClockView';
|
|||||||
import useGameClock from '../hooks/useGameClock';
|
import useGameClock from '../hooks/useGameClock';
|
||||||
import PlayerCard from '../components/game/PlayerCard';
|
import PlayerCard from '../components/game/PlayerCard';
|
||||||
import GameOverCard from '../components/game/GameOverCard';
|
import GameOverCard from '../components/game/GameOverCard';
|
||||||
|
import RulesPanel from '../components/rules/RulesPanel';
|
||||||
import '../styles/game.css';
|
import '../styles/game.css';
|
||||||
|
|
||||||
const GamePage: FunctionComponent<{
|
const GamePage: FunctionComponent<{
|
||||||
@ -61,6 +62,8 @@ const GamePage: FunctionComponent<{
|
|||||||
|
|
||||||
// Set when the server says the game ended, for whatever reason.
|
// Set when the server says the game ended, for whatever reason.
|
||||||
const [gameEnd, setGameEnd] = useState<GameEndMessage | undefined>(undefined);
|
const [gameEnd, setGameEnd] = useState<GameEndMessage | undefined>(undefined);
|
||||||
|
// The rules over the game; the game goes on behind them.
|
||||||
|
const [rulesOpen, setRulesOpen] = useState(false);
|
||||||
|
|
||||||
// Set when the player closes the game over card to look at the board.
|
// Set when the player closes the game over card to look at the board.
|
||||||
const [gameOverClosed, setGameOverClosed] = useState<boolean>(false);
|
const [gameOverClosed, setGameOverClosed] = useState<boolean>(false);
|
||||||
@ -419,11 +422,12 @@ const GamePage: FunctionComponent<{
|
|||||||
{showBoardView && renderGame()}
|
{showBoardView && renderGame()}
|
||||||
<LoadingComponent loadingState={gameLoadingState}></LoadingComponent>
|
<LoadingComponent loadingState={gameLoadingState}></LoadingComponent>
|
||||||
</main>
|
</main>
|
||||||
|
{rulesOpen && <RulesPanel onClose={() => setRulesOpen(false)} />}
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
function renderHeaderBar() {
|
function renderHeaderBar() {
|
||||||
return <AppHeader context={context}>
|
return <AppHeader context={context} onHelp={() => setRulesOpen(true)}>
|
||||||
{renderNewGameBtn
|
{renderNewGameBtn
|
||||||
? <button className="btn pri ic-m" aria-label={t("NewGame")} onClick={onNewGameClick}>
|
? <button className="btn pri ic-m" aria-label={t("NewGame")} onClick={onNewGameClick}>
|
||||||
<PlusIcon /><span className="hide-m">{t("NewGame")}</span>
|
<PlusIcon /><span className="hide-m">{t("NewGame")}</span>
|
||||||
|
|||||||
37
apps/frontend/src/routes/RulesPage.tsx
Normal file
37
apps/frontend/src/routes/RulesPage.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Theme } from "@mancala/core";
|
||||||
|
import AppHeader from "../components/headerbar/AppHeader";
|
||||||
|
import PageContainer from "../components/PageContainer";
|
||||||
|
import RulesView from "../components/rules/RulesView";
|
||||||
|
import { Context } from "../context/context";
|
||||||
|
|
||||||
|
// How to play: every rule with an example (docs/specs/tutorial, R1).
|
||||||
|
const RulesPage: FunctionComponent<{ context: Context, theme: Theme }> = ({ context }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<AppHeader context={context} />
|
||||||
|
<main className="wrap rules-page">
|
||||||
|
<h1>{t("HowToPlay")}</h1>
|
||||||
|
<RulesView />
|
||||||
|
</main>
|
||||||
|
<style jsx>{`
|
||||||
|
.rules-page {
|
||||||
|
max-width: 760px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 30px;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RulesPage;
|
||||||
@ -5,7 +5,7 @@ Work through the tasks in order; each ends with its tests passing and a commit.
|
|||||||
- [x] 1. **Spec**: requirements, design and tasks.
|
- [x] 1. **Spec**: requirements, design and tasks.
|
||||||
- [x] 2. **Core**: `RULES` with examples and lessons, `getMoveOutcome`, `fulfilsTask`, `explainMove`, `captured` and
|
- [x] 2. **Core**: `RULES` with examples and lessons, `getMoveOutcome`, `fulfilsTask`, `explainMove`, `captured` and
|
||||||
the capture kind in `describeMove`, the storage helpers, texts in English and Turkish; unit tests.
|
the capture kind in `describeMove`, the storage helpers, texts in English and Turkish; unit tests.
|
||||||
- [ ] 3. **Rules on the web**: `RuleBoard`, `RulesView`, `/rules`, the help button in `AppHeader`, `RulesPanel` on the
|
- [x] 3. **Rules on the web**: `RuleBoard`, `RulesView`, `/rules`, the help button in `AppHeader`, `RulesPanel` on the
|
||||||
game page.
|
game page.
|
||||||
- [ ] 4. **Lessons**: `/learn` with progress, `LessonPage` with the local game, animation, praise, explanation and
|
- [ ] 4. **Lessons**: `/learn` with progress, `LessonPage` with the local game, animation, praise, explanation and
|
||||||
reset, scripted opponent replies.
|
reset, scripted opponent replies.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user