Implemented a snake game with redux
This commit is contained in:
3
.babelrc
Normal file
3
.babelrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||||
|
}
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
public/main.js
|
||||||
|
*tar
|
||||||
3
Dockerfile
Normal file
3
Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
FROM nginx
|
||||||
|
COPY public /usr/share/nginx/html
|
||||||
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||||
44
default.conf
Normal file
44
default.conf
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
#charset koi8-r;
|
||||||
|
#access_log /var/log/nginx/host.access.log main;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
alias /usr/share/nginx/html/;
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error_page 404 /404.html;
|
||||||
|
|
||||||
|
# redirect server error pages to the static page /50x.html
|
||||||
|
#
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
|
||||||
|
#
|
||||||
|
#location ~ \.php$ {
|
||||||
|
# proxy_pass http://127.0.0.1;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
|
||||||
|
#
|
||||||
|
#location ~ \.php$ {
|
||||||
|
# root html;
|
||||||
|
# fastcgi_pass 127.0.0.1:9000;
|
||||||
|
# fastcgi_index index.php;
|
||||||
|
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
|
||||||
|
# include fastcgi_params;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# deny access to .htaccess files, if Apache's document root
|
||||||
|
# concurs with nginx's one
|
||||||
|
#
|
||||||
|
#location ~ /\.ht {
|
||||||
|
# deny all;
|
||||||
|
#}
|
||||||
|
}
|
||||||
31
nginx.conf
Normal file
31
nginx.conf
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes 1;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
#tcp_nopush on;
|
||||||
|
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
#gzip on;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
37
package.json
Normal file
37
package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "keybook-redux",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.5.5",
|
||||||
|
"@babel/preset-env": "^7.5.5",
|
||||||
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
"babel-loader": "^8.0.6",
|
||||||
|
"babel-plugin-styled-components": "^1.10.6",
|
||||||
|
"react-hot-loader": "^4.12.11",
|
||||||
|
"webpack": "^4.39.2",
|
||||||
|
"webpack-cli": "^3.3.7",
|
||||||
|
"webpack-dev-server": "^3.8.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"connected-react-router": "^6.5.2",
|
||||||
|
"history": "^4.9.0",
|
||||||
|
"react": "^16.9.0",
|
||||||
|
"react-dom": "^16.9.0",
|
||||||
|
"react-redux": "^7.1.0",
|
||||||
|
"react-router": "^5.0.1",
|
||||||
|
"react-router-dom": "^5.0.1",
|
||||||
|
"redux": "^4.0.4",
|
||||||
|
"styled-components": "^4.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
public/index.html
Normal file
11
public/index.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Redux Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
60
src/components/Button.js
Normal file
60
src/components/Button.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const SharedStyle = () => `
|
||||||
|
border: 1px solid gray;
|
||||||
|
border-radius: .1875rem;
|
||||||
|
background-color: palevioletred;
|
||||||
|
color: papayawhip;
|
||||||
|
text-align: center;
|
||||||
|
padding: .3125rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
text-decoration: none;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButton = styled.button`
|
||||||
|
${SharedStyle}
|
||||||
|
-webkit-appearance: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAnchor = styled.a`
|
||||||
|
${SharedStyle}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledNavLink = styled(NavLink)`
|
||||||
|
${SharedStyle}
|
||||||
|
&.active {
|
||||||
|
border-color: silver;
|
||||||
|
color: #828282;
|
||||||
|
background-color: #ecb1c5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Button = (props, refs) => {
|
||||||
|
const updatedProps = { ...props };
|
||||||
|
|
||||||
|
if (props.onClick) {
|
||||||
|
updatedProps.onClick = e => e.preventDefault() || props.onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.href) {
|
||||||
|
return StyledAnchor.render(updatedProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.to) {
|
||||||
|
return StyledNavLink.render(updatedProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StyledButton.render(updatedProps);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
48
src/components/HeaderMenu.js
Normal file
48
src/components/HeaderMenu.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
|
||||||
|
const MenuPanel = styled.ul`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItem = styled.li`
|
||||||
|
display: inline-block;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuButton = styled(NavLink)`
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid silver;
|
||||||
|
background: ${props => props.theme.header.menuButton.background};
|
||||||
|
color: ${props => props.theme.header.menuButton.color};
|
||||||
|
padding: 0.3125rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
min-width: 3.125rem;
|
||||||
|
&.active {
|
||||||
|
background: ${props => props.theme.header.menuButton.active.background};
|
||||||
|
color: ${props => props.theme.header.menuButton.active.color};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeaderMenu = () => (
|
||||||
|
<MenuPanel>
|
||||||
|
<MenuItem>
|
||||||
|
<MenuButton to="/" exact>
|
||||||
|
Home
|
||||||
|
</MenuButton>
|
||||||
|
</MenuItem>
|
||||||
|
{
|
||||||
|
// <MenuItem>
|
||||||
|
// <MenuButton to="/keyboards">Keyboards</MenuButton>
|
||||||
|
// </MenuItem>
|
||||||
|
}
|
||||||
|
<MenuItem>
|
||||||
|
<MenuButton to="/snake">Snake</MenuButton>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuPanel>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default HeaderMenu;
|
||||||
32
src/components/Link.js
Normal file
32
src/components/Link.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
|
||||||
|
const SharedStyle = () => `
|
||||||
|
color: gray;
|
||||||
|
text-decoration: underline;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLink = styled.a`
|
||||||
|
${SharedStyle}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledNavLink = styled(NavLink)`
|
||||||
|
${SharedStyle}
|
||||||
|
&.active {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Link = (props, refs) => {
|
||||||
|
const updatedProps = { ...props };
|
||||||
|
|
||||||
|
if (props.onClick) {
|
||||||
|
updatedProps.onClick = e => e.preventDefault() || props.onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.to ? StyledNavLink.render(updatedProps) : StyledLink.render(updatedProps);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Link;
|
||||||
23
src/components/List.js
Normal file
23
src/components/List.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const StyledList = styled.ul`
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-indent: 0;
|
||||||
|
list-style: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const List = ({ items, Item, itemProps, Empty }) => {
|
||||||
|
if (items && items.length) {
|
||||||
|
return <StyledList>{items.map((item, index) => Item({ ...itemProps, item }))}</StyledList>;
|
||||||
|
}
|
||||||
|
if ((!items || !items.length) && Empty) {
|
||||||
|
return Empty({ ...itemProps });
|
||||||
|
}
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
|
|
||||||
|
List.Item = styled.li``;
|
||||||
|
|
||||||
|
export default List;
|
||||||
30
src/components/Logo.js
Normal file
30
src/components/Logo.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const LogoPanel = styled.div`
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Logo = () => (
|
||||||
|
<LogoPanel>
|
||||||
|
<h1>Hello Redux</h1>
|
||||||
|
<h2>Redux demo application</h2>
|
||||||
|
</LogoPanel>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Logo;
|
||||||
9
src/components/Page.js
Normal file
9
src/components/Page.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Page = styled.div`
|
||||||
|
max-width: 60rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Page;
|
||||||
60
src/components/Spinner.js
Normal file
60
src/components/Spinner.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Spinner = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 1.875rem;
|
||||||
|
height: 1.875rem;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border: 0.4rem solid ${props => props.theme.spinner.shadow};
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
transform: translateZ(0);
|
||||||
|
border-top: 0.4rem solid ${props => props.theme.spinner.highlight}30;
|
||||||
|
border-right: 0.4rem solid ${props => props.theme.spinner.highlight}30;
|
||||||
|
border-bottom: 0.4rem solid ${props => props.theme.spinner.highlight}30;
|
||||||
|
border-left: 0.4rem solid ${props => props.theme.spinner.highlight};
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
animation: load8 1.1s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes load8 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes load8 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Spinner;
|
||||||
7
src/components/Title.js
Normal file
7
src/components/Title.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Title = styled.h1`
|
||||||
|
font-size: ${props => (props.large ? "3rem" : "1.5rem")};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Title;
|
||||||
44
src/containers/App.js
Normal file
44
src/containers/App.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Route, Switch } from "react-router"; // react-router v4/v5
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { hot } from "react-hot-loader/root";
|
||||||
|
|
||||||
|
/* Containers */
|
||||||
|
import Header from "./Header";
|
||||||
|
import Footer from "./Footer";
|
||||||
|
import Page from "../components/Page";
|
||||||
|
import Home from "./Home";
|
||||||
|
import Keyboards from "./Keyboards";
|
||||||
|
import KeyboardDetails from "./KeyboardDetails";
|
||||||
|
import Snake from "./Snake";
|
||||||
|
|
||||||
|
const App = ({ title, onNewTitle, getKeyboards, keyboards }) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Header />
|
||||||
|
<Page>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/" component={Home} />
|
||||||
|
<Route path="/keyboards/:id" exact component={KeyboardDetails} />
|
||||||
|
<Route path="/keyboards" exact component={Keyboards} />
|
||||||
|
<Route path="/snake" exact component={Snake} />
|
||||||
|
<Route path="/test/snake" exact component={Snake} />
|
||||||
|
</Switch>
|
||||||
|
</Page>
|
||||||
|
<Footer />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapState = () => ({});
|
||||||
|
|
||||||
|
const mapActions = {};
|
||||||
|
|
||||||
|
const ConnectedApp = connect(
|
||||||
|
mapState,
|
||||||
|
mapActions
|
||||||
|
)(App);
|
||||||
|
|
||||||
|
// const App = () => <h1>Hello Redux!!!</h1>;
|
||||||
|
|
||||||
|
console.log("env", process.env.NODE_ENV);
|
||||||
|
process.env.NODE_ENV === "development" ? console.log("development") : console.log("production");
|
||||||
|
export default (process.env.NODE_ENV === "development" ? hot(ConnectedApp) : ConnectedApp);
|
||||||
20
src/containers/Footer.js
Normal file
20
src/containers/Footer.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const FooterContainer = styled.header`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100vw;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 4.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Footer = () => (
|
||||||
|
<FooterContainer>
|
||||||
|
<p>Made with ❤ by Crafity</p>
|
||||||
|
</FooterContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
24
src/containers/Header.js
Normal file
24
src/containers/Header.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Logo from "../components/Logo.js";
|
||||||
|
import HeaderMenu from "../components/HeaderMenu.js";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const HeaderContainer = styled.header`
|
||||||
|
background-color: ${props => props.theme.header.background};
|
||||||
|
display: flex;
|
||||||
|
min-height: 6.25rem;
|
||||||
|
align-content: space-between;
|
||||||
|
place-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Header = () => (
|
||||||
|
<HeaderContainer>
|
||||||
|
<Logo />
|
||||||
|
<HeaderMenu />
|
||||||
|
</HeaderContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Header;
|
||||||
6
src/containers/Home.js
Normal file
6
src/containers/Home.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Title from "../components/Title.js";
|
||||||
|
|
||||||
|
const Home = () => <Title large>Home Page</Title>;
|
||||||
|
|
||||||
|
export default Home;
|
||||||
67
src/containers/KeyboardDetails.js
Normal file
67
src/containers/KeyboardDetails.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { getKeyboards } from "../redux/keyboards.js";
|
||||||
|
import Spinner from "../components/Spinner.js";
|
||||||
|
import Title from "../components/Title.js";
|
||||||
|
|
||||||
|
const KeyboardDetailsContainer = ({ children }) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Title large>Keyboard Details</Title>
|
||||||
|
{children}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
class KeyboardDetails extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.id = parseInt(this.props.match.params.id, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.getKeyboards();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const keyboardsLoaded = !!this.props.keyboards;
|
||||||
|
const loading = !keyboardsLoaded || this.props.spinner;
|
||||||
|
|
||||||
|
const keyboard = this.props.keyboards && this.props.keyboards.find(k => k.id === this.id);
|
||||||
|
const keyboardExists = keyboardsLoaded && !!keyboard;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<KeyboardDetailsContainer>
|
||||||
|
<Spinner />
|
||||||
|
</KeyboardDetailsContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keyboardExists) {
|
||||||
|
return (
|
||||||
|
<KeyboardDetailsContainer>
|
||||||
|
<p>Keyboard not found...</p>
|
||||||
|
</KeyboardDetailsContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardDetailsContainer>
|
||||||
|
<p>{keyboard.maker}</p>
|
||||||
|
</KeyboardDetailsContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = ({ keyboards, ui }) => ({
|
||||||
|
keyboards: keyboards.list,
|
||||||
|
spinner: ui.spinner
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
getKeyboards
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
state,
|
||||||
|
actions
|
||||||
|
)(KeyboardDetails);
|
||||||
138
src/containers/Snake.js
Normal file
138
src/containers/Snake.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { startSnake, stopSnake, pauseSnake, updateFrameSnake, keyPressedSnake, keys } from "../redux/snake.js";
|
||||||
|
import Title from "../components/Title.js";
|
||||||
|
import Button from "../components/Button.js";
|
||||||
|
|
||||||
|
const ToggleButton = styled(Button)`
|
||||||
|
background-color: ${props => (props.toggle ? "silver" : null)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Row = styled.div``;
|
||||||
|
|
||||||
|
const Cell = styled.div`
|
||||||
|
width: ${({ theme }) => theme.snake.cell.size};
|
||||||
|
height: ${({ theme }) => theme.snake.cell.size};
|
||||||
|
border: ${({ theme }) => theme.snake.cell.border};
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: center;
|
||||||
|
background-color: ${props => props.value && props.theme.snake.cellColors[props.value]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Grid = ({ data }) =>
|
||||||
|
data.map((r, y) => (
|
||||||
|
<Row key={`y${y}`}>
|
||||||
|
{r.map((c, x) => (
|
||||||
|
<Cell key={`x${x}y${y}`} value={c}>
|
||||||
|
{c === "a" && (
|
||||||
|
<span role="img" aria-label="apple">
|
||||||
|
🍏
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Cell>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
));
|
||||||
|
|
||||||
|
// const ArrowButton = styled(Button)`
|
||||||
|
// min-width: 5rem;
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// const ArrowCluster = () => (
|
||||||
|
// <div>
|
||||||
|
// <div>
|
||||||
|
// <ArrowButton>Up</ArrowButton>
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <ArrowButton>Left</ArrowButton>
|
||||||
|
// <ArrowButton>Down</ArrowButton>
|
||||||
|
// <ArrowButton>Right</ArrowButton>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
|
||||||
|
class Snake extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
|
this.supportedKeys = Object.values(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(e) {
|
||||||
|
if (!this.supportedKeys.includes(e.key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.keyPressedSnake(e.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener("keydown", this.handleKeyDown, false);
|
||||||
|
this.props.startSnake();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { startSnake, stopSnake, pauseSnake, updateFrameSnake, snakeGame } = this.props;
|
||||||
|
const { grid, started, paused, gameId, snake, died, fps } = snakeGame;
|
||||||
|
|
||||||
|
if (died) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Title large>Snake</Title>
|
||||||
|
<Title>
|
||||||
|
You died{" "}
|
||||||
|
<span role="img" aria-label="skull">
|
||||||
|
☠️
|
||||||
|
</span>{" "}
|
||||||
|
with {snake.length} points.
|
||||||
|
</Title>
|
||||||
|
<Button onClick={startSnake}>Restart</Button>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!started && !died) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Title large>Snake</Title>
|
||||||
|
<Button onClick={startSnake}>Start</Button>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Title large>Snake: Attempt {gameId}</Title>
|
||||||
|
<p>Points: {snake.length}</p>
|
||||||
|
<Grid data={grid} />
|
||||||
|
<Button onClick={updateFrameSnake}>Next</Button>
|
||||||
|
<ToggleButton toggle={paused} onClick={pauseSnake}>
|
||||||
|
{paused ? "Resume" : "Pause"}
|
||||||
|
</ToggleButton>
|
||||||
|
<Button onClick={stopSnake}>Stop</Button>
|
||||||
|
<Button onClick={startSnake}>Reset</Button>
|
||||||
|
<span>FPS: {fps}</span>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener("keydown", this.handleKeyDown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = ({ snakeGame }) => ({ snakeGame: snakeGame || {} });
|
||||||
|
|
||||||
|
const mapActionsToProps = {
|
||||||
|
startSnake,
|
||||||
|
stopSnake,
|
||||||
|
pauseSnake,
|
||||||
|
updateFrameSnake,
|
||||||
|
keyPressedSnake
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapActionsToProps
|
||||||
|
)(Snake);
|
||||||
54
src/containers/keyboards.js
Normal file
54
src/containers/keyboards.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import Title from "../components/Title.js";
|
||||||
|
import List from "../components/List.js";
|
||||||
|
import Button from "../components/Button.js";
|
||||||
|
import Link from "../components/Link.js";
|
||||||
|
import Spinner from "../components/Spinner.js";
|
||||||
|
import { getKeyboards, showKeyboard } from "../redux/keyboards.js";
|
||||||
|
|
||||||
|
const KeyboardListItem = ({ item, showKeyboard }) => (
|
||||||
|
<List.Item key={item.id}>
|
||||||
|
<Link to={`/keyboards/${item.id}`}>
|
||||||
|
{item.maker} - {item.model}
|
||||||
|
</Link>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
|
||||||
|
const NoKeyboardsFound = ({ spinner }) => {
|
||||||
|
if (spinner) {
|
||||||
|
return <p>Loading...</p>;
|
||||||
|
}
|
||||||
|
return <p>There are no keyboards</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Keyboards extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.getKeyboards();
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { keyboards, spinner, getKeyboards } = this.props;
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Title large>Keyboard List</Title>
|
||||||
|
<List items={keyboards} Item={KeyboardListItem} Empty={NoKeyboardsFound} itemProps={this.props} />
|
||||||
|
<Button onClick={() => getKeyboards({ force: true })}>Refresh</Button>
|
||||||
|
{spinner && <Spinner />}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mapState = ({ keyboards, ui }) => ({
|
||||||
|
keyboards: keyboards.list,
|
||||||
|
spinner: ui.spinner
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapActions = {
|
||||||
|
getKeyboards,
|
||||||
|
showKeyboard
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapState,
|
||||||
|
mapActions
|
||||||
|
)(Keyboards);
|
||||||
52
src/index.js
Normal file
52
src/index.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { createStore, combineReducers, compose, applyMiddleware } from "redux";
|
||||||
|
import { createBrowserHistory } from "history";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
|
||||||
|
import { connectRouter, routerMiddleware, ConnectedRouter } from "connected-react-router";
|
||||||
|
import { reducers as keyboardReducers, middleware as keyboardMiddleware } from "./redux/keyboards.js";
|
||||||
|
import { middleware as apiMiddleware } from "./redux/api.js";
|
||||||
|
import { reducers as uiReducers } from "./redux/ui.js";
|
||||||
|
import { reducers as snakeReducers, middleware as snakeMiddleware } from "./redux/snake.js";
|
||||||
|
|
||||||
|
import { ThemeProvider } from "styled-components";
|
||||||
|
import { themes } from "./theming/theme.js";
|
||||||
|
import GlobalStyle from "./theming/GlobalStyle.js";
|
||||||
|
|
||||||
|
import App from "./containers/App.js";
|
||||||
|
|
||||||
|
const history = createBrowserHistory({
|
||||||
|
basename: process.env.NODE_ENV === "production" ? "/redux/" : "/"
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = createStore(
|
||||||
|
combineReducers({
|
||||||
|
keyboards: keyboardReducers,
|
||||||
|
ui: uiReducers,
|
||||||
|
snakeGame: snakeReducers,
|
||||||
|
router: connectRouter(history)
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
compose.apply(
|
||||||
|
this,
|
||||||
|
[
|
||||||
|
applyMiddleware(routerMiddleware(history), apiMiddleware, keyboardMiddleware, snakeMiddleware),
|
||||||
|
process.env.NODE_ENV !== "production" &&
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION__ &&
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||||
|
].filter(m => m)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ThemeProvider theme={themes.main}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<GlobalStyle />
|
||||||
|
<App />
|
||||||
|
</ConnectedRouter>
|
||||||
|
</ThemeProvider>
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById("root")
|
||||||
|
);
|
||||||
22
src/redux/api.js
Normal file
22
src/redux/api.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const FETCH_DATA = "[API] FETCH_DATA";
|
||||||
|
|
||||||
|
export const fetchData = ({ url, onError, onSuccess }) => ({ type: FETCH_DATA, url, onError, onSuccess });
|
||||||
|
|
||||||
|
const keyboardList = [
|
||||||
|
{ id: 1, maker: "Percent Studio", model: "Canoe" },
|
||||||
|
{ id: 2, maker: "Gray Studio", model: "Space65" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const middleware = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === FETCH_DATA) {
|
||||||
|
if (action.url === "/keyboards") {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch({ type: action.onSuccess, data: keyboardList });
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
dispatch({ type: action.onError, data: new Error(`Unable to fetch data from url ${action.url}`) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
65
src/redux/keyboards.js
Normal file
65
src/redux/keyboards.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { push } from "connected-react-router";
|
||||||
|
import { showSpinner, hideSpinner } from "./ui.js";
|
||||||
|
import { fetchData } from "./api.js";
|
||||||
|
|
||||||
|
const GET_KEYBOARDS = "[Keyboards] GET_KEYBOARDS";
|
||||||
|
const LOAD_KEYBOARDS = "[Keyboards] LOAD_KEYBOARDS";
|
||||||
|
const LOAD_KEYBOARDS_SUCCEEDED = "[Keyboards] LOAD_KEYBOARDS_SUCCEEDED";
|
||||||
|
const LOAD_KEYBOARDS_FAILED = "[Keyboards] LOAD_KEYBOARDS_FAILED";
|
||||||
|
const UPDATE_KEYBOARDS = "[Keyboards] UPDATE_KEYBOARDS";
|
||||||
|
const GET_KEYBOARD = "[Keyboards] GET_KEYBOARD";
|
||||||
|
const SHOW_KEYBOARD_DETAILS = "[Keyboards] SHOW_KEYBOARD_DETAILS";
|
||||||
|
|
||||||
|
export const loadKeyboards = ({ onError, onSuccess }) => ({ type: LOAD_KEYBOARDS, onError, onSuccess });
|
||||||
|
export const getKeyboards = ({ force } = { force: false }) => ({ type: GET_KEYBOARDS, force });
|
||||||
|
export const updateKeyboards = keyboards => ({ type: UPDATE_KEYBOARDS, keyboards });
|
||||||
|
export const showKeyboard = id => ({ type: SHOW_KEYBOARD_DETAILS, id });
|
||||||
|
export const getKeyboard = id => ({ type: GET_KEYBOARD, id });
|
||||||
|
|
||||||
|
export const middleware = ({ dispatch, getState }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === SHOW_KEYBOARD_DETAILS) {
|
||||||
|
dispatch(push(`/keyboards/${action.id}`));
|
||||||
|
}
|
||||||
|
if (action.type === GET_KEYBOARDS) {
|
||||||
|
const { keyboards } = getState();
|
||||||
|
if (!keyboards.list || action.force) {
|
||||||
|
dispatch(showSpinner());
|
||||||
|
dispatch(loadKeyboards({ onError: LOAD_KEYBOARDS_FAILED, onSuccess: LOAD_KEYBOARDS_SUCCEEDED }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === LOAD_KEYBOARDS) {
|
||||||
|
dispatch(fetchData({ url: "/keyboards", onError: action.onError, onSuccess: action.onSuccess }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === LOAD_KEYBOARDS_SUCCEEDED) {
|
||||||
|
dispatch(hideSpinner());
|
||||||
|
dispatch(updateKeyboards(action.data));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducers = (keyboards = {}, action) => {
|
||||||
|
if (action.type === UPDATE_KEYBOARDS) {
|
||||||
|
return {
|
||||||
|
...keyboards,
|
||||||
|
list: action.keyboards,
|
||||||
|
error: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === LOAD_KEYBOARDS_FAILED) {
|
||||||
|
return {
|
||||||
|
...keyboards,
|
||||||
|
list: undefined,
|
||||||
|
error: action.err
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === GET_KEYBOARD) {
|
||||||
|
return {
|
||||||
|
...keyboards
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyboards;
|
||||||
|
};
|
||||||
181
src/redux/snake.js
Normal file
181
src/redux/snake.js
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
const START_SNAKE = "[Snake] START_SNAKE";
|
||||||
|
const STOP_SNAKE = "[Snake] STOP_SNAKE";
|
||||||
|
const PAUSE_SNAKE = "[Snake] PAUSE_SNAKE";
|
||||||
|
const RESET_SNAKE = "[Snake] RESET_SNAKE";
|
||||||
|
const NEXT_FRAME_SNAKE = "[Snake] NEXT_FRAME_SNAKE";
|
||||||
|
const UPDATE_FRAME_SNAKE = "[Snake] UPDATE_FRAME_SNAKE";
|
||||||
|
const KEY_PRESSED_SNAKE = "[Snake] KEY_PRESSED_SNAKE";
|
||||||
|
|
||||||
|
export const startSnake = () => ({ type: START_SNAKE });
|
||||||
|
export const stopSnake = () => ({ type: STOP_SNAKE });
|
||||||
|
export const pauseSnake = () => ({ type: PAUSE_SNAKE });
|
||||||
|
export const resetSnake = () => ({ type: RESET_SNAKE });
|
||||||
|
export const nextFrameSnake = () => ({ type: NEXT_FRAME_SNAKE });
|
||||||
|
export const updateFrameSnake = () => ({ type: UPDATE_FRAME_SNAKE });
|
||||||
|
export const keyPressedSnake = key => ({ type: KEY_PRESSED_SNAKE, key });
|
||||||
|
|
||||||
|
export const keys = {
|
||||||
|
UP: "ArrowUp",
|
||||||
|
RIGHT: "ArrowRight",
|
||||||
|
DOWN: "ArrowDown",
|
||||||
|
LEFT: "ArrowLeft",
|
||||||
|
INCREASE: "+",
|
||||||
|
DECREASE: "-"
|
||||||
|
};
|
||||||
|
export const size = 16;
|
||||||
|
|
||||||
|
const randomPosition = size => [Math.round(Math.random() * (size - 1)), Math.round(Math.random() * (size - 1))];
|
||||||
|
const comparePositions = (pos1, pos2) => pos1[0] === pos2[0] && pos1[1] === pos2[1];
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
grid: [[]],
|
||||||
|
size,
|
||||||
|
started: false,
|
||||||
|
paused: false,
|
||||||
|
vX: 1,
|
||||||
|
vY: 0,
|
||||||
|
snake: [[0, 0], [1, 0], [2, 0]],
|
||||||
|
apple: randomPosition(size),
|
||||||
|
gameId: 0,
|
||||||
|
fps: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
const createGrid = size => {
|
||||||
|
const rows = [];
|
||||||
|
for (let y = 0; y < size; y += 1) {
|
||||||
|
const columns = [];
|
||||||
|
for (let x = 0; x < size; x += 1) {
|
||||||
|
if (Math.random() > 9) {
|
||||||
|
columns[x] = "x";
|
||||||
|
} else {
|
||||||
|
columns[x] = " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows[y] = columns;
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
const markCell = (grid, [x, y], mark) => {
|
||||||
|
grid[y][x] = mark;
|
||||||
|
return grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveSnakePart = ([x, y], vX, vY, size) => {
|
||||||
|
let newX = x + vX;
|
||||||
|
let newY = y + vY;
|
||||||
|
if (newX > size - 1) {
|
||||||
|
newX = 0;
|
||||||
|
}
|
||||||
|
if (newX < 0) {
|
||||||
|
newX = size - 1;
|
||||||
|
}
|
||||||
|
if (newY > size - 1) {
|
||||||
|
newY = 0;
|
||||||
|
}
|
||||||
|
if (newY < 0) {
|
||||||
|
newY = size - 1;
|
||||||
|
}
|
||||||
|
return [newX, newY];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducers = (state = initialState, action) => {
|
||||||
|
if (action.type === STOP_SNAKE) {
|
||||||
|
return { ...state, started: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === PAUSE_SNAKE) {
|
||||||
|
return { ...state, paused: !state.paused };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === RESET_SNAKE) {
|
||||||
|
const grid = createGrid(state.size);
|
||||||
|
const { snake, apple } = initialState;
|
||||||
|
markCell(grid, apple, "a");
|
||||||
|
snake.forEach(p => markCell(grid, p, "s"));
|
||||||
|
return { ...initialState, started: true, paused: false, grid, snake, gameId: state.gameId + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === KEY_PRESSED_SNAKE) {
|
||||||
|
if (action.key === keys.UP) {
|
||||||
|
return { ...state, vX: 0, vY: -1 };
|
||||||
|
}
|
||||||
|
if (action.key === keys.DOWN) {
|
||||||
|
return { ...state, vX: 0, vY: 1 };
|
||||||
|
}
|
||||||
|
if (action.key === keys.LEFT) {
|
||||||
|
return { ...state, vX: -1, vY: 0 };
|
||||||
|
}
|
||||||
|
if (action.key === keys.RIGHT) {
|
||||||
|
return { ...state, vX: 1, vY: 0 };
|
||||||
|
}
|
||||||
|
if (action.key === keys.INCREASE) {
|
||||||
|
return { ...state, fps: state.fps + 1 };
|
||||||
|
}
|
||||||
|
if (action.key === keys.DECREASE) {
|
||||||
|
return { ...state, fps: state.fps - 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === UPDATE_FRAME_SNAKE) {
|
||||||
|
const { snake, vX, vY, size } = state;
|
||||||
|
let { apple, started, died } = state;
|
||||||
|
const grid = createGrid(size);
|
||||||
|
const newSnake = [...snake];
|
||||||
|
const nextPosition = moveSnakePart(newSnake[newSnake.length - 1], vX, vY, size);
|
||||||
|
if (comparePositions(nextPosition, apple)) {
|
||||||
|
apple = randomPosition(size);
|
||||||
|
// eslint-disable-next-line no-loop-func
|
||||||
|
while (newSnake.filter(p => comparePositions(p, apple)).length) {
|
||||||
|
apple = randomPosition(size);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newSnake.shift();
|
||||||
|
}
|
||||||
|
if (snake.filter(p => comparePositions(p, nextPosition)).length) {
|
||||||
|
started = false;
|
||||||
|
died = true;
|
||||||
|
}
|
||||||
|
markCell(grid, apple, "a");
|
||||||
|
newSnake.push(nextPosition);
|
||||||
|
newSnake.forEach(p => markCell(grid, p, "s"));
|
||||||
|
return { ...state, grid, vX, vY, snake: newSnake, apple, started, died };
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const middleware = ({ dispatch, getState }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === START_SNAKE) {
|
||||||
|
dispatch(resetSnake());
|
||||||
|
animate(1, () => dispatch(nextFrameSnake()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === NEXT_FRAME_SNAKE) {
|
||||||
|
const {
|
||||||
|
snakeGame: { started, paused, gameId, fps }
|
||||||
|
} = getState();
|
||||||
|
!paused && dispatch(updateFrameSnake());
|
||||||
|
started &&
|
||||||
|
animate(fps, () => {
|
||||||
|
if (gameId !== getState().snakeGame.gameId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(nextFrameSnake());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const animate = (fps, callback) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const loop = () =>
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (Date.now() - now > 1000 / fps) {
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
return loop();
|
||||||
|
});
|
||||||
|
return loop();
|
||||||
|
};
|
||||||
30
src/redux/ui.js
Normal file
30
src/redux/ui.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const SHOW_SPINNER = "[UI] SHOW_SPINNER";
|
||||||
|
const HIDE_SPINNER = "[UI] HIDE_SPINNER";
|
||||||
|
const TOGGLE_SPINNER = "[UI] TOGGLE_SPINNER";
|
||||||
|
|
||||||
|
export const showSpinner = () => ({ type: SHOW_SPINNER });
|
||||||
|
export const hideSpinner = () => ({ type: HIDE_SPINNER });
|
||||||
|
export const toggleSpinner = () => ({ type: TOGGLE_SPINNER });
|
||||||
|
|
||||||
|
export const reducers = (ui = {}, action) => {
|
||||||
|
if (action.type === SHOW_SPINNER) {
|
||||||
|
return {
|
||||||
|
...ui,
|
||||||
|
spinner: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === HIDE_SPINNER) {
|
||||||
|
return {
|
||||||
|
...ui,
|
||||||
|
spinner: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === TOGGLE_SPINNER) {
|
||||||
|
return {
|
||||||
|
...ui,
|
||||||
|
spinner: !ui.spinner
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui;
|
||||||
|
};
|
||||||
17
src/theming/GlobalStyle.js
Normal file
17
src/theming/GlobalStyle.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createGlobalStyle } from "styled-components";
|
||||||
|
|
||||||
|
const GlobalStyle = createGlobalStyle`
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 0 4.25rem;
|
||||||
|
box-sizing:border-box;
|
||||||
|
margin: 0;
|
||||||
|
background: ${props => props.theme.body.background};
|
||||||
|
color: ${props => props.theme.body.color};
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default GlobalStyle;
|
||||||
38
src/theming/theme.js
Normal file
38
src/theming/theme.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const themes = {
|
||||||
|
main: {
|
||||||
|
body: {
|
||||||
|
background: "papayawhip",
|
||||||
|
color: "palevioletred"
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
background: "#ffe6bd",
|
||||||
|
color: "palevioletred",
|
||||||
|
|
||||||
|
menuButton: {
|
||||||
|
background: "#ffe6bd",
|
||||||
|
color: "palevioletred",
|
||||||
|
active: {
|
||||||
|
background: "palevioletred",
|
||||||
|
color: "#ffe6bd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spinner: {
|
||||||
|
shadow: "#eeeeee",
|
||||||
|
highlight: "#db7093"
|
||||||
|
},
|
||||||
|
snake: {
|
||||||
|
cell: {
|
||||||
|
border: "0.0625rem solid silver",
|
||||||
|
size: "1.5rem"
|
||||||
|
},
|
||||||
|
cellColors: {
|
||||||
|
" ": "",
|
||||||
|
a: "green",
|
||||||
|
s: "orange"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { themes as default, themes };
|
||||||
32
webpack.config.js
Normal file
32
webpack.config.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const webpack = require("webpack");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
target: "web",
|
||||||
|
mode: "development",
|
||||||
|
entry: path.resolve(__dirname, "src/index.js"),
|
||||||
|
output: { path: path.resolve(__dirname, "public") },
|
||||||
|
devtool: "inline-source-map",
|
||||||
|
devServer: {
|
||||||
|
contentBase: path.join(__dirname, "public"),
|
||||||
|
// public: "http://localhost:9000/redux",
|
||||||
|
// publicPath: "/redux/",
|
||||||
|
hot: true,
|
||||||
|
host: "0.0.0.0",
|
||||||
|
compress: true,
|
||||||
|
port: 9000,
|
||||||
|
historyApiFallback: true
|
||||||
|
},
|
||||||
|
plugins: [new webpack.HotModuleReplacementPlugin()],
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
25
webpack.production.config.js
Normal file
25
webpack.production.config.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const webpack = require("webpack");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
target: "web",
|
||||||
|
mode: "production",
|
||||||
|
entry: path.resolve(__dirname, "src/index.js"),
|
||||||
|
output: { path: path.resolve(__dirname, "public") },
|
||||||
|
plugins: [new webpack.HotModuleReplacementPlugin()],
|
||||||
|
devServer: {
|
||||||
|
hot: false,
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user