feat(frontend): offer the tutorial to newcomers and a practice game
The home page shows newcomers a banner to start the lessons; it goes away for good after Not now, a lesson or a game. The lessons end with a practice game against the Easy bot with hints on.
This commit is contained in:
parent
9beb8ba7d4
commit
d444447425
47
apps/frontend/src/components/PracticeGameCard.tsx
Normal file
47
apps/frontend/src/components/PracticeGameCard.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { setHintsEnabled } from "@mancala/core";
|
||||||
|
import { Context } from "../context/context";
|
||||||
|
import Util from "../util/Util";
|
||||||
|
|
||||||
|
// The tutorial's last step: a game against the Easy bot with hints on (R3.1).
|
||||||
|
const PracticeGameCard: FunctionComponent<{ context: Context }> = ({ context }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const start = () => {
|
||||||
|
if (Util.checkConnectionAndMaybeAlert(context, t)) return;
|
||||||
|
setHintsEnabled(context.storage, true);
|
||||||
|
navigate("/loby?bot=easy");
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<section className="practice panel">
|
||||||
|
<div>
|
||||||
|
<strong>{t("PracticeGame")}</strong>
|
||||||
|
<p>{t("PracticeGameText")}</p>
|
||||||
|
</div>
|
||||||
|
<button className="btn pri" onClick={start}>{t("PlayWithBot")}</button>
|
||||||
|
<style jsx>{`
|
||||||
|
.practice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
strong {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PracticeGameCard;
|
||||||
47
apps/frontend/src/components/home/TutorialBanner.tsx
Normal file
47
apps/frontend/src/components/home/TutorialBanner.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { hideTutorialBanner, shouldShowTutorialBanner } from "@mancala/core";
|
||||||
|
import { Context } from "../../context/context";
|
||||||
|
|
||||||
|
// For newcomers: an offer to learn the game, until they open the lessons, play a game or say not now (R4).
|
||||||
|
const TutorialBanner: FunctionComponent<{ context: Context }> = ({ context }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [visible, setVisible] = useState(() => shouldShowTutorialBanner(context.storage));
|
||||||
|
if (!visible) return null;
|
||||||
|
return (
|
||||||
|
<section className="tbanner panel">
|
||||||
|
<strong>{t("NewToMangala")}</strong>
|
||||||
|
<div className="actions">
|
||||||
|
<button className="btn pri" onClick={() => navigate("/learn")}>{t("StartTutorial")}</button>
|
||||||
|
<button className="btn ghost" onClick={() => {
|
||||||
|
hideTutorialBanner(context.storage);
|
||||||
|
setVisible(false);
|
||||||
|
}}>{t("NotNow")}</button>
|
||||||
|
</div>
|
||||||
|
<style jsx>{`
|
||||||
|
.tbanner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--accent);
|
||||||
|
}
|
||||||
|
strong {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TutorialBanner;
|
||||||
@ -7,6 +7,7 @@ import PageContainer from "../components/PageContainer";
|
|||||||
import { PlusIcon } from "../components/Icons";
|
import { PlusIcon } from "../components/Icons";
|
||||||
import BotLevelMenu from "../components/BotLevelMenu";
|
import BotLevelMenu from "../components/BotLevelMenu";
|
||||||
import ActiveGameBanner from "../components/home/ActiveGameBanner";
|
import ActiveGameBanner from "../components/home/ActiveGameBanner";
|
||||||
|
import TutorialBanner from "../components/home/TutorialBanner";
|
||||||
import LiveGamesSection from "../components/home/LiveGamesSection";
|
import LiveGamesSection from "../components/home/LiveGamesSection";
|
||||||
import RecentGamesSection from "../components/home/RecentGamesSection";
|
import RecentGamesSection from "../components/home/RecentGamesSection";
|
||||||
import PopularGamesSection from "../components/home/PopularGamesSection";
|
import PopularGamesSection from "../components/home/PopularGamesSection";
|
||||||
@ -58,6 +59,7 @@ const Home: FunctionComponent<{
|
|||||||
</button>
|
</button>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
<main className="wrap">
|
<main className="wrap">
|
||||||
|
<TutorialBanner context={context} />
|
||||||
{activeGame && authUser && (
|
{activeGame && authUser && (
|
||||||
<ActiveGameBanner
|
<ActiveGameBanner
|
||||||
game={activeGame}
|
game={activeGame}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { getCompletedLessons, hideTutorialBanner, LESSON_RULES, Theme } from "@m
|
|||||||
import AppHeader from "../components/headerbar/AppHeader";
|
import AppHeader from "../components/headerbar/AppHeader";
|
||||||
import PageContainer from "../components/PageContainer";
|
import PageContainer from "../components/PageContainer";
|
||||||
import { Context } from "../context/context";
|
import { Context } from "../context/context";
|
||||||
|
import PracticeGameCard from "../components/PracticeGameCard";
|
||||||
|
|
||||||
// The lessons in order, the ones done marked (docs/specs/tutorial, R2.6).
|
// The lessons in order, the ones done marked (docs/specs/tutorial, R2.6).
|
||||||
const LearnPage: FunctionComponent<{ context: Context, theme: Theme }> = ({ context }) => {
|
const LearnPage: FunctionComponent<{ context: Context, theme: Theme }> = ({ context }) => {
|
||||||
@ -31,6 +32,7 @@ const LearnPage: FunctionComponent<{ context: Context, theme: Theme }> = ({ cont
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
|
<PracticeGameCard context={context} />
|
||||||
<Link className="rules-link" to="/rules">{t("HowToPlay")}</Link>
|
<Link className="rules-link" to="/rules">{t("HowToPlay")}</Link>
|
||||||
</main>
|
</main>
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import AppHeader from "../components/headerbar/AppHeader";
|
|||||||
import PageContainer from "../components/PageContainer";
|
import PageContainer from "../components/PageContainer";
|
||||||
import BoardView from "../components/board/BoardView";
|
import BoardView from "../components/board/BoardView";
|
||||||
import { Context } from "../context/context";
|
import { Context } from "../context/context";
|
||||||
|
import PracticeGameCard from "../components/PracticeGameCard";
|
||||||
|
|
||||||
const YOU = "you";
|
const YOU = "you";
|
||||||
const OPPONENT = "opponent";
|
const OPPONENT = "opponent";
|
||||||
@ -130,6 +131,7 @@ const LessonPage: FunctionComponent<{ context: Context, theme: Theme }> = ({ con
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{fulfilled && !next && <PracticeGameCard context={context} />}
|
||||||
{game && boardViewModel && (
|
{game && boardViewModel && (
|
||||||
<div className="lboard">
|
<div className="lboard">
|
||||||
<BoardView
|
<BoardView
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
|||||||
import AppHeader from '../components/headerbar/AppHeader';
|
import AppHeader from '../components/headerbar/AppHeader';
|
||||||
import PageContainer from '../components/PageContainer';
|
import PageContainer from '../components/PageContainer';
|
||||||
import { BOT_OFFER_DELAY_MS, BotLevel, channel_cancel_new_game, channel_new_bot_game, channel_new_game, channel_on_game_start, channel_on_new_game_rejected, isBotLevel } from '@mancala/core';
|
import { BOT_OFFER_DELAY_MS, BotLevel, channel_cancel_new_game, channel_new_bot_game, channel_new_game, channel_on_game_start, channel_on_new_game_rejected, isBotLevel } from '@mancala/core';
|
||||||
|
import { markGamePlayed } from '@mancala/core';
|
||||||
import BotLevelPicker from '../components/BotLevelPicker';
|
import BotLevelPicker from '../components/BotLevelPicker';
|
||||||
import { Context } from '../context/context';
|
import { Context } from '../context/context';
|
||||||
import { Theme } from '@mancala/core';
|
import { Theme } from '@mancala/core';
|
||||||
@ -35,6 +36,7 @@ const LobyPage: FunctionComponent<{
|
|||||||
|
|
||||||
const onGameStart = (message: Object) => {
|
const onGameStart = (message: Object) => {
|
||||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||||
|
markGamePlayed(context.storage);
|
||||||
navigate(`/game/${newGame.id}`)
|
navigate(`/game/${newGame.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ Work through the tasks in order; each ends with its tests passing and a commit.
|
|||||||
game page.
|
game page.
|
||||||
- [x] 4. **Lessons**: `/learn` with progress, `LessonPage` with the local game, animation, praise, explanation and
|
- [x] 4. **Lessons**: `/learn` with progress, `LessonPage` with the local game, animation, praise, explanation and
|
||||||
reset, scripted opponent replies.
|
reset, scripted opponent replies.
|
||||||
- [ ] 5. **Newcomers and practice game**: `TutorialBanner`, `markGamePlayed`, the practice game button.
|
- [x] 5. **Newcomers and practice game**: `TutorialBanner`, `markGamePlayed`, the practice game button.
|
||||||
- [ ] 6. **Hints**: hint chips on `BoardView`, the explanation line and the "Hints" switch in bot games on `GamePage`.
|
- [ ] 6. **Hints**: hint chips on `BoardView`, the explanation line and the "Hints" switch in bot games on `GamePage`.
|
||||||
- [ ] 7. **Mobile rules**: `RulesScreen`, links from the help and game screens; type check and bundle.
|
- [ ] 7. **Mobile rules**: `RulesScreen`, links from the help and game screens; type check and bundle.
|
||||||
- [ ] 8. **End to end and docs**: the Chrome tests from `design.md`; `AGENTS.md` (specs list), `TODO.md` (out of scope
|
- [ ] 8. **End to end and docs**: the Chrome tests from `design.md`; `AGENTS.md` (specs list), `TODO.md` (out of scope
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user