Implement highscore/leaderboard top 10 best games and add tests with jest
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
public/main.js
|
||||
*tar
|
||||
coverage
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
FROM nginx
|
||||
COPY public /usr/share/nginx/html
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY ./config/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
[ ] Create welcome / instructions page
|
||||
[ ] Link to repository
|
||||
[ ] Convert components to use react hooks
|
||||
[ ] Add license
|
||||
[ ] High score
|
||||
[ ] Improve banner
|
||||
|
||||
## License
|
||||
|
||||
|
||||
5
docker.md
Normal file
5
docker.md
Normal file
@@ -0,0 +1,5 @@
|
||||
npm run build
|
||||
docker build -t briemens/snake .
|
||||
docker tag briemens/snake briemens/snake
|
||||
docker push briemens/snake
|
||||
docker image save briemens/snake:latest -o snake-container.tar
|
||||
188
jest.config.js
Normal file
188
jest.config.js
Normal file
@@ -0,0 +1,188 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// Respect "browser" field in package.json when resolving modules
|
||||
// browser: false,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/j0/3bkm_b290232jgx7bhffq64w0000gp/T/jest_dy",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: null,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: "coverage",
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: null,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: null,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: null,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: null,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: null,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: null,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: null,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: null,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: null,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: null,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: null,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev": "webpack-dev-server --mode development --open",
|
||||
"build": "webpack --optimize-minimize --config webpack.production.config.js",
|
||||
"webpack": "webpack --config webpack.config.js --watch",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "jest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -20,6 +20,7 @@
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-styled-components": "^1.10.6",
|
||||
"jest": "^24.9.0",
|
||||
"react-hot-loader": "^4.12.11",
|
||||
"webpack": "^4.39.2",
|
||||
"webpack-cli": "^3.3.7",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
const StyledBanner = styled.div`
|
||||
border-width: ${({ theme }) => theme.banner.borderWidth};
|
||||
@@ -15,11 +15,22 @@ const StyledBanner = styled.div`
|
||||
transition: opacity 100ms ease-in;
|
||||
border-radius: 2px;
|
||||
box-shadow: 1px 1px 5px ${({ theme }) => theme.banner.shadowColor};
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
${({ fade = false }) =>
|
||||
fade &&
|
||||
css`
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Banner = ({ children }) => <StyledBanner>{children}</StyledBanner>;
|
||||
|
||||
Banner.Title = styled.h2`
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1rem;
|
||||
`;
|
||||
|
||||
export default Banner;
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import styled, { css } from "styled-components";
|
||||
import Tooltip from "./Tooltip.js";
|
||||
|
||||
const SharedStyle = () => `
|
||||
const SharedStyle = css`
|
||||
border-width: ${({ theme }) => theme.button.borderWidth};
|
||||
border-style: solid;
|
||||
border-color: ${({ theme }) => theme.button.borderColor};
|
||||
border-radius: ${({ theme }) => theme.button.borderRadius};
|
||||
background-color: ${({ theme }) => theme.button.background};
|
||||
color: ${({ theme }) => theme.button.color};
|
||||
|
||||
${({ inverse = false }) => {
|
||||
return !inverse
|
||||
? css`
|
||||
border-color: ${({ theme }) => theme.button.borderColor};
|
||||
background-color: ${({ theme }) => theme.button.background};
|
||||
color: ${({ theme }) => theme.button.color};
|
||||
`
|
||||
: css`
|
||||
border-color: ${({ theme }) => theme.button.color};
|
||||
background-color: ${({ theme }) => theme.button.color};
|
||||
color: ${({ theme }) => theme.button.background};
|
||||
`;
|
||||
}}
|
||||
|
||||
text-align: center;
|
||||
padding: .3125rem;
|
||||
padding: 0.3125rem;
|
||||
font-size: 1rem;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
@@ -19,6 +31,11 @@ const SharedStyle = () => `
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
outline: ${({ disabled }) => (!disabled ? "1px solid silver" : "none")};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.button.hover.background};
|
||||
}
|
||||
@@ -29,6 +46,17 @@ const SharedStyle = () => `
|
||||
color: ${({ theme }) => theme.button.active.color};
|
||||
background-color: ${({ theme }) => theme.button.active.background};
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&.disabled {
|
||||
// background-color: #aaa;
|
||||
// color: gray;
|
||||
// border-color: gray;
|
||||
background-color: ${({ theme }) => theme.button.disabled.background};
|
||||
color: ${({ theme }) => theme.button.disabled.color};
|
||||
border-color: ${({ theme }) => theme.button.disabled.borderColor};
|
||||
cursor: default;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled.div.attrs(({ disabled }) => ({ tabIndex: disabled ? null : 0 }))`
|
||||
@@ -85,6 +113,7 @@ export const Button = props => {
|
||||
onMouseLeave={() => setHover(false)}
|
||||
onKeyUp={updatedProps.onKeyUp}
|
||||
onClick={updatedProps.onClick}
|
||||
className={disabled && "disabled"}
|
||||
disabled={disabled}>
|
||||
<i
|
||||
disabled={disabled}
|
||||
@@ -111,16 +140,49 @@ export const IconButton = styled(Button)`
|
||||
cursor: pointer;
|
||||
|
||||
&:before {
|
||||
text-shadow: 0 0 1px black, 1px 1px 3px #d0bfa3;
|
||||
text-shadow: 0 0 1px gray, 0 0 1px #d0bfa3;
|
||||
// text-shadow: 0 0 1px black, 1px 1px 3px #d0bfa3;
|
||||
// text-shadow: 0 0 1px gray, 0 0 1px #d0bfa3;
|
||||
text-shadow: 0 0 1px ${({ theme }) => theme.button.icon.borderColor};
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&.disabled {
|
||||
background-color: ${({ theme }) => theme.button.icon.disabled.background};
|
||||
color: ${({ theme }) => theme.button.icon.disabled.color};
|
||||
border-color: ${({ theme }) => theme.button.icon.disabled.borderColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ToggleIconButton = styled(IconButton)`
|
||||
color: ${({ theme, toggle }) => (toggle ? theme.button.toggled.color : null)};
|
||||
&:before {
|
||||
text-shadow: 0 0 1px gray, 0 0 1px #d0bfa3;
|
||||
// text-shadow: 0 0 1px gray, 0 0 1px #d0bfa3;
|
||||
text-shadow: 0 0 1px
|
||||
${({ theme, toggle }) => (toggle ? theme.button.icon.disabled.borderColor : theme.button.icon.borderColor)};
|
||||
}
|
||||
`;
|
||||
|
||||
/* ===== Button Container ============================================================== */
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
place-content: ${({ alignment = "center" }) => alignment};
|
||||
align-content: space-around;
|
||||
margin: ${({ margin }) => margin};
|
||||
${StyledAnchor},
|
||||
${StyledNavLink},
|
||||
${StyledButton} {
|
||||
margin-right: 1rem;
|
||||
&:nth-last-child(1) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
ButtonContainer.aligment = {
|
||||
left: "flex-start",
|
||||
right: "flex-end",
|
||||
center: "center"
|
||||
};
|
||||
|
||||
export default Button;
|
||||
|
||||
@@ -76,14 +76,18 @@ export const ControlPanel = ({
|
||||
</ThemeSelector>
|
||||
</HorizontalStack>
|
||||
<HorizontalStack style={{ minHeight: "2rem" }}>
|
||||
<span>Zoom: {zoom}</span>
|
||||
<span>
|
||||
Zoom: <b>{zoom}</b>
|
||||
</span>
|
||||
<div style={{ display: "inline-block" }}>
|
||||
<IconButton icon="minus-circle" size={1} onClick={zoomOut} />
|
||||
<IconButton icon="plus-circle" size={1} onClick={zoomIn} />
|
||||
</div>
|
||||
</HorizontalStack>
|
||||
<HorizontalStack style={{ minHeight: "2rem" }}>
|
||||
<span>FPS: {fps}</span>
|
||||
<span>
|
||||
FPS: <b>{fps}</b>
|
||||
</span>
|
||||
<div style={{ display: "inline-block" }}>
|
||||
<IconButton icon="minus-circle" size={1} onClick={() => setFps(fps - 1)} />
|
||||
<IconButton icon="plus-circle" size={1} onClick={() => setFps(fps + 1)} />
|
||||
|
||||
@@ -35,7 +35,9 @@ const MenuButton = styled(NavLink)`
|
||||
const HeaderMenu = () => (
|
||||
<MenuPanel>
|
||||
<MenuItem>
|
||||
<MenuButton to="/">Snake</MenuButton>
|
||||
<MenuButton to="/" exact>
|
||||
Snake
|
||||
</MenuButton>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuButton to="/about" exact>
|
||||
|
||||
72
src/components/Highscore.js
Normal file
72
src/components/Highscore.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
import { module, registerHighscore } from "../redux/highscore.js";
|
||||
import styled from "styled-components";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
const HighscoreContainer = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0.5rem 0 1rem;
|
||||
`;
|
||||
|
||||
const HighscoreEntries = styled.ul`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-indent: 0;
|
||||
`;
|
||||
|
||||
const Position = styled.div`
|
||||
font-weight: bold;
|
||||
min-width: 1rem;
|
||||
text-align: right;
|
||||
margin-right: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: ${({ theme }) => theme.colors.colorInactive};
|
||||
`;
|
||||
const Username = styled.div`
|
||||
overflow: hidden;
|
||||
width: 9rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
const Score = styled.div`
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
min-width: 1.5rem;
|
||||
text-align: right;
|
||||
color: ${({ theme }) => theme.colors.colorAlternate};
|
||||
`;
|
||||
|
||||
const HighscoreEntry = styled.li`
|
||||
list-style: none;
|
||||
line-height: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-content: center;
|
||||
`;
|
||||
|
||||
const Highscore = ({ highscores = [] }) => {
|
||||
console.log("highscores", highscores);
|
||||
return (
|
||||
<HighscoreContainer>
|
||||
<Title>Highscore</Title>
|
||||
{!highscores.length && <p>No highscores yet</p>}
|
||||
<HighscoreEntries>
|
||||
{highscores.map((highscore, index) => (
|
||||
<HighscoreEntry key={`${highscore.name}-${highscore.gameId}-${highscore.score}`}>
|
||||
<Position>{index + 1}</Position>
|
||||
<Username>{highscore.name}</Username>
|
||||
<Score>{highscore.score}</Score>
|
||||
</HighscoreEntry>
|
||||
))}
|
||||
</HighscoreEntries>
|
||||
</HighscoreContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => ({ ...state[module.name] }),
|
||||
{ registerHighscore }
|
||||
)(Highscore);
|
||||
61
src/components/HighscoreInput.js
Normal file
61
src/components/HighscoreInput.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import Banner from "./Banner.js";
|
||||
import Button, { ButtonContainer } from "./Button.js";
|
||||
import Input from "./Input.js";
|
||||
|
||||
const Field = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
// align-content: center;
|
||||
// place-content: center;
|
||||
// justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
${Input} {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const OkButton = styled(Button)`
|
||||
min-width: 4.6875rem;
|
||||
`;
|
||||
|
||||
export const HighscoreInput = ({ name, score, gameId, onSubmit }) => {
|
||||
const [username, setUsername] = React.useState(name || "");
|
||||
return (
|
||||
<Banner>
|
||||
<Banner.Title>Congratulations!</Banner.Title>
|
||||
<p style={{ textAlign: "center" }}>
|
||||
You have a new highscore of <b>{score}</b>
|
||||
</p>
|
||||
<form
|
||||
onSubmit={e => e.preventDefault() || onSubmit({ name: username, score, gameId })}
|
||||
onKeyUp={({ which }) => which === 27 && onSubmit(null)}>
|
||||
<Field>
|
||||
<label htmlFor="name">Name</label>
|
||||
<Input
|
||||
maxLength="20"
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<ButtonContainer margin="1rem 0 0" alignment={ButtonContainer.aligment.center}>
|
||||
<OkButton disabled={!username.length} inverse type="submit">
|
||||
OK
|
||||
</OkButton>
|
||||
<OkButton>Cancel</OkButton>
|
||||
</ButtonContainer>
|
||||
</form>
|
||||
</Banner>
|
||||
);
|
||||
};
|
||||
|
||||
export default HighscoreInput;
|
||||
19
src/components/Input.js
Normal file
19
src/components/Input.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const Input = styled.input`
|
||||
appearance: none;
|
||||
border-width: ${({ theme }) => theme.input.borderWidth};
|
||||
border-style: solid;
|
||||
border-color: ${({ theme }) => theme.input.borderColor};
|
||||
background-color: ${({ theme }) => theme.input.background};
|
||||
color: ${({ theme }) => theme.input.color};
|
||||
padding: ${({ theme }) => theme.input.padding};
|
||||
border-radius: ${({ theme }) => theme.input.borderRadius};
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
outline: ${({ disabled }) => (!disabled ? "1px solid silver" : "none")};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Input;
|
||||
@@ -24,8 +24,8 @@ const App = ({ theme }) => (
|
||||
<Page>
|
||||
<Switch>
|
||||
<Route exact path="/" component={Snake} />
|
||||
<Route path="/snake" exact component={Snake} />
|
||||
<Route path="/about" exact component={Home} />
|
||||
<Route exact path="/snake" component={Snake} />
|
||||
<Route exact path="/about" component={Home} />
|
||||
</Switch>
|
||||
</Page>
|
||||
<Footer />
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
zoomIn,
|
||||
zoomOut
|
||||
} from "../redux/snake.js";
|
||||
import { module as highscoreModule, hasHighscore, registerHighscore, skipHighscore } from "../redux/highscore.js";
|
||||
|
||||
/* Components */
|
||||
import ControlPanel from "../components/ControlPanel.js";
|
||||
@@ -23,6 +24,8 @@ import Stage from "../components/Stage.js";
|
||||
import SnakePart from "../components/SnakePart.js";
|
||||
import Apple from "../components/Apple.js";
|
||||
import Banner from "../components/Banner.js";
|
||||
import Highscore from "../components/Highscore.js";
|
||||
import HighscoreInput from "../components/HighscoreInput.js";
|
||||
|
||||
const Layout = styled.div`
|
||||
display: flex;
|
||||
@@ -48,6 +51,10 @@ const Snake = ({
|
||||
pauseSnake,
|
||||
updateFrameSnake,
|
||||
changeTheme,
|
||||
hasHighscore,
|
||||
registerHighscore,
|
||||
skipHighscore,
|
||||
lastUsername,
|
||||
theme,
|
||||
grid,
|
||||
started,
|
||||
@@ -56,10 +63,20 @@ const Snake = ({
|
||||
fps,
|
||||
setFps,
|
||||
score,
|
||||
gameId,
|
||||
lastGameId,
|
||||
zoom,
|
||||
zoomIn,
|
||||
zoomOut
|
||||
}) => {
|
||||
const onSubmitHighscore = result => {
|
||||
if (result) {
|
||||
registerHighscore(result);
|
||||
} else {
|
||||
skipHighscore(gameId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Layout>
|
||||
@@ -70,10 +87,14 @@ const Snake = ({
|
||||
(cell.type === "snake" && <SnakePart zoom={zoom} value={cell} died={died} />)
|
||||
}
|
||||
</Stage>
|
||||
{!started && <Banner>Press 'r' to start the game</Banner>}
|
||||
{!started && <Banner fade>Press 'r' to start the game</Banner>}
|
||||
{died && lastGameId !== gameId && hasHighscore(score) && (
|
||||
<HighscoreInput score={score} gameId={gameId} name={lastUsername} onSubmit={onSubmitHighscore} />
|
||||
)}
|
||||
</StageContainer>
|
||||
<SidePanel>
|
||||
<Scoreboard score={score} zoom={2} />
|
||||
<Scoreboard score={score} zoom={3} />
|
||||
<Highscore />
|
||||
<ControlPanel
|
||||
updateFrameSnake={updateFrameSnake}
|
||||
started={started}
|
||||
@@ -98,7 +119,9 @@ const Snake = ({
|
||||
const mapStateToProps = state => ({
|
||||
...state[uiModule.name],
|
||||
...state[gameModule.name],
|
||||
...state[snakeModule.name]
|
||||
...state[snakeModule.name],
|
||||
...state[highscoreModule.name],
|
||||
hasHighscore: hasHighscore(state)
|
||||
});
|
||||
|
||||
const mapActionsToProps = {
|
||||
@@ -111,7 +134,9 @@ const mapActionsToProps = {
|
||||
setFps,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
changeTheme
|
||||
changeTheme,
|
||||
registerHighscore,
|
||||
skipHighscore
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -19,13 +19,17 @@ export const [SET_FPS, setFps] = module.action("SET_FPS", fps => ({ fps }));
|
||||
export const [INVOKE_CALLERS, invokeCallers] = module.action("INVOKE_CALLERS");
|
||||
export const [REGISTER_CALLER, registerCaller] = module.action("REGISTER_CALLER", name => ({ name }));
|
||||
export const [UNREGISTER_CALLER, unregisterCaller] = module.action("UNREGISTER_CALLER", name => ({ name }));
|
||||
export const [KEY_PRESS, keyPress] = module.action("KEY_PRESS", ({ key, altKey, ctrlKey, metaKey, shiftKey }) => ({
|
||||
key,
|
||||
altKey,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
shiftKey
|
||||
}));
|
||||
export const [KEY_PRESS, keyPress] = module.action(
|
||||
"KEY_PRESS",
|
||||
({ key, altKey, ctrlKey, metaKey, shiftKey, target }) => ({
|
||||
key,
|
||||
altKey,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
shiftKey,
|
||||
target
|
||||
})
|
||||
);
|
||||
export const [SUBSCRIBE_KEY_PRESSED, subscribeKeyPressed] = module.action("SUBSCRIBE_KEY_PRESSED", name => ({ name }));
|
||||
export const [UNSUBSCRIBE_KEY_PRESSED, unsubscribeKeyPressed] = module.action("UNSUBSCRIBE_KEY_PRESSED", name => ({
|
||||
name
|
||||
@@ -100,9 +104,10 @@ module.middleware(INIT, (dispatch, { loopId }) => {
|
||||
|
||||
module.middleware(
|
||||
KEY_PRESS,
|
||||
(dispatch, { keyPressSubscribers = [], fps }, { key, altKey, ctrlKey, metaKey, shiftKey }) => {
|
||||
console.log("key", key);
|
||||
keyPressSubscribers.forEach(subscriber => dispatch({ type: subscriber, key, altKey, ctrlKey, metaKey, shiftKey }));
|
||||
(dispatch, { keyPressSubscribers = [], fps }, { key, altKey, ctrlKey, metaKey, shiftKey, target }) => {
|
||||
keyPressSubscribers.forEach(subscriber =>
|
||||
dispatch({ type: subscriber, key, altKey, ctrlKey, metaKey, shiftKey, target })
|
||||
);
|
||||
|
||||
if (key === keys.PLUS || key === keys.EQUAL) {
|
||||
dispatch(setFps(fps + 1));
|
||||
|
||||
70
src/redux/highscore.js
Normal file
70
src/redux/highscore.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import Module from "./Module.js";
|
||||
|
||||
const MODULE_NAME = "HIGHSCORE";
|
||||
const HIGHSCORE_LENGTH = 10;
|
||||
|
||||
const initialState = {
|
||||
highscores: [],
|
||||
lastUsername: "",
|
||||
lastGameId: null
|
||||
};
|
||||
|
||||
export const module = new Module(MODULE_NAME, initialState);
|
||||
|
||||
export const [UPDATE_HIGHSCORE, updateHighscore] = module.action("UPDATE_HIGHSCORE");
|
||||
export const [SKIP_HIGHSCORE, skipHighscore] = module.action("SKIP_HIGHSCORE", gameId => ({ gameId }));
|
||||
export const [REGISTER_HIGHSCORE, registerHighscore] = module.action(
|
||||
"REGISTER_HIGHSCORE",
|
||||
({ name, score, gameId }) => ({
|
||||
name,
|
||||
score,
|
||||
gameId
|
||||
})
|
||||
);
|
||||
|
||||
export const sortHighscores = highscores =>
|
||||
highscores.sort((a, b) => {
|
||||
if (a.score > b.score) {
|
||||
return -1;
|
||||
}
|
||||
if (a.score < b.score) {
|
||||
return 1;
|
||||
}
|
||||
if (a.gameId < b.gameId) {
|
||||
return -1;
|
||||
}
|
||||
if (a.gameId > b.gameId) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
export const updateHighscores = (highscores, { name, score, gameId }) => {
|
||||
const newHighscores = [...highscores, { name, score, gameId }];
|
||||
return sortHighscores(newHighscores).slice(0, HIGHSCORE_LENGTH);
|
||||
};
|
||||
|
||||
/* === Selectors ================================================================================ */
|
||||
|
||||
export const hasHighscore = module.selector(
|
||||
({ [MODULE_NAME]: { highscores = [] } = { highscores: [] } } = { [MODULE_NAME]: { highscores: [] } }, score) => {
|
||||
const currentScores = sortHighscores(highscores || []).slice(0, HIGHSCORE_LENGTH);
|
||||
if (currentScores.length < HIGHSCORE_LENGTH) {
|
||||
return true;
|
||||
}
|
||||
return score > (currentScores[currentScores.length - 1] || { score: 0 }).score;
|
||||
}
|
||||
);
|
||||
|
||||
/* === Reducers ================================================================================= */
|
||||
|
||||
module.reducer(REGISTER_HIGHSCORE, (state = {}, { name, score, gameId }) => ({
|
||||
...state,
|
||||
lastGameId: gameId,
|
||||
lastUsername: name,
|
||||
highscores: updateHighscores(state.highscores || [], { name, gameId, score })
|
||||
}));
|
||||
|
||||
module.reducer(SKIP_HIGHSCORE, (state = {}, { gameId }) => ({ ...state, lastGameId: gameId }));
|
||||
|
||||
export default module;
|
||||
111
src/redux/highscore.spec.js
Normal file
111
src/redux/highscore.spec.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import module, { skipHighscore, registerHighscore, hasHighscore } from "./highscore.js";
|
||||
|
||||
const reducers = module.reducers[module.name];
|
||||
|
||||
test("Skip highscore", () => {
|
||||
const result1 = reducers(null, skipHighscore(123));
|
||||
expect(result1).toStrictEqual({ lastGameId: 123 });
|
||||
|
||||
const result2 = reducers({ test: 456, lastGameId: 348 }, skipHighscore(999));
|
||||
expect(result2).toStrictEqual({ test: 456, lastGameId: 999 });
|
||||
});
|
||||
|
||||
const highscores = [
|
||||
{ name: "mock101", score: 101, gameId: 22 },
|
||||
{ name: "mock60", score: 60, gameId: 22 },
|
||||
{ name: "mock20", score: 20, gameId: 10 },
|
||||
{ name: "mock40", score: 40, gameId: 9 },
|
||||
{ name: "mock50", score: 50, gameId: 29 },
|
||||
{ name: "mock130", score: 130, gameId: 2 },
|
||||
{ name: "mock80", score: 80, gameId: 12 },
|
||||
{ name: "mock90", score: 90, gameId: 2 },
|
||||
{ name: "mock30", score: 30, gameId: 13 },
|
||||
{ name: "mock140", score: 140, gameId: 2 },
|
||||
{ name: "mock100", score: 100, gameId: 2 },
|
||||
{ name: "mock110", score: 110, gameId: 2 },
|
||||
{ name: "mock120", score: 120, gameId: 2 },
|
||||
{ name: "mock70", score: 70, gameId: 20 },
|
||||
{ name: "mock150", score: 150, gameId: 2 }
|
||||
];
|
||||
|
||||
test("Register highscore where there is only 1 out 10 scores stored", () => {
|
||||
const result1 = reducers(undefined, registerHighscore({ name: "test", score: 10, gameId: 2 }));
|
||||
expect(result1).toStrictEqual({ highscores: [{ name: "test", score: 10, gameId: 2 }], lastGameId: 2 });
|
||||
});
|
||||
|
||||
test("Register highscore when there are more than 10 scores stored and the new score is not in the top 10", () => {
|
||||
const result = reducers({ highscores }, registerHighscore({ name: "test", score: 10, gameId: 8 }));
|
||||
expect(highscores.length).toBeGreaterThan(10);
|
||||
expect(result.highscores.length).toEqual(10);
|
||||
expect(result).toStrictEqual({
|
||||
highscores: [
|
||||
{ name: "mock150", score: 150, gameId: 2 },
|
||||
{ name: "mock140", score: 140, gameId: 2 },
|
||||
{ name: "mock130", score: 130, gameId: 2 },
|
||||
{ name: "mock120", score: 120, gameId: 2 },
|
||||
{ name: "mock110", score: 110, gameId: 2 },
|
||||
{ name: "mock101", score: 101, gameId: 22 },
|
||||
{ name: "mock100", score: 100, gameId: 2 },
|
||||
{ name: "mock90", score: 90, gameId: 2 },
|
||||
{ name: "mock80", score: 80, gameId: 12 },
|
||||
{ name: "mock70", score: 70, gameId: 20 }
|
||||
],
|
||||
lastGameId: 8
|
||||
});
|
||||
});
|
||||
|
||||
test("Register highscore when there are more than 10 scores stored and the new score is in the top 10", () => {
|
||||
const result = reducers({ highscores }, registerHighscore({ name: "test", score: 103, gameId: 3 }));
|
||||
expect(result).toStrictEqual({
|
||||
highscores: [
|
||||
{ name: "mock150", score: 150, gameId: 2 },
|
||||
{ name: "mock140", score: 140, gameId: 2 },
|
||||
{ name: "mock130", score: 130, gameId: 2 },
|
||||
{ name: "mock120", score: 120, gameId: 2 },
|
||||
{ name: "mock110", score: 110, gameId: 2 },
|
||||
{ name: "test", score: 103, gameId: 3 },
|
||||
{ name: "mock101", score: 101, gameId: 22 },
|
||||
{ name: "mock100", score: 100, gameId: 2 },
|
||||
{ name: "mock90", score: 90, gameId: 2 },
|
||||
{ name: "mock80", score: 80, gameId: 12 }
|
||||
],
|
||||
lastGameId: 3
|
||||
});
|
||||
});
|
||||
|
||||
test("Register highscore when there are more than 10 scores stored and the new score has the same score as the current pos 10", () => {
|
||||
const result = reducers({ highscores }, registerHighscore({ name: "test", score: 70, gameId: 99 }));
|
||||
expect(result).toStrictEqual({
|
||||
highscores: [
|
||||
{ name: "mock150", score: 150, gameId: 2 },
|
||||
{ name: "mock140", score: 140, gameId: 2 },
|
||||
{ name: "mock130", score: 130, gameId: 2 },
|
||||
{ name: "mock120", score: 120, gameId: 2 },
|
||||
{ name: "mock110", score: 110, gameId: 2 },
|
||||
{ name: "mock101", score: 101, gameId: 22 },
|
||||
{ name: "mock100", score: 100, gameId: 2 },
|
||||
{ name: "mock90", score: 90, gameId: 2 },
|
||||
{ name: "mock80", score: 80, gameId: 12 },
|
||||
{ name: "mock70", score: 70, gameId: 20 }
|
||||
],
|
||||
lastGameId: 99
|
||||
});
|
||||
});
|
||||
|
||||
test("hasHighscore must return true when the new score is within the top 10", () => {
|
||||
const selector = hasHighscore({});
|
||||
const result = selector(34);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test("hasHighscore must return false when the new scrore is outside the top 10", () => {
|
||||
const selector = hasHighscore({ [module.name]: { highscores } });
|
||||
const result = selector(1);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test("hasHighscore must return false when the new scrore is the same as the number 10 on the list", () => {
|
||||
const selector = hasHighscore({ [module.name]: { highscores } });
|
||||
const result = selector(70);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
@@ -218,6 +218,10 @@ export class Module {
|
||||
return selection;
|
||||
}, {});
|
||||
}
|
||||
|
||||
selector(selectorFn) {
|
||||
return state => (...args) => selectorFn(state, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default Module;
|
||||
|
||||
@@ -12,10 +12,10 @@ const initialState = {
|
||||
zoom: 1.5,
|
||||
size: DEFAULT_GRID_SIZE,
|
||||
paused: false,
|
||||
gameId: 0,
|
||||
vX: 1,
|
||||
vY: 0,
|
||||
vXNext: undefined,
|
||||
vYNext: undefined,
|
||||
next: [],
|
||||
snake: [],
|
||||
score: 0,
|
||||
apple: []
|
||||
@@ -52,6 +52,18 @@ const markCell = (grid, [x, y] = [], mark) => {
|
||||
return grid;
|
||||
};
|
||||
|
||||
const updateGrid = ({ grid, apple, snake }) => {
|
||||
markCell(grid, apple, { type: "apple" });
|
||||
|
||||
snake.forEach((snakePart, i) =>
|
||||
markCell(grid, snakePart, {
|
||||
type: "snake",
|
||||
index: snake.length - 1 - i,
|
||||
length: snake.length
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const moveSnakePart = ([x, y], vX, vY, size) => {
|
||||
let newX = x + vX;
|
||||
let newY = y + vY;
|
||||
@@ -105,52 +117,41 @@ module.reducer(ZOOM_OUT, (state, { step = DEFAUL_ZOOM_STEP }) =>
|
||||
module.reducer(RESET_SNAKE, (state, options) => {
|
||||
const grid = createGrid(state.size);
|
||||
const zoom = state.zoom;
|
||||
const gameId = (state.gameId || 0) + 1;
|
||||
const apple = options.started ? randomPosition(state.size) : initialState.apple;
|
||||
const snake = options.started ? [[0, 0]] : initialState.snake;
|
||||
|
||||
markCell(grid, apple, { type: "apple" });
|
||||
snake.forEach((snakePart, i) =>
|
||||
markCell(grid, snakePart, {
|
||||
type: "snake",
|
||||
index: snake.length - 1 - i,
|
||||
length: snake.length
|
||||
})
|
||||
);
|
||||
return { ...state, ...initialState, grid, zoom, snake, apple, ...options };
|
||||
updateGrid({ grid, apple, snake });
|
||||
return { ...state, ...initialState, grid, zoom, snake, apple, gameId, ...options };
|
||||
});
|
||||
|
||||
module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
|
||||
const { vX, vY, vXNext, vYNext } = state;
|
||||
const { vX, vY, next } = state;
|
||||
const [pX, pY] = next.length ? next.slice(-1)[0] : [vX, vY];
|
||||
|
||||
if (vXNext !== undefined || vYNext !== undefined) {
|
||||
return state;
|
||||
if (direction === directions.UP && !pY) {
|
||||
return { ...state, next: [...next, [0, -1]] };
|
||||
}
|
||||
if (direction === directions.UP && vY !== 1) {
|
||||
return { ...state, vXNext: 0, vYNext: -1 };
|
||||
if (direction === directions.DOWN && !pY) {
|
||||
return { ...state, next: [...next, [0, 1]] };
|
||||
}
|
||||
if (direction === directions.DOWN && vY !== -1) {
|
||||
return { ...state, vXNext: 0, vYNext: 1 };
|
||||
if (direction === directions.LEFT && !pX) {
|
||||
return { ...state, next: [...next, [-1, 0]] };
|
||||
}
|
||||
if (direction === directions.LEFT && vX !== 1) {
|
||||
return { ...state, vXNext: -1, vYNext: 0 };
|
||||
}
|
||||
if (direction === directions.RIGHT && vX !== -1) {
|
||||
return { ...state, vXNext: 1, vYNext: 0 };
|
||||
if (direction === directions.RIGHT && !pX) {
|
||||
return { ...state, next: [...next, [1, 0]] };
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
module.reducer(UPDATE_FRAME_SNAKE, state => {
|
||||
const { snake, vX, vY, vXNext, vYNext, size } = state;
|
||||
const { snake, vX, vY, next, size } = state;
|
||||
let { apple, started, died, score } = state;
|
||||
const grid = createGrid(size);
|
||||
const nextPosition = moveSnakePart(
|
||||
snake[snake.length - 1],
|
||||
vXNext !== undefined ? vXNext : vX,
|
||||
vYNext !== undefined ? vYNext : vY,
|
||||
size
|
||||
);
|
||||
|
||||
const [vXNext, vYNext] = next.length ? next[0] : [vX, vY];
|
||||
|
||||
const nextPosition = moveSnakePart(snake[snake.length - 1], vXNext, vYNext, size);
|
||||
const newSnake = [...snake, nextPosition];
|
||||
|
||||
/* If the snake did not eat an apple then remove the last part of its tail */
|
||||
@@ -171,22 +172,15 @@ module.reducer(UPDATE_FRAME_SNAKE, state => {
|
||||
apple = randomPosition(size);
|
||||
}
|
||||
}
|
||||
markCell(grid, apple, { type: "apple" });
|
||||
newSnake.forEach((snakePart, i) =>
|
||||
markCell(grid, snakePart, {
|
||||
type: "snake",
|
||||
index: newSnake.length - 1 - i,
|
||||
length: snake.length
|
||||
})
|
||||
);
|
||||
updateGrid({ grid, apple, snake: newSnake });
|
||||
|
||||
score = newSnake.length;
|
||||
return {
|
||||
...state,
|
||||
grid,
|
||||
vX: vXNext !== undefined ? vXNext : vX,
|
||||
vY: vYNext !== undefined ? vYNext : vY,
|
||||
vXNext: undefined,
|
||||
vYNext: undefined,
|
||||
vX: vXNext,
|
||||
vY: vYNext,
|
||||
next: next.slice(1),
|
||||
snake: newSnake,
|
||||
apple,
|
||||
started,
|
||||
@@ -208,12 +202,10 @@ module.middleware(INIT, (dispatch, { started, paused, died }) => {
|
||||
if (!started && !died) {
|
||||
dispatch(resetSnake({ started: false, died: false }));
|
||||
}
|
||||
// dispatch({ type: "[GAME] UPDATE_LOOP_ID", loopId: undefined });
|
||||
if (!paused) {
|
||||
dispatch(registerCaller(UPDATE_FRAME_SNAKE));
|
||||
dispatch(subscribeKeyPressed(KEY_PRESSED_SNAKE));
|
||||
}
|
||||
// dispatch(startLoop());
|
||||
dispatch(initGame());
|
||||
});
|
||||
|
||||
@@ -226,7 +218,6 @@ module.middleware(START_SNAKE, dispatch => {
|
||||
|
||||
module.middleware(STOP_SNAKE, (dispatch, { started }) => {
|
||||
dispatch(unregisterCaller(UPDATE_FRAME_SNAKE));
|
||||
// dispatch(unsubscribeKeyPressed(KEY_PRESSED_SNAKE));
|
||||
dispatch(stopLoop());
|
||||
dispatch(resetSnake({ started: false }));
|
||||
});
|
||||
@@ -240,10 +231,13 @@ module.middleware(PAUSE_SNAKE, (dispatch, { paused }) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key, altKey, ctrlKey, metaKey, shiftKey }) => {
|
||||
module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key, altKey, ctrlKey, metaKey, shiftKey, target }) => {
|
||||
if (altKey || ctrlKey || metaKey) {
|
||||
return;
|
||||
}
|
||||
if (target.nodeName === "INPUT") {
|
||||
return;
|
||||
}
|
||||
if (key === keys.UP || key === keys.k) {
|
||||
dispatch(changeDirection(directions.UP));
|
||||
}
|
||||
|
||||
@@ -8,28 +8,22 @@ import { throttle } from "../utils/throttle.js";
|
||||
import { module as snakeModule } from "./snake.js";
|
||||
import { module as gameModule } from "./game.js";
|
||||
import { module as uiModule } from "./ui.js";
|
||||
import { module as highscoreModule } from "./highscore.js";
|
||||
|
||||
export const createStore = () => {
|
||||
const history = createBrowserHistory({
|
||||
basename: process.env.NODE_ENV === "production" ? "/redux/" : "/"
|
||||
});
|
||||
|
||||
console.log("loadState", loadState(), {
|
||||
router: connectRouter(history),
|
||||
...uiModule.reducers,
|
||||
...snakeModule.reducers,
|
||||
...gameModule.reducers
|
||||
});
|
||||
|
||||
const store = createReduxStore(
|
||||
combineReducers({
|
||||
router: connectRouter(history),
|
||||
...snakeModule.reducers,
|
||||
...uiModule.reducers,
|
||||
...gameModule.reducers
|
||||
...gameModule.reducers,
|
||||
...highscoreModule.reducers
|
||||
}),
|
||||
loadState(),
|
||||
// {},
|
||||
compose.apply(
|
||||
this,
|
||||
[
|
||||
@@ -42,13 +36,14 @@ export const createStore = () => {
|
||||
);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept(["./ui.js", "./snake.js", "./game.js", "./Module.js"], () => {
|
||||
module.hot.accept(["./ui.js", "./snake.js", "./game.js", "./Module.js", "./highscore.js"], () => {
|
||||
store.replaceReducer(
|
||||
combineReducers({
|
||||
router: connectRouter(history),
|
||||
...uiModule.reducers,
|
||||
...snakeModule.reducers,
|
||||
...gameModule.reducers
|
||||
...gameModule.reducers,
|
||||
...highscoreModule.reducers
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,9 +8,11 @@ const GlobalStyle = createGlobalStyle`
|
||||
margin: 0;
|
||||
background: ${props => props.theme.body.background};
|
||||
color: ${props => props.theme.body.color};
|
||||
min-height: 100vh;
|
||||
}
|
||||
body, input, select {
|
||||
font-family: ${({ theme }) => theme.body.fontFamily};
|
||||
font-size: ${({ theme }) => theme.body.fontSize};
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1, h2 {
|
||||
font-family: Lato;
|
||||
|
||||
@@ -4,15 +4,16 @@ import { renderTheme } from "./default.js";
|
||||
export const colors = {
|
||||
background: "#333",
|
||||
backgroundActive: "#828282",
|
||||
backgroundInactive: "silver",
|
||||
backgroundInactive: "#4d4d4d",
|
||||
backgroundAlternate: "#555",
|
||||
color: "palevioletred",
|
||||
colorActive: "#ecb1c5",
|
||||
colorInactive: "#e6b4c4",
|
||||
colorInactive: "#97727e",
|
||||
colorAlternate: "#ecb1c5",
|
||||
shadowColor: "#222",
|
||||
borderColor: "#222",
|
||||
borderColorActive: "silver",
|
||||
borderColorActive: "#e6b4c4",
|
||||
borderColorInactive: "#97727e",
|
||||
spinnerShadow: "#444",
|
||||
spinnerHighlight: "#db7093",
|
||||
cardFoldHighlight: "#ad5a75",
|
||||
|
||||
@@ -4,15 +4,16 @@ import { renderTheme } from "./default.js";
|
||||
export const colors = {
|
||||
background: "#333",
|
||||
backgroundActive: "#828282",
|
||||
backgroundInactive: "silver",
|
||||
backgroundInactive: "#4d4d4d",
|
||||
backgroundAlternate: "#555",
|
||||
color: "#7094db",
|
||||
colorActive: "#ecb1c5",
|
||||
colorInactive: "#b3c3e6",
|
||||
colorInactive: "#7d98d4",
|
||||
colorAlternate: "#ecb1c5",
|
||||
shadowColor: "#222",
|
||||
borderColor: "#222",
|
||||
borderColorActive: "silver",
|
||||
borderColorInactive: "#7d98d4",
|
||||
spinnerShadow: "#444",
|
||||
spinnerHighlight: "#db7093",
|
||||
cardFoldHighlight: "#091225",
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import merge from "deepmerge";
|
||||
|
||||
export const defaultColors = {
|
||||
none: "none",
|
||||
background: "white",
|
||||
backgroundAlternate: "#ddd",
|
||||
backgroundInactive: "silver",
|
||||
backgroundInactive: "#e2e2e2",
|
||||
backgroundActive: "#ccc",
|
||||
color: "black",
|
||||
colorActive: "#444",
|
||||
colorAlternate: "#222",
|
||||
colorInactive: "silver",
|
||||
colorInactive: "#bdbdbd",
|
||||
shadowColor: "gray",
|
||||
borderColor: "black",
|
||||
borderColorActive: "silver",
|
||||
borderColorInactive: "#bdbdbd",
|
||||
spinnerShadow: "#eee",
|
||||
spinnerHighlight: "black",
|
||||
cardFoldHighlight: "#888",
|
||||
@@ -27,6 +29,7 @@ const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + mi
|
||||
export const renderTheme = themeColors => {
|
||||
const colors = merge(defaultColors, themeColors);
|
||||
return {
|
||||
colors,
|
||||
body: {
|
||||
background: colors.background,
|
||||
color: colors.color,
|
||||
@@ -63,6 +66,11 @@ export const renderTheme = themeColors => {
|
||||
color: colors.background,
|
||||
borderWidth: `${1 / 16}rem`,
|
||||
borderRadius: `${3 / 16}rem`,
|
||||
disabled: {
|
||||
background: colors.backgroundInactive,
|
||||
color: colors.colorInactive,
|
||||
borderColor: colors.borderColorInactive
|
||||
},
|
||||
toggled: {
|
||||
background: colors.backgroundInactive,
|
||||
color: colors.colorInactive
|
||||
@@ -74,8 +82,24 @@ export const renderTheme = themeColors => {
|
||||
background: colors.backgroundActive,
|
||||
color: colors.colorActive,
|
||||
borderColor: colors.borderColorActive
|
||||
},
|
||||
icon: {
|
||||
borderColor: colors.borderColorActive,
|
||||
disabled: {
|
||||
background: colors.none,
|
||||
color: colors.backgroundInactive,
|
||||
borderColor: colors.borderColorInactive
|
||||
}
|
||||
}
|
||||
},
|
||||
input: {
|
||||
borderColor: colors.borderColor,
|
||||
padding: ".5rem",
|
||||
background: colors.background,
|
||||
color: colors.color,
|
||||
borderWidth: `${1 / 16}rem`,
|
||||
borderRadius: `${3 / 16}rem`
|
||||
},
|
||||
tooltip: {
|
||||
borderColor: colors.borderColor,
|
||||
borderWidth: `${1 / 16}rem`,
|
||||
|
||||
Reference in New Issue
Block a user