feat(frontend): add the new themes and header
Ceviz, Gece and Yeşim themes as CSS variables on <html>, 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.
This commit is contained in:
parent
e0c66a1265
commit
6279b5a820
@ -11,7 +11,6 @@ import Home from './routes/Home';
|
|||||||
|
|
||||||
import { initContext } from './context/context';
|
import { initContext } from './context/context';
|
||||||
import { RTMTWS, ConnectionState, AuthUser } from '@mancala/core';
|
import { RTMTWS, ConnectionState, AuthUser } from '@mancala/core';
|
||||||
import { getColorByBrightness } 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';
|
||||||
@ -19,6 +18,8 @@ import swal from 'sweetalert';
|
|||||||
import Util from './util/Util';
|
import Util from './util/Util';
|
||||||
import { server } from './const/config';
|
import { server } from './const/config';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { applyThemeClass } from './theme/themes';
|
||||||
|
import './styles/app.css';
|
||||||
|
|
||||||
const context = initContext();
|
const context = initContext();
|
||||||
|
|
||||||
@ -42,7 +43,10 @@ const MancalaApp: FunctionComponent = () => {
|
|||||||
|
|
||||||
const onConnectionChange = (_connectionState: ConnectionState) => setConnetionState(_connectionState);
|
const onConnectionChange = (_connectionState: ConnectionState) => setConnetionState(_connectionState);
|
||||||
|
|
||||||
const onThemeChange = (theme: Theme) => setTheme(theme);
|
const onThemeChange = (theme: Theme) => {
|
||||||
|
applyThemeClass(theme);
|
||||||
|
setTheme(theme);
|
||||||
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@ -99,6 +103,7 @@ const MancalaApp: FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
applyThemeClass(context.themeManager.theme);
|
||||||
startSession().catch((error) => {
|
startSession().catch((error) => {
|
||||||
swal(t("Error") + "!", t("ErrorWhenRetrievingInformation"), "error");
|
swal(t("Error") + "!", t("ErrorWhenRetrievingInformation"), "error");
|
||||||
console.error(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasSession && <BrowserRouter>
|
{hasSession && <BrowserRouter>
|
||||||
@ -133,8 +133,8 @@ const MancalaApp: FunctionComponent = () => {
|
|||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>}
|
</BrowserRouter>}
|
||||||
<FloatingPanel context={context} color={context.themeManager.theme.boardColor} visible={connectionState != "connected"}>
|
<FloatingPanel visible={connectionState != "connected"}>
|
||||||
<span style={{ color: textColorOnBoard, transition: 'color 0.5s' }}>{Util.getTextByConnectionState(context, connectionState, t)}</span>
|
{Util.getTextByConnectionState(context, connectionState, t)}
|
||||||
</FloatingPanel>
|
</FloatingPanel>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,30 +1,36 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
import { Context } from "../context/context";
|
|
||||||
|
|
||||||
const FloatingPanel: FunctionComponent<{
|
// A status pill in the bottom left corner, e.g. while connecting.
|
||||||
context: Context;
|
const FloatingPanel: FunctionComponent<{ visible: boolean }> = (props) => {
|
||||||
color: string;
|
if (props.visible === false) return <></>
|
||||||
visible: boolean;
|
|
||||||
}> = (props) => {
|
|
||||||
if(props.visible === false) return <></>
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="floating-panel" role="status">
|
||||||
backgroundColor: props.color,
|
<span className="dot" />
|
||||||
}}>
|
{props.children}
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
div {
|
.floating-panel {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
bottom: 0px;
|
left: 16px;
|
||||||
left: 0px;
|
bottom: 16px;
|
||||||
padding: 15px;
|
|
||||||
border-top-right-radius: 1vw;
|
|
||||||
min-width: 10vw;
|
|
||||||
min-height: 1vw;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--ink);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: inset 0 0 0 1px var(--line), 0 12px 30px rgba(0, 0, 0, .3);
|
||||||
|
}
|
||||||
|
.floating-panel .dot {
|
||||||
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
{props.children}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
69
apps/frontend/src/components/Icons.tsx
Normal file
69
apps/frontend/src/components/Icons.tsx
Normal file
@ -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) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 32 32" aria-hidden="true">
|
||||||
|
<rect x="1" y="1" width="30" height="30" rx="9" style={{ fill: "var(--accent)" }} />
|
||||||
|
<rect x="6" y="7" width="6" height="18" rx="3" style={{ fill: "var(--onAccent)" }} />
|
||||||
|
<circle cx="18" cy="11" r="3.2" style={{ fill: "var(--onAccent)" }} />
|
||||||
|
<circle cx="18" cy="21" r="3.2" style={{ fill: "var(--onAccent)" }} />
|
||||||
|
<circle cx="25" cy="11" r="3.2" style={{ fill: "var(--onAccent)", opacity: .55 }} />
|
||||||
|
<circle cx="25" cy="21" r="3.2" style={{ fill: "var(--onAccent)" }} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PlusIcon = ({ size = 20 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2.4)}><path d="M12 5v14M5 12h14" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ProfileIcon = ({ size = 20 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2)}><circle cx="12" cy="8" r="4" /><path d="M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LeaveIcon = ({ size = 20 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2)}><path d="M14 4h5v16h-5" /><path d="M10 8l-4 4 4 4" /><path d="M6 12h10" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ChevronIcon = ({ size = 20, className }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2.2, className)}><path d="M6 9l6 6 6-6" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CloseIcon = ({ size = 20 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2.2)}><path d="M6 6l12 12M18 6L6 18" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ReplayIcon = ({ size = 18 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2.2)}><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v5h5" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FirstIcon = ({ size = 20 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2.2)}><path d="M6 5v14" /><path d="M18 6l-8 6 8 6z" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PreviousIcon = ({ size = 20 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2.4)}><path d="M15 6l-6 6 6 6" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NextIcon = ({ size = 20 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2.4)}><path d="M9 6l6 6-6 6" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LastIcon = ({ size = 20 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 2.2)}><path d="M18 5v14" /><path d="M6 6l8 6-8 6z" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PlayIcon = ({ size = 22 }: IconProps) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5l11 7-11 7z" fill="currentColor" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PauseIcon = ({ size = 22 }: IconProps) => (
|
||||||
|
<svg {...stroke(size, 3)}><path d="M8 5v14M16 5v14" /></svg>
|
||||||
|
);
|
||||||
@ -1,32 +1,33 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
import { Context } from '../context/context';
|
|
||||||
import { LoadingState } from "@mancala/core";
|
import { LoadingState } from "@mancala/core";
|
||||||
import { getColorByBrightness } from "@mancala/core";
|
|
||||||
import CircularPanel from './CircularPanel';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const LoadingComponent: FunctionComponent<{ context: Context, loadingState: LoadingState<any> }> = ({ context, loadingState }) => {
|
const LoadingComponent: FunctionComponent<{ loadingState: LoadingState<any> }> = ({ loadingState }) => {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (loadingState.isUnset() || loadingState.isLoaded()) {
|
if (!loadingState.isLoading() && !loadingState.isError()) return <></>;
|
||||||
return <></>
|
const text = loadingState.isLoading() ? t("Loading") + "..." : loadingState.errorMessage;
|
||||||
}
|
|
||||||
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 (
|
return (
|
||||||
<CircularPanel color={context.themeManager.theme.boardColor}>
|
<div className="loading panel" role="status">
|
||||||
<h4 style={{ margin: "0", color: textColorOnBoard }}>{`${text}`}</h4>
|
{loadingState.isLoading() && <span className="dot" />}
|
||||||
</CircularPanel>
|
{text}
|
||||||
);
|
<style jsx>{`
|
||||||
|
.loading {
|
||||||
|
align-self: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 22px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
return <></>
|
.dot {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoadingComponent;
|
export default LoadingComponent;
|
||||||
@ -1,25 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
import { Theme } from "@mancala/core";
|
|
||||||
|
|
||||||
const PageContainer: FunctionComponent<{ theme: Theme }> = (props) => {
|
// A page: the header, then the page's content.
|
||||||
return (
|
const PageContainer: FunctionComponent = ({ children }) => <div className="page">{children}</div>;
|
||||||
<div style={{
|
|
||||||
background: props.theme?.background,
|
|
||||||
}}>
|
|
||||||
<style jsx>{`
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 400px;
|
|
||||||
transition: background-color 0.5s;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PageContainer;
|
export default PageContainer;
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { FunctionComponent } from 'react';
|
|
||||||
|
|
||||||
const Row: FunctionComponent = ({children}) => {
|
|
||||||
return (
|
|
||||||
<div className="row">
|
|
||||||
{children}
|
|
||||||
<style jsx>{`
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Row;
|
|
||||||
@ -4,46 +4,30 @@ import { FunctionComponent } from "react";
|
|||||||
import { AuthUser, getPlayerDisplayName } from "@mancala/core";
|
import { AuthUser, getPlayerDisplayName } from "@mancala/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Context } from "../../context/context";
|
import { Context } from "../../context/context";
|
||||||
|
import { ProfileIcon } from "../Icons";
|
||||||
import "@szhsin/react-menu/dist/index.css";
|
import "@szhsin/react-menu/dist/index.css";
|
||||||
import "@szhsin/react-menu/dist/transitions/slide.css"
|
import "@szhsin/react-menu/dist/transitions/slide.css"
|
||||||
|
|
||||||
// Guests can log in or sign up; logged in users can log out.
|
// Guests can log in or sign up; logged in users can log out.
|
||||||
const AccountMenu: FunctionComponent<{ context: Context, textColor: string, authUser?: AuthUser }> = (props) => {
|
const AccountMenu: FunctionComponent<{ context: Context, authUser: AuthUser }> = ({ context, authUser }) => {
|
||||||
const { context, textColor, authUser } = props;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (!authUser) return <></>;
|
|
||||||
|
|
||||||
const authService = context.authService;
|
const authService = context.authService;
|
||||||
const menuButton = <span
|
const menuButton = (
|
||||||
style={{ color: textColor, cursor: 'pointer', userSelect: 'none' }}
|
<button className="btn icon ghost" aria-label={t("Account")}>
|
||||||
className="material-symbols-outlined">
|
<ProfileIcon />
|
||||||
account_circle
|
</button>
|
||||||
</span>;
|
);
|
||||||
const itemStyle = { color: textColor };
|
|
||||||
const menuItems = authUser.isGuest ? [
|
const menuItems = authUser.isGuest ? [
|
||||||
<MenuItem key="login" style={itemStyle} onClick={() => authService.login()}>{t("LogIn")}</MenuItem>,
|
<MenuItem key="login" onClick={() => authService.login()}>{t("LogIn")}</MenuItem>,
|
||||||
<MenuItem key="signup" style={itemStyle} onClick={() => authService.signUp()}>{t("SignUp")}</MenuItem>,
|
<MenuItem key="signup" onClick={() => authService.signUp()}>{t("SignUp")}</MenuItem>,
|
||||||
] : [
|
] : [
|
||||||
<MenuItem key="logout" style={itemStyle} onClick={() => authService.logout()}>{t("LogOut")}</MenuItem>,
|
<MenuItem key="logout" onClick={() => authService.logout()}>{t("LogOut")}</MenuItem>,
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="menu-container">
|
<Menu menuButton={menuButton} transition align="end" offsetY={8}>
|
||||||
<Menu
|
<MenuHeader>{getPlayerDisplayName(authUser, t)}</MenuHeader>
|
||||||
menuStyle={{ background: context.themeManager.theme.appBarBgColor }}
|
|
||||||
menuButton={menuButton}
|
|
||||||
transition
|
|
||||||
align="end">
|
|
||||||
<MenuHeader style={itemStyle}>{getPlayerDisplayName(authUser, t)}</MenuHeader>
|
|
||||||
{menuItems}
|
{menuItems}
|
||||||
</Menu>
|
</Menu>
|
||||||
<style jsx>{`
|
|
||||||
.menu-container {
|
|
||||||
margin: 0 1vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
apps/frontend/src/components/headerbar/AppHeader.tsx
Normal file
28
apps/frontend/src/components/headerbar/AppHeader.tsx
Normal file
@ -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 (
|
||||||
|
<header className={"hdr" + (authUser ? " has-account" : "")}>
|
||||||
|
<Link className="logo" to="/" aria-label={`${t("Mancala")} · ${t("Home")}`}>
|
||||||
|
<LogoIcon />
|
||||||
|
<span>{t("Mancala")}</span>
|
||||||
|
</Link>
|
||||||
|
<ThemeSwatches context={context} />
|
||||||
|
{authUser && <AccountMenu context={context} authUser={authUser} />}
|
||||||
|
{children}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppHeader;
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { FunctionComponent } from "react";;
|
|
||||||
|
|
||||||
const HeaderBar: FunctionComponent<{ color?: string }> = ({children, color }) => {
|
|
||||||
return (
|
|
||||||
<div style={{ background: color }} className="header-bar">
|
|
||||||
{children}
|
|
||||||
<style jsx>{`
|
|
||||||
.header-bar {
|
|
||||||
padding: 0px 4vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-self: stretch;
|
|
||||||
transition: background-color 0.5s;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
export default HeaderBar;
|
|
||||||
@ -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 (
|
|
||||||
<div className="header-bar-icon-wrapper">
|
|
||||||
<MancalaIcon style={{ height: "30px", width: "30px" }} />
|
|
||||||
<style jsx>{`
|
|
||||||
.header-bar-icon-wrapper {
|
|
||||||
margin-right: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HeaderbarIcon;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { FunctionComponent } from 'react';
|
|
||||||
|
|
||||||
const HeaderbarTitle: FunctionComponent<{ title: string, color: string }> = ({ title, color }) => {
|
|
||||||
return (
|
|
||||||
<h1 style={{ color: color, transition: 'color 0.5s' }} className="header-bar-title">
|
|
||||||
{title}
|
|
||||||
<style jsx>{`
|
|
||||||
.header-bar-title {
|
|
||||||
margin: 10px 0px;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HeaderbarTitle;
|
|
||||||
26
apps/frontend/src/components/headerbar/ThemeSwatches.tsx
Normal file
26
apps/frontend/src/components/headerbar/ThemeSwatches.tsx
Normal file
@ -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 (
|
||||||
|
<div className="themes" role="group" aria-label={t("ChooseTheme")}>
|
||||||
|
{webThemes.map((theme) => (
|
||||||
|
<button
|
||||||
|
key={theme.id}
|
||||||
|
className={"sw" + (theme === current ? " on" : "")}
|
||||||
|
aria-label={t(theme.labelKey)}
|
||||||
|
aria-pressed={theme === current}
|
||||||
|
onClick={() => (context.themeManager.theme = theme)}>
|
||||||
|
<span style={{ background: `linear-gradient(135deg, ${theme.swatch.board} 0 50%, ${theme.swatch.accent} 50% 100%)` }} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeSwatches;
|
||||||
@ -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 = <span
|
|
||||||
style={{ color: textColor, cursor: 'pointer', userSelect: 'none' }}
|
|
||||||
className="material-symbols-outlined">
|
|
||||||
light_mode
|
|
||||||
</span>;
|
|
||||||
const menuItems = context.themeManager.themes.map((theme, index) => {
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
key={index}
|
|
||||||
style={{ color: textColor }}
|
|
||||||
//@ts-ignore
|
|
||||||
onMouseOver={(event) => {
|
|
||||||
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)}>
|
|
||||||
<div style={{ background: theme.themePreviewColor }} className="theme-color-circle" />
|
|
||||||
{theme.name}
|
|
||||||
<style jsx>{`
|
|
||||||
.theme-color-circle {
|
|
||||||
border-radius: 5vw;
|
|
||||||
width: 1vw;
|
|
||||||
height: 1vw;
|
|
||||||
margin-right: 1vw;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div className="menu-container">
|
|
||||||
<Menu
|
|
||||||
menuStyle={{ background: context.themeManager.theme.appBarBgColor }}
|
|
||||||
menuButton={menuButton}
|
|
||||||
transition
|
|
||||||
align="end">
|
|
||||||
{menuItems}
|
|
||||||
</Menu>
|
|
||||||
<style jsx>{`
|
|
||||||
.menu-container {
|
|
||||||
margin: 0 1vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ThemeSwitchMenu;
|
|
||||||
@ -24,13 +24,10 @@ import { formatDate, formatDuration, getResultText, getScoreText, getViewerSeat
|
|||||||
import { getMoveText } from "../../util/MoveText";
|
import { getMoveText } from "../../util/MoveText";
|
||||||
import BoardView from "../board/BoardView";
|
import BoardView from "../board/BoardView";
|
||||||
import Button from "../Button";
|
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 PageContainer from "../PageContainer";
|
||||||
|
import AppHeader from "../headerbar/AppHeader";
|
||||||
|
import { PlusIcon } from "../Icons";
|
||||||
import GameClockView from "../GameClockView";
|
import GameClockView from "../GameClockView";
|
||||||
import Row from "../Row";
|
|
||||||
import MoveList from "./MoveList";
|
import MoveList from "./MoveList";
|
||||||
|
|
||||||
const SPEEDS = [1, 2, 4];
|
const SPEEDS = [1, 2, 4];
|
||||||
@ -166,19 +163,12 @@ const ReplayView: FunctionComponent<{ context: Context, theme: Theme, record: Ga
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer theme={theme}>
|
<PageContainer>
|
||||||
<HeaderBar color={theme.appBarBgColor}>
|
<AppHeader context={context}>
|
||||||
<Row>
|
<button className="btn pri ic-m" aria-label={t("NewGame")} onClick={() => navigate("/loby")}>
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}><HeaderbarIcon /></Link>
|
<PlusIcon /><span className="hide-m">{t("NewGame")}</span>
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
</button>
|
||||||
<HeaderbarTitle title={t("Mancala")} color={textColorOnAppBar} />
|
</AppHeader>
|
||||||
</Link>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
|
||||||
<Button context={context} color={theme.pitColor} text={t("NewGame")} onClick={() => navigate("/loby")} />
|
|
||||||
</Row>
|
|
||||||
</HeaderBar>
|
|
||||||
|
|
||||||
<div className="result" style={{ background: theme.boardColor, color: panelTextColor }}>
|
<div className="result" style={{ background: theme.boardColor, color: panelTextColor }}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { GameStore, GameStoreImpl } from "../store/GameStore";
|
|||||||
import { AuthService } from "../auth/AuthService";
|
import { AuthService } from "../auth/AuthService";
|
||||||
import { LocalStorage } from "../storage";
|
import { LocalStorage } from "../storage";
|
||||||
import { Storage } from '@mancala/core'
|
import { Storage } from '@mancala/core'
|
||||||
|
import { webThemes } from "../theme/themes";
|
||||||
|
|
||||||
export type Context = {
|
export type Context = {
|
||||||
rtmt: RTMT;
|
rtmt: RTMT;
|
||||||
@ -20,7 +21,7 @@ export const initContext = (): Context => {
|
|||||||
const authService = new AuthService({ keycloakUrl: keycloak.url, realm: keycloak.realm, clientId: keycloak.clientId });
|
const authService = new AuthService({ keycloakUrl: keycloak.url, realm: keycloak.realm, clientId: keycloak.clientId });
|
||||||
const gameStore = new GameStoreImpl({ httpService });
|
const gameStore = new GameStoreImpl({ httpService });
|
||||||
const storage = new LocalStorage();
|
const storage = new LocalStorage();
|
||||||
const themeManager = new ThemeManager(storage);
|
const themeManager = new ThemeManager(storage, webThemes);
|
||||||
return {
|
return {
|
||||||
rtmt: rtmt,
|
rtmt: rtmt,
|
||||||
authService,
|
authService,
|
||||||
|
|||||||
@ -1,31 +1,18 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html class="t-ceviz">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<title>Mancala</title>
|
<title>Mancala</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="mancala.svg">
|
<link rel="icon" type="image/x-icon" href="mancala.svg">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&display=swap" />
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0px;">
|
<body>
|
||||||
<div id="main" style="display: flex;"></div>
|
<div id="main"></div>
|
||||||
<script type="module" src="./App.tsx"></script>
|
<script type="module" src="./App.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
html{
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body{
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main{
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</html>
|
</html>
|
||||||
@ -2,20 +2,15 @@ import { MancalaGame, Pit } from 'mancala.js';
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FunctionComponent, useState } from 'react';
|
import { FunctionComponent, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { PitAnimator } from '@mancala/core';
|
import { PitAnimator } from '@mancala/core';
|
||||||
import BoardToolbar from '../components/board/BoardToolbar';
|
import BoardToolbar from '../components/board/BoardToolbar';
|
||||||
import BoardView from '../components/board/BoardView';
|
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 InfoPanel from '../components/InfoPanel';
|
||||||
import LoadingComponent from '../components/LoadingComponent';
|
import LoadingComponent from '../components/LoadingComponent';
|
||||||
import PageContainer from '../components/PageContainer';
|
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 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 { 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';
|
import { Context } from '../context/context';
|
||||||
@ -341,7 +336,7 @@ const GamePage: FunctionComponent<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer theme={theme!}>
|
<PageContainer>
|
||||||
{renderHeaderBar()}
|
{renderHeaderBar()}
|
||||||
{renderMobileBoardToolbar()}
|
{renderMobileBoardToolbar()}
|
||||||
{buildBoardTopToolbar()}
|
{buildBoardTopToolbar()}
|
||||||
@ -371,30 +366,21 @@ const GamePage: FunctionComponent<{
|
|||||||
collapsedByDefault={isMobile} />
|
collapsedByDefault={isMobile} />
|
||||||
)}
|
)}
|
||||||
<Center>
|
<Center>
|
||||||
<LoadingComponent context={context} loadingState={gameLoadingState}></LoadingComponent>
|
<LoadingComponent loadingState={gameLoadingState}></LoadingComponent>
|
||||||
</Center>
|
</Center>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
function renderHeaderBar() {
|
function renderHeaderBar() {
|
||||||
return <HeaderBar color={theme?.appBarBgColor}>
|
return <AppHeader context={context}>
|
||||||
<Row>
|
{renderNewGameBtn
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
? <button className="btn pri ic-m" aria-label={t("NewGame")} onClick={onNewGameClick}>
|
||||||
<HeaderbarIcon />
|
<PlusIcon /><span className="hide-m">{t("NewGame")}</span>
|
||||||
</Link>
|
</button>
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
: <button className="btn ghost ic-m" aria-label={t("Leave")} onClick={onLeaveGameClick}>
|
||||||
<HeaderbarTitle title={t("Mancala")} color={textColorOnAppBar} />
|
<LeaveIcon /><span className="hide-m">{t("Leave")}</span>
|
||||||
</Link>
|
</button>}
|
||||||
</Row>
|
</AppHeader>;
|
||||||
<Row>
|
|
||||||
<ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
|
||||||
<Button
|
|
||||||
context={context}
|
|
||||||
color={context.themeManager.theme.pitColor}
|
|
||||||
text={renderNewGameBtn ? t("NewGame") : t("Leave")}
|
|
||||||
onClick={renderNewGameBtn ? onNewGameClick : onLeaveGameClick} />
|
|
||||||
</Row>
|
|
||||||
</HeaderBar>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMobileBoardToolbar() {
|
function renderMobileBoardToolbar() {
|
||||||
|
|||||||
@ -1,20 +1,15 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { FunctionComponent, useEffect, useState } 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 { AuthUser, Theme } from "@mancala/core";
|
||||||
import HeaderBar from "../components/headerbar/HeaderBar";
|
import AppHeader from "../components/headerbar/AppHeader";
|
||||||
import PageContainer from "../components/PageContainer";
|
import PageContainer from "../components/PageContainer";
|
||||||
import Row from "../components/Row";
|
import { PlusIcon } from "../components/Icons";
|
||||||
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 ActiveGameBanner from "../components/home/ActiveGameBanner";
|
import ActiveGameBanner from "../components/home/ActiveGameBanner";
|
||||||
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 Button from "../components/Button";
|
|
||||||
import { Context } from "../context/context";
|
import { Context } from "../context/context";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Util from "../util/Util";
|
import Util from "../util/Util";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -46,32 +41,13 @@ const Home: FunctionComponent<{
|
|||||||
navigate("/loby")
|
navigate("/loby")
|
||||||
};
|
};
|
||||||
|
|
||||||
const textColorOnAppBar = getColorByBrightness(
|
|
||||||
context.themeManager.theme.appBarBgColor,
|
|
||||||
context.themeManager.theme.textColor,
|
|
||||||
context.themeManager.theme.textLightColor
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<PageContainer theme={theme!}>
|
<PageContainer>
|
||||||
<HeaderBar color={theme?.appBarBgColor}>
|
<AppHeader context={context} authUser={authUser}>
|
||||||
<Row>
|
<button className="btn pri ic-m" aria-label={t("NewGame")} onClick={onNewGameClick}>
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
<PlusIcon /><span className="hide-m">{t("NewGame")}</span>
|
||||||
<HeaderbarIcon />
|
</button>
|
||||||
</Link>
|
</AppHeader>
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
|
||||||
<HeaderbarTitle title={t("Mancala")} color={textColorOnAppBar} />
|
|
||||||
</Link>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
|
||||||
<AccountMenu context={context} textColor={textColorOnAppBar} authUser={authUser} />
|
|
||||||
<Button
|
|
||||||
context={context}
|
|
||||||
color={context.themeManager.theme.pitColor}
|
|
||||||
text={t("NewGame")}
|
|
||||||
onClick={onNewGameClick} />
|
|
||||||
</Row>
|
|
||||||
</HeaderBar>
|
|
||||||
{activeGame && authUser && (
|
{activeGame && authUser && (
|
||||||
<ActiveGameBanner
|
<ActiveGameBanner
|
||||||
context={context}
|
context={context}
|
||||||
|
|||||||
@ -1,27 +1,19 @@
|
|||||||
import { CommonMancalaGame, MancalaGame } from 'mancala.js';
|
import { CommonMancalaGame } from 'mancala.js';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FunctionComponent, useEffect, useState } from 'react';
|
import { FunctionComponent, useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import Center from '../components/Center';
|
import AppHeader from '../components/headerbar/AppHeader';
|
||||||
import CircularPanel from '../components/CircularPanel';
|
|
||||||
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 PageContainer from '../components/PageContainer';
|
import PageContainer from '../components/PageContainer';
|
||||||
import Row from '../components/Row';
|
|
||||||
import { channel_cancel_new_game, channel_on_game_start, channel_on_new_game_rejected } from '@mancala/core';
|
import { channel_cancel_new_game, channel_on_game_start, channel_on_new_game_rejected } from '@mancala/core';
|
||||||
import { Context } from '../context/context';
|
import { Context } from '../context/context';
|
||||||
import { Theme } from '@mancala/core';
|
import { Theme } from '@mancala/core';
|
||||||
import { getColorByBrightness } from "@mancala/core";
|
|
||||||
import Button from "../components/Button";
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const LobyPage: FunctionComponent<{
|
const LobyPage: FunctionComponent<{
|
||||||
context: Context,
|
context: Context,
|
||||||
userKey?: string,
|
userKey?: string,
|
||||||
theme: Theme
|
theme: Theme
|
||||||
}> = ({ context, userKey, theme }) => {
|
}> = ({ context }) => {
|
||||||
|
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
@ -53,55 +45,57 @@ const LobyPage: FunctionComponent<{
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<PageContainer theme={theme!}>
|
<PageContainer>
|
||||||
<HeaderBar color={theme?.appBarBgColor}>
|
<AppHeader context={context} />
|
||||||
<Row>
|
<main className="wrap">
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
<section className="search panel" aria-live="polite">
|
||||||
<HeaderbarIcon />
|
{!activeGameId && <span className="dot" />}
|
||||||
</Link>
|
<h1>{activeGameId ? t("GameInProgress") : t("SearchingOpponent")}</h1>
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
{!activeGameId && <p>{`${t("PleaseWait")}...`}</p>}
|
||||||
<HeaderbarTitle title={t("Mancala")} color={textColorOnAppBar} />
|
<div className="actions">
|
||||||
</Link>
|
{activeGameId && (
|
||||||
</Row>
|
<button className="btn pri" onClick={() => navigate(`/game/${activeGameId}`)}>{t("Continue")}</button>
|
||||||
<Row>
|
)}
|
||||||
<ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
<button className="btn ghost" onClick={onCancelNewGameClick}>{t("Cancel")}</button>
|
||||||
</Row>
|
|
||||||
</HeaderBar>
|
|
||||||
<Center>
|
|
||||||
<div style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignContent: "center"
|
|
||||||
}}>
|
|
||||||
<CircularPanel color={context.themeManager.theme.boardColor}>
|
|
||||||
<h4 style={{ margin: "0", color: textColorOnBoard }}>
|
|
||||||
{activeGameId ? t("GameInProgress") : `${t("SearchingOpponent")} ${t("PleaseWait")}...`}
|
|
||||||
</h4>
|
|
||||||
</CircularPanel>
|
|
||||||
{activeGameId && <Button
|
|
||||||
context={context}
|
|
||||||
color={context.themeManager.theme.pitColor}
|
|
||||||
text={t("Continue")}
|
|
||||||
onClick={() => navigate(`/game/${activeGameId}`)} />}
|
|
||||||
<Button
|
|
||||||
context={context}
|
|
||||||
color={context.themeManager.theme.boardColor}
|
|
||||||
text={t("Cancel")}
|
|
||||||
onClick={onCancelNewGameClick} />
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</Center>
|
</main>
|
||||||
|
<style jsx>{`
|
||||||
|
.search {
|
||||||
|
align-self: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 460px;
|
||||||
|
margin-top: 8vh;
|
||||||
|
padding: 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,56 +1,19 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FunctionComponent, useEffect } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import AppHeader from '../components/headerbar/AppHeader';
|
||||||
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 PageContainer from '../components/PageContainer';
|
import PageContainer from '../components/PageContainer';
|
||||||
import Row from '../components/Row';
|
|
||||||
import { Context } from '../context/context';
|
import { Context } from '../context/context';
|
||||||
import { Theme } from '@mancala/core';
|
import { Theme } from '@mancala/core';
|
||||||
import { getColorByBrightness } from "@mancala/core";
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const PrivacyPage: FunctionComponent<{
|
const PrivacyPage: FunctionComponent<{
|
||||||
context: Context,
|
context: Context,
|
||||||
userKey?: string,
|
userKey?: string,
|
||||||
theme: Theme
|
theme: Theme
|
||||||
}> = ({ context, userKey, theme }) => {
|
}> = ({ context }) => {
|
||||||
|
|
||||||
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"
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<PageContainer theme={theme!}>
|
<PageContainer>
|
||||||
<HeaderBar color={theme?.appBarBgColor}>
|
<AppHeader context={context} />
|
||||||
<Row>
|
<main className="wrap privacy">
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
|
||||||
<HeaderbarIcon />
|
|
||||||
</Link>
|
|
||||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
|
||||||
<HeaderbarTitle title={t("Mancala")} color={textColorOnAppBar} />
|
|
||||||
</Link>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
|
||||||
</Row>
|
|
||||||
</HeaderBar>
|
|
||||||
<div style={{
|
|
||||||
margin: '30px',
|
|
||||||
overflowY: 'scroll',
|
|
||||||
color: textColorOnBoard
|
|
||||||
}}>
|
|
||||||
|
|
||||||
<h2>Privacy Policy</h2>
|
<h2>Privacy Policy</h2>
|
||||||
<p> 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
|
<p> 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.</p>
|
for use as is.</p>
|
||||||
@ -82,7 +45,20 @@ const PrivacyPage: FunctionComponent<{
|
|||||||
<p><strong>Contact Us</strong></p>
|
<p><strong>Contact Us</strong></p>
|
||||||
<p>If you have any questions or suggestions about our Privacy Policy, do not hesitate to <a href='/contact'>contact
|
<p>If you have any questions or suggestions about our Privacy Policy, do not hesitate to <a href='/contact'>contact
|
||||||
us</a>. </p>
|
us</a>. </p>
|
||||||
</div>
|
</main>
|
||||||
|
<style jsx>{`
|
||||||
|
.privacy {
|
||||||
|
max-width: 760px;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.privacy :global(p) {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.privacy :global(strong), .privacy :global(h2) {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
244
apps/frontend/src/styles/app.css
Normal file
244
apps/frontend/src/styles/app.css
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
/* Themes, base styles and the building blocks shared by the pages. The theme class is set on <html>. */
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
}
|
||||||
94
apps/frontend/src/theme/themes.ts
Normal file
94
apps/frontend/src/theme/themes.ts
Normal file
@ -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 <html>.
|
||||||
|
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);
|
||||||
|
}
|
||||||
@ -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`
|
- [x] 1. **Core**: `ThemeManager` takes its themes; `PitViewModel.highlight` set by `PitAnimator`; `getSowingPath`
|
||||||
with unit tests; new texts (en, tr).
|
with unit tests; new texts (en, tr).
|
||||||
- [x] 2. **Backend**: `GET /games/popular?kind=finished`; test.
|
- [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.
|
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.
|
- [ ] 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.
|
- [ ] 5. **Web app game page**: players, turn, clocks, game-over card, move list and review banner.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user