From 6279b5a820a7ff32ed1e9028569f3164a030cf59 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Fri, 16 Oct 2026 09:35:39 +0300 Subject: [PATCH] feat(frontend): add the new themes and header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ceviz, Gece and Yeşim themes as CSS variables on , Bricolage Grotesque, shared styles, and one header for every page with theme swatches, the account menu and the page's main action. The lobby, privacy page and status panels use the new look. --- apps/frontend/src/MancalaApp.tsx | 18 +- .../frontend/src/components/FloatingPanel.tsx | 46 ++-- apps/frontend/src/components/Icons.tsx | 69 +++++ .../src/components/LoadingComponent.tsx | 45 ++-- .../frontend/src/components/PageContainer.tsx | 24 +- apps/frontend/src/components/Row.tsx | 20 -- .../src/components/headerbar/AccountMenu.tsx | 44 +--- .../src/components/headerbar/AppHeader.tsx | 28 ++ .../src/components/headerbar/HeaderBar.tsx | 21 -- .../components/headerbar/HeaderbarIcon.tsx | 22 -- .../components/headerbar/HeaderbarTitle.tsx | 17 -- .../components/headerbar/ThemeSwatches.tsx | 26 ++ .../components/headerbar/ThemeSwitchMenu.tsx | 64 ----- .../src/components/history/ReplayView.tsx | 26 +- apps/frontend/src/context/context.tsx | 3 +- apps/frontend/src/index.html | 29 +-- apps/frontend/src/routes/GamePage.tsx | 40 +-- apps/frontend/src/routes/Home.tsx | 44 +--- apps/frontend/src/routes/LobyPage.tsx | 118 ++++----- apps/frontend/src/routes/PrivacyPage.tsx | 66 ++--- apps/frontend/src/styles/app.css | 244 ++++++++++++++++++ apps/frontend/src/theme/themes.ts | 94 +++++++ docs/specs/web-redesign/tasks.md | 2 +- 23 files changed, 655 insertions(+), 455 deletions(-) create mode 100644 apps/frontend/src/components/Icons.tsx delete mode 100644 apps/frontend/src/components/Row.tsx create mode 100644 apps/frontend/src/components/headerbar/AppHeader.tsx delete mode 100644 apps/frontend/src/components/headerbar/HeaderBar.tsx delete mode 100644 apps/frontend/src/components/headerbar/HeaderbarIcon.tsx delete mode 100644 apps/frontend/src/components/headerbar/HeaderbarTitle.tsx create mode 100644 apps/frontend/src/components/headerbar/ThemeSwatches.tsx delete mode 100644 apps/frontend/src/components/headerbar/ThemeSwitchMenu.tsx create mode 100644 apps/frontend/src/styles/app.css create mode 100644 apps/frontend/src/theme/themes.ts diff --git a/apps/frontend/src/MancalaApp.tsx b/apps/frontend/src/MancalaApp.tsx index b381e91..bf2a1f8 100644 --- a/apps/frontend/src/MancalaApp.tsx +++ b/apps/frontend/src/MancalaApp.tsx @@ -11,7 +11,6 @@ import Home from './routes/Home'; import { initContext } from './context/context'; import { RTMTWS, ConnectionState, AuthUser } from '@mancala/core'; -import { getColorByBrightness } from "@mancala/core"; import { Theme } from '@mancala/core'; import LobyPage from './routes/LobyPage'; import PrivacyPage from './routes/PrivacyPage'; @@ -19,6 +18,8 @@ import swal from 'sweetalert'; import Util from './util/Util'; import { server } from './const/config'; import { useTranslation } from 'react-i18next'; +import { applyThemeClass } from './theme/themes'; +import './styles/app.css'; const context = initContext(); @@ -42,7 +43,10 @@ const MancalaApp: FunctionComponent = () => { const onConnectionChange = (_connectionState: ConnectionState) => setConnetionState(_connectionState); - const onThemeChange = (theme: Theme) => setTheme(theme); + const onThemeChange = (theme: Theme) => { + applyThemeClass(theme); + setTheme(theme); + }; React.useEffect(() => { //@ts-ignore @@ -99,6 +103,7 @@ const MancalaApp: FunctionComponent = () => { } React.useEffect(() => { + applyThemeClass(context.themeManager.theme); startSession().catch((error) => { swal(t("Error") + "!", t("ErrorWhenRetrievingInformation"), "error"); console.error(error); @@ -112,11 +117,6 @@ const MancalaApp: FunctionComponent = () => { }; }, []); - const textColorOnBoard = getColorByBrightness( - context.themeManager.theme.boardColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); return ( <> {hasSession && @@ -133,8 +133,8 @@ const MancalaApp: FunctionComponent = () => { } - - {Util.getTextByConnectionState(context, connectionState, t)} + + {Util.getTextByConnectionState(context, connectionState, t)} ); diff --git a/apps/frontend/src/components/FloatingPanel.tsx b/apps/frontend/src/components/FloatingPanel.tsx index 564ec06..779a95d 100644 --- a/apps/frontend/src/components/FloatingPanel.tsx +++ b/apps/frontend/src/components/FloatingPanel.tsx @@ -1,32 +1,38 @@ import * as React from "react"; import { FunctionComponent } from "react"; -import { Context } from "../context/context"; -const FloatingPanel: FunctionComponent<{ - context: Context; - color: string; - visible: boolean; -}> = (props) => { - if(props.visible === false) return <> +// A status pill in the bottom left corner, e.g. while connecting. +const FloatingPanel: FunctionComponent<{ visible: boolean }> = (props) => { + if (props.visible === false) return <> return ( -
+
+ + {props.children} - {props.children}
) }; -export default FloatingPanel; \ No newline at end of file +export default FloatingPanel; diff --git a/apps/frontend/src/components/Icons.tsx b/apps/frontend/src/components/Icons.tsx new file mode 100644 index 0000000..49fadab --- /dev/null +++ b/apps/frontend/src/components/Icons.tsx @@ -0,0 +1,69 @@ +import * as React from "react"; + +// Inline stroke icons in the current text colour. All are decorative; buttons carry their own labels. + +type IconProps = { size?: number, className?: string }; + +const stroke = (size: number, width: number, className?: string) => ({ + width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: width, + strokeLinecap: "round" as const, strokeLinejoin: "round" as const, className, "aria-hidden": true, +}); + +export const LogoIcon = ({ size = 34 }: IconProps) => ( + +); + +export const PlusIcon = ({ size = 20 }: IconProps) => ( + +); + +export const ProfileIcon = ({ size = 20 }: IconProps) => ( + +); + +export const LeaveIcon = ({ size = 20 }: IconProps) => ( + +); + +export const ChevronIcon = ({ size = 20, className }: IconProps) => ( + +); + +export const CloseIcon = ({ size = 20 }: IconProps) => ( + +); + +export const ReplayIcon = ({ size = 18 }: IconProps) => ( + +); + +export const FirstIcon = ({ size = 20 }: IconProps) => ( + +); + +export const PreviousIcon = ({ size = 20 }: IconProps) => ( + +); + +export const NextIcon = ({ size = 20 }: IconProps) => ( + +); + +export const LastIcon = ({ size = 20 }: IconProps) => ( + +); + +export const PlayIcon = ({ size = 22 }: IconProps) => ( + +); + +export const PauseIcon = ({ size = 22 }: IconProps) => ( + +); diff --git a/apps/frontend/src/components/LoadingComponent.tsx b/apps/frontend/src/components/LoadingComponent.tsx index ea0e15c..2538447 100644 --- a/apps/frontend/src/components/LoadingComponent.tsx +++ b/apps/frontend/src/components/LoadingComponent.tsx @@ -1,32 +1,33 @@ import * as React from 'react'; import { FunctionComponent } from 'react'; -import { Context } from '../context/context'; import { LoadingState } from "@mancala/core"; -import { getColorByBrightness } from "@mancala/core"; -import CircularPanel from './CircularPanel'; import { useTranslation } from 'react-i18next'; -const LoadingComponent: FunctionComponent<{ context: Context, loadingState: LoadingState }> = ({ context, loadingState }) => { +const LoadingComponent: FunctionComponent<{ loadingState: LoadingState }> = ({ loadingState }) => { const { t } = useTranslation(); - if (loadingState.isUnset() || loadingState.isLoaded()) { - return <> - } - if (loadingState.isLoading() || loadingState.isError()) { - const textColorOnBoard = getColorByBrightness( - context.themeManager.theme.boardColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); - const text = loadingState.isLoading() ? t("Loading") +"..." : loadingState.errorMessage; - return ( - -

{`${text}`}

-
- ); - } - return <> + if (!loadingState.isLoading() && !loadingState.isError()) return <>; + const text = loadingState.isLoading() ? t("Loading") + "..." : loadingState.errorMessage; + return ( +
+ {loadingState.isLoading() && } + {text} + +
+ ); } -export default LoadingComponent; \ No newline at end of file +export default LoadingComponent; diff --git a/apps/frontend/src/components/PageContainer.tsx b/apps/frontend/src/components/PageContainer.tsx index 8740ee2..6db896a 100644 --- a/apps/frontend/src/components/PageContainer.tsx +++ b/apps/frontend/src/components/PageContainer.tsx @@ -1,25 +1,7 @@ import * as React from "react"; import { FunctionComponent } from "react"; -import { Theme } from "@mancala/core"; -const PageContainer: FunctionComponent<{ theme: Theme }> = (props) => { - return ( -
- - {props.children} -
- ); -} +// A page: the header, then the page's content. +const PageContainer: FunctionComponent = ({ children }) =>
{children}
; -export default PageContainer; \ No newline at end of file +export default PageContainer; diff --git a/apps/frontend/src/components/Row.tsx b/apps/frontend/src/components/Row.tsx deleted file mode 100644 index bc0db42..0000000 --- a/apps/frontend/src/components/Row.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; -import { FunctionComponent } from 'react'; - -const Row: FunctionComponent = ({children}) => { - return ( -
- {children} - -
- ); -} - -export default Row; \ No newline at end of file diff --git a/apps/frontend/src/components/headerbar/AccountMenu.tsx b/apps/frontend/src/components/headerbar/AccountMenu.tsx index dc20841..f971cf6 100644 --- a/apps/frontend/src/components/headerbar/AccountMenu.tsx +++ b/apps/frontend/src/components/headerbar/AccountMenu.tsx @@ -4,46 +4,30 @@ import { FunctionComponent } from "react"; import { AuthUser, getPlayerDisplayName } from "@mancala/core"; import { useTranslation } from "react-i18next"; import { Context } from "../../context/context"; +import { ProfileIcon } from "../Icons"; import "@szhsin/react-menu/dist/index.css"; import "@szhsin/react-menu/dist/transitions/slide.css" // Guests can log in or sign up; logged in users can log out. -const AccountMenu: FunctionComponent<{ context: Context, textColor: string, authUser?: AuthUser }> = (props) => { - const { context, textColor, authUser } = props; +const AccountMenu: FunctionComponent<{ context: Context, authUser: AuthUser }> = ({ context, authUser }) => { const { t } = useTranslation(); - if (!authUser) return <>; - const authService = context.authService; - const menuButton = - account_circle - ; - const itemStyle = { color: textColor }; + const menuButton = ( + + ); const menuItems = authUser.isGuest ? [ - authService.login()}>{t("LogIn")}, - authService.signUp()}>{t("SignUp")}, + authService.login()}>{t("LogIn")}, + authService.signUp()}>{t("SignUp")}, ] : [ - authService.logout()}>{t("LogOut")}, + authService.logout()}>{t("LogOut")}, ]; return ( -
- - {getPlayerDisplayName(authUser, t)} - {menuItems} - - -
+ + {getPlayerDisplayName(authUser, t)} + {menuItems} + ); } diff --git a/apps/frontend/src/components/headerbar/AppHeader.tsx b/apps/frontend/src/components/headerbar/AppHeader.tsx new file mode 100644 index 0000000..236e159 --- /dev/null +++ b/apps/frontend/src/components/headerbar/AppHeader.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { FunctionComponent } from "react"; +import { Link } from "react-router-dom"; +import { AuthUser } from "@mancala/core"; +import { useTranslation } from "react-i18next"; +import { Context } from "../../context/context"; +import { LogoIcon } from "../Icons"; +import AccountMenu from "./AccountMenu"; +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 +// main action (children). +const AppHeader: FunctionComponent<{ context: Context, authUser?: AuthUser }> = ({ context, authUser, children }) => { + const { t } = useTranslation(); + return ( +
+ + + {t("Mancala")} + + + {authUser && } + {children} +
+ ); +}; + +export default AppHeader; diff --git a/apps/frontend/src/components/headerbar/HeaderBar.tsx b/apps/frontend/src/components/headerbar/HeaderBar.tsx deleted file mode 100644 index 683f8c2..0000000 --- a/apps/frontend/src/components/headerbar/HeaderBar.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; -import { FunctionComponent } from "react";; - -const HeaderBar: FunctionComponent<{ color?: string }> = ({children, color }) => { - return ( -
- {children} - -
) -} -export default HeaderBar; diff --git a/apps/frontend/src/components/headerbar/HeaderbarIcon.tsx b/apps/frontend/src/components/headerbar/HeaderbarIcon.tsx deleted file mode 100644 index b012df0..0000000 --- a/apps/frontend/src/components/headerbar/HeaderbarIcon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from "react"; -import { FunctionComponent } from "react"; -//@ts-ignore -import MancalaIcon from "jsx:../../mancala.svg"; - -const HeaderbarIcon: FunctionComponent = () => { - return ( -
- - -
- ); -} - -export default HeaderbarIcon; \ No newline at end of file diff --git a/apps/frontend/src/components/headerbar/HeaderbarTitle.tsx b/apps/frontend/src/components/headerbar/HeaderbarTitle.tsx deleted file mode 100644 index 73e2bb0..0000000 --- a/apps/frontend/src/components/headerbar/HeaderbarTitle.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import { FunctionComponent } from 'react'; - -const HeaderbarTitle: FunctionComponent<{ title: string, color: string }> = ({ title, color }) => { - return ( -

- {title} - -

- ); -} - -export default HeaderbarTitle; \ No newline at end of file diff --git a/apps/frontend/src/components/headerbar/ThemeSwatches.tsx b/apps/frontend/src/components/headerbar/ThemeSwatches.tsx new file mode 100644 index 0000000..add43d9 --- /dev/null +++ b/apps/frontend/src/components/headerbar/ThemeSwatches.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { FunctionComponent } from "react"; +import { useTranslation } from "react-i18next"; +import { Context } from "../../context/context"; +import { getWebTheme, webThemes } from "../../theme/themes"; + +const ThemeSwatches: FunctionComponent<{ context: Context }> = ({ context }) => { + const { t } = useTranslation(); + const current = getWebTheme(context.themeManager.theme); + return ( +
+ {webThemes.map((theme) => ( + + ))} +
+ ); +}; + +export default ThemeSwatches; diff --git a/apps/frontend/src/components/headerbar/ThemeSwitchMenu.tsx b/apps/frontend/src/components/headerbar/ThemeSwitchMenu.tsx deleted file mode 100644 index 6e45818..0000000 --- a/apps/frontend/src/components/headerbar/ThemeSwitchMenu.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Menu, MenuItem } from "@szhsin/react-menu"; -import * as React from "react"; -import { FunctionComponent } from "react"; -import { Context } from "../../context/context"; -import "@szhsin/react-menu/dist/index.css"; -import "@szhsin/react-menu/dist/transitions/slide.css" - -const ThemeSwitchMenu: FunctionComponent<{ context: Context, textColor: string }> = (props) => { - const { context, textColor } = props; - const menuButton = - light_mode - ; - const menuItems = context.themeManager.themes.map((theme, index) => { - return ( - { - const htmlElement: HTMLElement = event.target as HTMLElement; - if (htmlElement.localName === "li") htmlElement.style.background = "transparent"; - }} - //@ts-ignore - onMouseOut={(event) => { - const htmlElement: HTMLElement = event.target as HTMLElement; - if (htmlElement.localName === "li") htmlElement.style.background = "transparent"; - }} - onClick={() => (context.themeManager.theme = theme)}> -
- {theme.name} - - - ); - }) - return ( -
- - {menuItems} - - -
- ); -} - -export default ThemeSwitchMenu; \ No newline at end of file diff --git a/apps/frontend/src/components/history/ReplayView.tsx b/apps/frontend/src/components/history/ReplayView.tsx index f3a2f4f..0a03491 100644 --- a/apps/frontend/src/components/history/ReplayView.tsx +++ b/apps/frontend/src/components/history/ReplayView.tsx @@ -24,13 +24,10 @@ import { formatDate, formatDuration, getResultText, getScoreText, getViewerSeat import { getMoveText } from "../../util/MoveText"; import BoardView from "../board/BoardView"; import Button from "../Button"; -import HeaderBar from "../headerbar/HeaderBar"; -import HeaderbarIcon from "../headerbar/HeaderbarIcon"; -import HeaderbarTitle from "../headerbar/HeaderbarTitle"; -import ThemeSwitchMenu from "../headerbar/ThemeSwitchMenu"; import PageContainer from "../PageContainer"; +import AppHeader from "../headerbar/AppHeader"; +import { PlusIcon } from "../Icons"; import GameClockView from "../GameClockView"; -import Row from "../Row"; import MoveList from "./MoveList"; const SPEEDS = [1, 2, 4]; @@ -166,19 +163,12 @@ const ReplayView: FunctionComponent<{ context: Context, theme: Theme, record: Ga ); return ( - - - - - - - - - - - +
diff --git a/apps/frontend/src/context/context.tsx b/apps/frontend/src/context/context.tsx index b848039..e881751 100644 --- a/apps/frontend/src/context/context.tsx +++ b/apps/frontend/src/context/context.tsx @@ -5,6 +5,7 @@ import { GameStore, GameStoreImpl } from "../store/GameStore"; import { AuthService } from "../auth/AuthService"; import { LocalStorage } from "../storage"; import { Storage } from '@mancala/core' +import { webThemes } from "../theme/themes"; export type Context = { rtmt: RTMT; @@ -20,7 +21,7 @@ export const initContext = (): Context => { const authService = new AuthService({ keycloakUrl: keycloak.url, realm: keycloak.realm, clientId: keycloak.clientId }); const gameStore = new GameStoreImpl({ httpService }); const storage = new LocalStorage(); - const themeManager = new ThemeManager(storage); + const themeManager = new ThemeManager(storage, webThemes); return { rtmt: rtmt, authService, diff --git a/apps/frontend/src/index.html b/apps/frontend/src/index.html index 3e12ef0..156bef2 100644 --- a/apps/frontend/src/index.html +++ b/apps/frontend/src/index.html @@ -1,31 +1,18 @@ - + Mancala + + + - -
+ +
- - - - - \ No newline at end of file + diff --git a/apps/frontend/src/routes/GamePage.tsx b/apps/frontend/src/routes/GamePage.tsx index e0e776e..44840ee 100644 --- a/apps/frontend/src/routes/GamePage.tsx +++ b/apps/frontend/src/routes/GamePage.tsx @@ -2,20 +2,15 @@ import { MancalaGame, Pit } from 'mancala.js'; import * as React from 'react'; import { FunctionComponent, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; -import { Link } from 'react-router-dom'; import { v4 } from 'uuid'; import { PitAnimator } from '@mancala/core'; import BoardToolbar from '../components/board/BoardToolbar'; import BoardView from '../components/board/BoardView'; -import Button from '../components/Button'; -import HeaderBar from '../components/headerbar/HeaderBar'; -import HeaderbarIcon from '../components/headerbar/HeaderbarIcon'; -import HeaderbarTitle from '../components/headerbar/HeaderbarTitle'; -import ThemeSwitchMenu from '../components/headerbar/ThemeSwitchMenu'; import InfoPanel from '../components/InfoPanel'; import LoadingComponent from '../components/LoadingComponent'; import PageContainer from '../components/PageContainer'; -import Row from '../components/Row'; +import AppHeader from '../components/headerbar/AppHeader'; +import { LeaveIcon, PlusIcon } from '../components/Icons'; import UserStatus from '../components/UserStatus'; import { GameRecord, channel_on_game_update, channel_on_game_crashed, channel_on_game_end, channel_on_game_user_leave, channel_on_user_connection_change, channel_leave_game, channel_game_move, channel_listen_game_events, channel_unlisten_game_events, FinishedGameSummary, GameEndMessage, getBoardAfterMove, getSeat, PlayedMove } from '@mancala/core'; import { Context } from '../context/context'; @@ -341,7 +336,7 @@ const GamePage: FunctionComponent<{ } return ( - + {renderHeaderBar()} {renderMobileBoardToolbar()} {buildBoardTopToolbar()} @@ -371,30 +366,21 @@ const GamePage: FunctionComponent<{ collapsedByDefault={isMobile} /> )}
- +
); function renderHeaderBar() { - return - - - - - - - - - - - + : } + ; } function renderMobileBoardToolbar() { diff --git a/apps/frontend/src/routes/Home.tsx b/apps/frontend/src/routes/Home.tsx index d3959cf..b9bc59f 100644 --- a/apps/frontend/src/routes/Home.tsx +++ b/apps/frontend/src/routes/Home.tsx @@ -1,20 +1,15 @@ import * as React from "react"; import { FunctionComponent, useEffect, useState } from "react"; -import { getColorByBrightness, LiveGameSummary } from "@mancala/core"; +import { LiveGameSummary } from "@mancala/core"; import { AuthUser, Theme } from "@mancala/core"; -import HeaderBar from "../components/headerbar/HeaderBar"; +import AppHeader from "../components/headerbar/AppHeader"; import PageContainer from "../components/PageContainer"; -import Row from "../components/Row"; -import HeaderbarIcon from "../components/headerbar/HeaderbarIcon"; -import HeaderbarTitle from "../components/headerbar/HeaderbarTitle"; -import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu"; -import AccountMenu from "../components/headerbar/AccountMenu"; +import { PlusIcon } from "../components/Icons"; import ActiveGameBanner from "../components/home/ActiveGameBanner"; import LiveGamesSection from "../components/home/LiveGamesSection"; import RecentGamesSection from "../components/home/RecentGamesSection"; -import Button from "../components/Button"; import { Context } from "../context/context"; -import { Link, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import Util from "../util/Util"; import { useTranslation } from "react-i18next"; @@ -46,32 +41,13 @@ const Home: FunctionComponent<{ navigate("/loby") }; - const textColorOnAppBar = getColorByBrightness( - context.themeManager.theme.appBarBgColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); return ( - - - - - - - - - - - - - - + {activeGame && authUser && ( = ({ context, userKey, theme }) => { +}> = ({ context }) => { let navigate = useNavigate(); @@ -52,58 +44,60 @@ const LobyPage: FunctionComponent<{ context.rtmt.removeMessageListener(channel_on_new_game_rejected, onNewGameRejected); } }, []); - - const textColorOnAppBar = getColorByBrightness( - context.themeManager.theme.appBarBgColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); - const textColorOnBoard = getColorByBrightness( - context.themeManager.theme.boardColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); - return ( - - - - - - - - - - - - - - -
-
- -

- {activeGameId ? t("GameInProgress") : `${t("SearchingOpponent")} ${t("PleaseWait")}...`} -

-
- {activeGameId &&
-
+ return ( + + +
+
+ {!activeGameId && } +

{activeGameId ? t("GameInProgress") : t("SearchingOpponent")}

+ {!activeGameId &&

{`${t("PleaseWait")}...`}

} +
+ {activeGameId && ( + + )} + +
+
+
+
); } -export default LobyPage; \ No newline at end of file +export default LobyPage; diff --git a/apps/frontend/src/routes/PrivacyPage.tsx b/apps/frontend/src/routes/PrivacyPage.tsx index a5096e0..177dd1a 100644 --- a/apps/frontend/src/routes/PrivacyPage.tsx +++ b/apps/frontend/src/routes/PrivacyPage.tsx @@ -1,56 +1,19 @@ import * as React from 'react'; -import { FunctionComponent, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import HeaderBar from '../components/headerbar/HeaderBar'; -import HeaderbarIcon from '../components/headerbar/HeaderbarIcon'; -import HeaderbarTitle from '../components/headerbar/HeaderbarTitle'; -import ThemeSwitchMenu from '../components/headerbar/ThemeSwitchMenu'; +import { FunctionComponent } from 'react'; +import AppHeader from '../components/headerbar/AppHeader'; import PageContainer from '../components/PageContainer'; -import Row from '../components/Row'; import { Context } from '../context/context'; import { Theme } from '@mancala/core'; -import { getColorByBrightness } from "@mancala/core"; -import { useTranslation } from 'react-i18next'; const PrivacyPage: FunctionComponent<{ context: Context, userKey?: string, theme: Theme -}> = ({ context, userKey, theme }) => { - - const { t } = useTranslation(); - - const textColorOnAppBar = getColorByBrightness( - context.themeManager.theme.appBarBgColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); - const textColorOnBoard = getColorByBrightness( - context.themeManager.theme.background, - "#000", - "#aaa" - ); +}> = ({ context }) => { return ( - - - - - - - - - - - - - - -
- + + +

Privacy Policy

The Mancala team and volunteers built the Mancala app as a open source app. This SERVICE is provided by mancala team and volunteers at no cost and is intended for use as is.

@@ -82,9 +45,22 @@ const PrivacyPage: FunctionComponent<{

Contact Us

If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.

-
+ +
); } -export default PrivacyPage; \ No newline at end of file +export default PrivacyPage; diff --git a/apps/frontend/src/styles/app.css b/apps/frontend/src/styles/app.css new file mode 100644 index 0000000..99c6653 --- /dev/null +++ b/apps/frontend/src/styles/app.css @@ -0,0 +1,244 @@ +/* Themes, base styles and the building blocks shared by the pages. The theme class is set on . */ + +.t-ceviz { + --bg: #1f1712; --surface: #2a2019; --surface2: #382a20; --line: rgba(246, 234, 216, .10); + --ink: #f6ead8; --muted: #c4ad93; --accent: #f2b84b; --onAccent: #2a1a0a; + --accentSoft: rgba(242, 184, 75, .45); --accentGlow: rgba(242, 184, 75, .35); --lastRing: rgba(246, 234, 216, .55); + --chip: #1a110b; --chipInk: #f6ead8; --rim: #8a5d3b; --pit: #2c1d13; --live: #ff8a73; + --miniBg: #5b3d28; --miniPit: #2c1d13; + --boardBg: repeating-linear-gradient(94deg, rgba(255, 255, 255, .025) 0 3px, rgba(0, 0, 0, .04) 3px 9px, rgba(255, 255, 255, .015) 9px 14px), linear-gradient(180deg, #6e4a30, #573822); + --boardShadow: inset 0 1px 0 rgba(255, 255, 255, .12), inset 0 -3px 0 rgba(0, 0, 0, .25), 0 26px 50px rgba(0, 0, 0, .45), 0 0 0 1px #3a2516; + --pitShadow: inset 0 8px 16px rgba(0, 0, 0, .6), inset 0 -2px 0 rgba(255, 255, 255, .07), 0 1px 0 rgba(255, 255, 255, .08); + --stoneShadow: 0 3px 4px rgba(0, 0, 0, .45); + color-scheme: dark; +} + +.t-gece { + --bg: #0c1220; --surface: #141c30; --surface2: #1d2744; --line: rgba(238, 241, 248, .09); + --ink: #eef1f8; --muted: #9aa6c2; --accent: #e2b36a; --onAccent: #241707; + --accentSoft: rgba(226, 179, 106, .45); --accentGlow: rgba(226, 179, 106, .35); --lastRing: rgba(238, 241, 248, .5); + --chip: #0a0f1c; --chipInk: #eef1f8; --rim: #b8925a; --pit: #0b1020; --live: #ff8a7a; + --miniBg: #1d2a4d; --miniPit: #0b1020; + --boardBg: linear-gradient(180deg, #1d2a4d, #141d38); + --boardShadow: inset 0 0 0 2px #b8925a, inset 0 0 0 7px #18223f, inset 0 0 0 8px rgba(184, 146, 90, .35), 0 26px 50px rgba(0, 0, 0, .5); + --pitShadow: inset 0 8px 18px rgba(0, 0, 0, .7), inset 0 0 0 1px rgba(184, 146, 90, .28), 0 1px 0 rgba(255, 255, 255, .06); + --stoneShadow: 0 3px 5px rgba(0, 0, 0, .55); + color-scheme: dark; +} + +.t-yesim { + --bg: #e9ebe3; --surface: #f7f8f3; --surface2: #dde0d3; --line: rgba(29, 39, 33, .12); + --ink: #1d2721; --muted: #4f5d55; --accent: #2a7650; --onAccent: #ffffff; + --accentSoft: rgba(42, 118, 80, .4); --accentGlow: rgba(42, 118, 80, .28); --lastRing: rgba(29, 39, 33, .5); + --chip: #1d2721; --chipInk: #ffffff; --rim: #e9ebe3; --pit: #c3c7b6; --live: #b43a26; + --miniBg: #d3d6c8; --miniPit: #bcc0ae; + --boardBg: linear-gradient(180deg, #dcdfd2, #cfd2c3); + --boardShadow: inset 0 2px 0 rgba(255, 255, 255, .75), inset 0 -3px 0 rgba(0, 0, 0, .08), 0 22px 44px rgba(40, 50, 40, .2), 0 0 0 1px #bcc0ae; + --pitShadow: inset 0 7px 14px rgba(40, 50, 40, .38), inset 0 -2px 0 rgba(255, 255, 255, .55), 0 1px 0 rgba(255, 255, 255, .75); + --stoneShadow: 0 3px 4px rgba(30, 40, 30, .35); + color-scheme: light; +} + +html, body, #main { min-height: 100%; } + +body { + margin: 0; + background: var(--bg); + color: var(--ink); + font-family: 'Bricolage Grotesque', system-ui, sans-serif; + transition: background .3s, color .3s; +} + +#main { display: flex; flex-direction: column; min-height: 100vh; } + +a { color: inherit; } + +/* Page */ + +.page { display: flex; flex-direction: column; flex: 1; min-width: 0; } + +.wrap { + width: 100%; + max-width: 1180px; + margin: 0 auto; + padding: 28px 32px 56px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 26px; +} + +/* Header */ + +.hdr { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + gap: 12px; + padding: 12px 32px; + background: var(--bg); + border-bottom: 1px solid var(--line); + transition: background .3s; +} + +.logo { + display: flex; + align-items: center; + gap: 10px; + margin-right: auto; + min-width: 0; + color: var(--ink); + text-decoration: none; + font-weight: 800; + font-size: 24px; + letter-spacing: -.02em; +} + +.logo svg { flex: none; } + +.logo span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +.themes { display: flex; gap: 2px; padding: 3px; border-radius: 26px; background: var(--surface); } + +.sw { + width: 44px; + height: 44px; + border-radius: 50%; + border: none; + padding: 0; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.sw span { width: 22px; height: 22px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .25); } + +.sw.on { background: var(--surface2); box-shadow: inset 0 0 0 2px var(--accent); } + +/* Buttons */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 44px; + padding: 0 18px; + border-radius: 22px; + border: none; + font: inherit; + font-weight: 700; + font-size: 15px; + cursor: pointer; + text-decoration: none; + white-space: nowrap; + box-sizing: border-box; +} + +.btn.pri { background: var(--accent); color: var(--onAccent); } + +.btn.ghost { background: transparent; color: var(--ink); box-shadow: inset 0 0 0 1.5px var(--line); } + +.btn.icon { width: 44px; padding: 0; } + +.btn:disabled { opacity: .4; cursor: default; } + +.sw:focus-visible, .btn:focus-visible, .card:focus-visible, .logo:focus-visible { + outline: 3px solid var(--accent); + outline-offset: 3px; +} + +/* Pills, cards */ + +.pill { + display: inline-flex; + align-items: center; + gap: 6px; + height: 26px; + padding: 0 10px; + border-radius: 13px; + font-size: 12px; + font-weight: 700; + background: var(--surface2); + color: var(--ink); + white-space: nowrap; +} + +.pill.live, .pill.lose { color: var(--live); } + +.pill.live::before { + content: ''; + width: 7px; + height: 7px; + border-radius: 50%; + background: currentColor; + animation: kpulse 1.6s ease-in-out infinite; +} + +.pill.win { background: var(--accent); color: var(--onAccent); } + +.panel { border-radius: 24px; background: var(--surface); box-shadow: inset 0 0 0 1px var(--line); } + +.dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: currentColor; animation: kpulse 1.6s ease-in-out infinite; } + +@keyframes kpulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: .35; transform: scale(.7); } +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +/* Account menu (@szhsin/react-menu) */ + +.szh-menu { + background: var(--surface); + color: var(--ink); + border-radius: 16px; + padding: 6px; + box-shadow: inset 0 0 0 1px var(--line), 0 16px 30px rgba(0, 0, 0, .25); + font-family: 'Bricolage Grotesque', system-ui, sans-serif; +} + +.szh-menu__header { color: var(--muted); font-weight: 700; text-transform: none; font-size: 13px; } + +.szh-menu__item { border-radius: 10px; min-height: 40px; font-weight: 600; color: var(--ink); } + +.szh-menu__item--hover { background: var(--surface2); } + +/* Dialogs (sweetalert) */ + +.swal-modal { background: var(--surface); border-radius: 24px; font-family: 'Bricolage Grotesque', system-ui, sans-serif; } + +.swal-title, .swal-text { color: var(--ink); } + +.swal-button { border-radius: 22px; font-family: inherit; } + +@media (max-width: 640px) { + .hdr { padding: 10px 16px; gap: 8px; } + .logo { font-size: 20px; gap: 8px; } + .logo svg { width: 28px; height: 28px; } + .themes .sw { width: 40px; height: 40px; } + .hide-m { display: none !important; } + .ic-m { width: 44px; padding: 0; } + .wrap { padding: 16px 16px 32px; gap: 16px; } +} + +/* The account button makes the header too wide for the name on phones. */ +@media (max-width: 420px) { + .hdr.has-account .logo span { display: none; } +} + +@media (max-width: 360px) { + .logo span { display: none; } +} diff --git a/apps/frontend/src/theme/themes.ts b/apps/frontend/src/theme/themes.ts new file mode 100644 index 0000000..09baab5 --- /dev/null +++ b/apps/frontend/src/theme/themes.ts @@ -0,0 +1,94 @@ +import { Theme } from "@mancala/core"; + +// The web app's themes. Pages are coloured by the CSS variables of the theme's class (styles/app.css); the colours +// here are for code that still reads core's Theme, such as PitAnimator. +export type WebTheme = Theme & { + // CSS class on . + className: string; + // Translation key of the theme's name. + labelKey: string; + // Colours of the theme's swatch. + swatch: { board: string, accent: string }; +}; + +const walnut: WebTheme = { + id: "ceviz", + name: "Ceviz", + className: "t-ceviz", + labelKey: "ThemeWalnut", + swatch: { board: "#6e4a30", accent: "#f2b84b" }, + themePreviewColor: "#6e4a30", + background: "#1f1712", + appBarBgColor: "#2a2019", + textColor: "#2a1a0a", + textLightColor: "#f6ead8", + playerTurnColor: "#f2b84b", + boardColor: "#6e4a30", + pitColor: "#2c1d13", + pitSelectedColor: "#f2b84b", + stoneColor: "#e39a32", + stoneLightColor: "#fff1c9", + pitGameMoveAnimateColor: "#f2b84b", + pitEmptyPitAnimateColor: "#ff8a73", + pitLastStoneInBankPitAnimateColor: "#f2b84b", + pitGetRivalStonePitAnimateColor: "#ff8a73", +}; + +const night: WebTheme = { + id: "gece", + name: "Gece", + className: "t-gece", + labelKey: "ThemeNight", + swatch: { board: "#1d2a4d", accent: "#e2b36a" }, + themePreviewColor: "#1d2a4d", + background: "#0c1220", + appBarBgColor: "#141c30", + textColor: "#241707", + textLightColor: "#eef1f8", + playerTurnColor: "#e2b36a", + boardColor: "#1d2a4d", + pitColor: "#0b1020", + pitSelectedColor: "#e2b36a", + stoneColor: "#dfe3ee", + stoneLightColor: "#ffffff", + pitGameMoveAnimateColor: "#e2b36a", + pitEmptyPitAnimateColor: "#ff8a7a", + pitLastStoneInBankPitAnimateColor: "#e2b36a", + pitGetRivalStonePitAnimateColor: "#ff8a7a", +}; + +const jade: WebTheme = { + id: "yesim", + name: "Yeşim", + className: "t-yesim", + labelKey: "ThemeJade", + swatch: { board: "#cfd2c3", accent: "#2a7650" }, + themePreviewColor: "#cfd2c3", + background: "#e9ebe3", + appBarBgColor: "#f7f8f3", + textColor: "#1d2721", + textLightColor: "#ffffff", + playerTurnColor: "#2a7650", + boardColor: "#cfd2c3", + pitColor: "#c3c7b6", + pitSelectedColor: "#2a7650", + stoneColor: "#3f9f6f", + stoneLightColor: "#e2ffee", + pitGameMoveAnimateColor: "#2a7650", + pitEmptyPitAnimateColor: "#b43a26", + pitLastStoneInBankPitAnimateColor: "#2a7650", + pitGetRivalStonePitAnimateColor: "#b43a26", +}; + +// The first one is the default. +export const webThemes: WebTheme[] = [walnut, night, jade]; + +export function getWebTheme(theme: Theme): WebTheme { + return webThemes.find((webTheme) => webTheme.id === theme.id) ?? walnut; +} + +export function applyThemeClass(theme: Theme) { + const root = document.documentElement; + webThemes.forEach((webTheme) => root.classList.remove(webTheme.className)); + root.classList.add(getWebTheme(theme).className); +} diff --git a/docs/specs/web-redesign/tasks.md b/docs/specs/web-redesign/tasks.md index 4d0e2fc..e79371d 100644 --- a/docs/specs/web-redesign/tasks.md +++ b/docs/specs/web-redesign/tasks.md @@ -5,7 +5,7 @@ Work through the tasks in order; each ends with its checks passing and a commit. - [x] 1. **Core**: `ThemeManager` takes its themes; `PitViewModel.highlight` set by `PitAnimator`; `getSowingPath` with unit tests; new texts (en, tr). - [x] 2. **Backend**: `GET /games/popular?kind=finished`; test. -- [ ] 3. **Web app shell**: the three themes, font, base styles, `AppHeader` (logo, theme swatches, account menu, +- [x] 3. **Web app shell**: the three themes, font, base styles, `AppHeader` (logo, theme swatches, account menu, action) on every page; restyle the lobby, privacy page, loading and connection panels. - [ ] 4. **Web app board**: `BoardView`, `PitView`, `StoreView`, stones, upright layout, highlights and move preview. - [ ] 5. **Web app game page**: players, turn, clocks, game-over card, move list and review banner.