Release: Quarkus backend, Keycloak login, game history, clock, bot, tutorial and rematch #3
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 BotLevelMenu from "../components/BotLevelMenu";
|
||||
import ActiveGameBanner from "../components/home/ActiveGameBanner";
|
||||
import TutorialBanner from "../components/home/TutorialBanner";
|
||||
import LiveGamesSection from "../components/home/LiveGamesSection";
|
||||
import RecentGamesSection from "../components/home/RecentGamesSection";
|
||||
import PopularGamesSection from "../components/home/PopularGamesSection";
|
||||
@ -58,6 +59,7 @@ const Home: FunctionComponent<{
|
||||
</button>
|
||||
</AppHeader>
|
||||
<main className="wrap">
|
||||
<TutorialBanner context={context} />
|
||||
{activeGame && authUser && (
|
||||
<ActiveGameBanner
|
||||
game={activeGame}
|
||||
|
||||
@ -6,6 +6,7 @@ import { getCompletedLessons, hideTutorialBanner, LESSON_RULES, Theme } from "@m
|
||||
import AppHeader from "../components/headerbar/AppHeader";
|
||||
import PageContainer from "../components/PageContainer";
|
||||
import { Context } from "../context/context";
|
||||
import PracticeGameCard from "../components/PracticeGameCard";
|
||||
|
||||
// The lessons in order, the ones done marked (docs/specs/tutorial, R2.6).
|
||||
const LearnPage: FunctionComponent<{ context: Context, theme: Theme }> = ({ context }) => {
|
||||
@ -31,6 +32,7 @@ const LearnPage: FunctionComponent<{ context: Context, theme: Theme }> = ({ cont
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<PracticeGameCard context={context} />
|
||||
<Link className="rules-link" to="/rules">{t("HowToPlay")}</Link>
|
||||
</main>
|
||||
<style jsx>{`
|
||||
|
||||
@ -12,6 +12,7 @@ import AppHeader from "../components/headerbar/AppHeader";
|
||||
import PageContainer from "../components/PageContainer";
|
||||
import BoardView from "../components/board/BoardView";
|
||||
import { Context } from "../context/context";
|
||||
import PracticeGameCard from "../components/PracticeGameCard";
|
||||
|
||||
const YOU = "you";
|
||||
const OPPONENT = "opponent";
|
||||
@ -130,6 +131,7 @@ const LessonPage: FunctionComponent<{ context: Context, theme: Theme }> = ({ con
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{fulfilled && !next && <PracticeGameCard context={context} />}
|
||||
{game && boardViewModel && (
|
||||
<div className="lboard">
|
||||
<BoardView
|
||||
|
||||
@ -5,6 +5,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import AppHeader from '../components/headerbar/AppHeader';
|
||||
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 { markGamePlayed } from '@mancala/core';
|
||||
import BotLevelPicker from '../components/BotLevelPicker';
|
||||
import { Context } from '../context/context';
|
||||
import { Theme } from '@mancala/core';
|
||||
@ -35,6 +36,7 @@ const LobyPage: FunctionComponent<{
|
||||
|
||||
const onGameStart = (message: Object) => {
|
||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||
markGamePlayed(context.storage);
|
||||
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.
|
||||
- [x] 4. **Lessons**: `/learn` with progress, `LessonPage` with the local game, animation, praise, explanation and
|
||||
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`.
|
||||
- [ ] 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user