diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..7f92389 --- /dev/null +++ b/.babelrc @@ -0,0 +1,31 @@ +{ + "plugins": [ + "@babel/transform-runtime", + "@babel/proposal-class-properties" + ], + "presets": [ + [ + "@babel/env", + { + "modules": false + } + ], + "@babel/react" + ], + "env": { + "production": { + "compact": false, + "only": [ + "src" + ], + "plugins": [ + "transform-react-remove-prop-types", + "@babel/transform-react-inline-elements", + "@babel/transform-react-constant-elements" + ] + }, + "development" : { + "compact": false + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7174001 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a20d07d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,86 @@ +const fs = require('fs'); +const path = require('path'); + +const prettierOptions = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '.prettierrc'), 'utf8') +); + +module.exports = { + parser: 'babel-eslint', + extends: ['airbnb', 'prettier'], + plugins: ['prettier', 'react', 'jsx-a11y'], + env: { + jest: true, + browser: true, + node: true, + es6: true + }, + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + }, + rules: { + 'prettier/prettier': ['error', prettierOptions], + 'arrow-body-style': [2, 'as-needed'], + 'class-methods-use-this': 0, + 'import/imports-first': 0, + 'import/newline-after-import': 0, + 'import/no-dynamic-require': 0, + 'import/no-extraneous-dependencies': 0, + 'import/no-named-as-default': 0, + 'import/no-unresolved': 2, + 'import/no-webpack-loader-syntax': 0, + 'import/prefer-default-export': 0, + indent: [ + 2, + 2, + { + SwitchCase: 1 + } + ], + 'jsx-a11y/aria-props': 2, + 'jsx-a11y/heading-has-content': 0, + 'jsx-a11y/label-has-associated-control': [ + 2, + { + // NOTE: If this error triggers, either disable it or add + // your custom components, labels and attributes via these options + // See https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/label-has-associated-control.md + controlComponents: ['Input'] + } + ], + 'jsx-a11y/label-has-for': 0, + 'jsx-a11y/mouse-events-have-key-events': 2, + 'jsx-a11y/role-has-required-aria-props': 2, + 'jsx-a11y/role-supports-aria-props': 2, + 'max-len': 0, + 'newline-per-chained-call': 0, + 'no-confusing-arrow': 0, + 'no-console': 1, + 'no-unused-vars': 2, + 'no-use-before-define': 0, + 'prefer-template': 2, + 'react/destructuring-assignment': 0, + 'react/jsx-closing-tag-location': 0, + 'react/forbid-prop-types': 0, + 'react/jsx-first-prop-new-line': [2, 'multiline'], + 'react/jsx-filename-extension': 0, + 'react/jsx-no-target-blank': 0, + 'react/jsx-uses-vars': 2, + 'react/require-default-props': 0, + 'react/require-extension': 0, + 'react/self-closing-comp': 0, + 'react/sort-comp': 0, + 'require-yield': 0 + }, + settings: { + 'import/resolver': { + webpack: { + config: './config/webpack.prod.babel.js' + } + } + } +}; diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..d6bf1a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1b0cca8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true +} diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 0000000..388d813 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,7 @@ +{ + "processors": ["stylelint-processor-styled-components"], + "extends": [ + "stylelint-config-recommended", + "stylelint-config-styled-components" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c099341 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome", + "url": "http://localhost:3000", + "webRoot": "${workspaceRoot}/src", + "sourceMapPathOverrides": { + "webpack:///./app/*": "${webRoot}/*", + "webpack:///app/*": "${webRoot}/*" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1a2cc06 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,33 @@ +{ + "editor.wordWrap": "on", + "editor.wordWrapColumn": 80, + + // Files + "files.associations": { + ".babelrc": "jsonc", + ".eslintrc": "jsonc", + ".flowconfig": "ini", + "*.js": "javascriptreact" + }, + "files.exclude": { + "**/*.log": true, + "**/*.log*": true, + "**/dist": true, + "**/coverage": true + }, + + // Linting: + "eslint.enable": true, + "eslint.validate": ["javascript", "javascriptreact", "vue"], + "stylelint.enable": true, + + // Set the default + "editor.formatOnSave": false, + // Enable per-language + "[javascript]": { + "editor.formatOnSave": true + }, + "[javascriptreact]": { + "editor.formatOnSave": true + } +} diff --git a/README.md b/README.md new file mode 100755 index 0000000..c6fa6c6 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ + +### `npm start` + +Runs the app in the development mode.
+ +### `npm run build` + +Builds the app for production to the `build` folder.
diff --git a/config/webpack.base.babel.js b/config/webpack.base.babel.js new file mode 100755 index 0000000..55f7320 --- /dev/null +++ b/config/webpack.base.babel.js @@ -0,0 +1,92 @@ +/** + * COMMON WEBPACK CONFIGURATION + */ + +const path = require('path'); +const webpack = require('webpack'); + +process.noDeprecation = true; + +module.exports = options => ({ + mode: options.mode, + entry: options.entry, + output: Object.assign( + { + path: path.resolve(process.cwd(), 'build'), + publicPath: '/' + }, + options.output + ), + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: options.babelQuery + } + }, + { + // Preprocess our own .scss files + test: /\.scss$/, + exclude: /node_modules/, + use: ['style-loader', 'css-loader', 'sass-loader'] + }, + { + // Preprocess 3rd party .css files located in node_modules + test: /\.css$/, + include: /node_modules/, + use: ['style-loader', 'css-loader'] + }, + { + test: /\.(eot|svg|otf|ttf|woff|woff2)$/, + use: 'file-loader' + }, + { + test: /\.(jpg|png|gif)$/, + use: 'file-loader' + }, + { + test: /\.html$/, + use: 'html-loader' + }, + { + test: /\.(mp4|webm)$/, + use: { + loader: 'url-loader', + options: { + limit: 10000 + } + } + } + ] + }, + plugins: options.plugins.concat([ + new webpack.ProvidePlugin({ + // make fetch available + fetch: 'exports-loader?self.fetch!whatwg-fetch' + }), + + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify(process.env.NODE_ENV) + } + }) + ]), + resolve: { + modules: ['app', 'node_modules'], + extensions: ['.js', '.jsx', '.scss', '.react.js'], + mainFields: ['browser', 'jsnext:main', 'main'] + }, + devtool: options.devtool, + target: 'web', + performance: options.performance || {}, + optimization: { + namedModules: true, + splitChunks: { + name: 'vendor', + minChunks: 2 + } + } +}); diff --git a/config/webpack.dev.babel.js b/config/webpack.dev.babel.js new file mode 100755 index 0000000..2d09a7c --- /dev/null +++ b/config/webpack.dev.babel.js @@ -0,0 +1,39 @@ +/** + * DEVELOPMENT WEBPACK CONFIGURATION + */ + +const path = require('path'); +const webpack = require('webpack'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const CircularDependencyPlugin = require('circular-dependency-plugin'); + +module.exports = require('./webpack.base.babel')({ + mode: 'development', + entry: [ + 'eventsource-polyfill', // Necessary for hot reloading with IE + 'webpack-hot-middleware/client?reload=true', + path.join(process.cwd(), 'src/index.js') + ], + + output: { + filename: '[name].js', + chunkFilename: '[name].chunk.js' + }, + + plugins: [ + new webpack.HotModuleReplacementPlugin(), + new HtmlWebpackPlugin({ + inject: true, + template: 'static/index.html' + }), + new CircularDependencyPlugin({ + exclude: /a\.js|node_modules/, // exclude node_modules + failOnError: false + }) + ], + devtool: 'inline-source-map', // 'eval-source-map', + + performance: { + hints: false + } +}); diff --git a/config/webpack.prod.babel.js b/config/webpack.prod.babel.js new file mode 100755 index 0000000..437cf8f --- /dev/null +++ b/config/webpack.prod.babel.js @@ -0,0 +1,36 @@ +// Important modules this config uses +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = require('./webpack.base.babel')({ + mode: 'production', + entry: [path.join(process.cwd(), 'src/index.js')], + output: { + filename: '[name].[chunkhash].js', + chunkFilename: '[name].[chunkhash].chunk.js' + }, + + plugins: [ + new HtmlWebpackPlugin({ + template: 'static/index.html', + minify: { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true + }, + inject: true + }) + ], + + performance: { + assetFilter: assetFilename => + !/(\.map$)|(^(main\.|favicon\.))/.test(assetFilename) + } +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..131a09a --- /dev/null +++ b/package.json @@ -0,0 +1,110 @@ +{ + "name": "shake-tree", + "version": "0.1.0", + "private": true, + "author": { + "name": "Özgür ERSOY", + "email": "oersoy@hotmail.com.tr", + "url": "http://www.ozgurersoy.com.tr/shake-tree" + }, + "scripts": { + "prebuild": "npm run build:clean", + "build": "npm run prebuild && cross-env NODE_ENV=production webpack --config config/webpack.prod.babel.js --color -p --progress --hide-modules --display-optimization-bailout", + "build:clean": "rimraf ./build", + "start": "cross-env NODE_ENV=development node server", + "start:prod": "cross-env NODE_ENV=production node server", + "start:production": "npm run build && npm run start:prod" + }, + "dependencies": { + "@babel/plugin-transform-runtime": "^7.2.0", + "@babel/polyfill": "^7.0.0", + "compression": "1.7.3", + "cross-env": "5.2.0", + "eslint-config-prettier": "^4.0.0", + "eslint-plugin-prettier": "^3.0.1", + "express": "4.16.3", + "fontfaceobserver": "2.0.13", + "immutable": "3.8.2", + "invariant": "2.2.4", + "ip": "1.1.5", + "lodash": "4.17.11", + "minimist": "^1.2.0", + "prettier": "^1.16.4", + "react": "^16.8.1", + "react-app-polyfill": "^0.2.0", + "react-dev-utils": "^7.0.1", + "react-dom": "^16.8.1", + "react-redux": "^6.0.0", + "redux": "^4.0.1", + "redux-immutable": "^4.0.0" + }, + "devDependencies": { + "@babel/cli": "7.1.2", + "@babel/core": "^7.1.2", + "@babel/plugin-proposal-class-properties": "7.1.0", + "@babel/plugin-syntax-dynamic-import": "7.0.0", + "@babel/plugin-transform-modules-commonjs": "7.1.0", + "@babel/plugin-transform-react-constant-elements": "7.0.0", + "@babel/plugin-transform-react-inline-elements": "7.0.0", + "@babel/preset-env": "^7.1.0", + "@babel/preset-react": "7.0.0", + "@babel/register": "^7.0.0", + "add-asset-html-webpack-plugin": "3.0.1", + "babel-core": "7.0.0-bridge.0", + "babel-eslint": "10.0.1", + "babel-loader": "^8.0.4", + "babel-plugin-dynamic-import-node": "2.2.0", + "babel-plugin-lodash": "3.3.4", + "babel-plugin-react-intl": "3.0.1", + "babel-plugin-react-transform": "3.0.0", + "babel-plugin-transform-react-remove-prop-types": "0.4.19", + "circular-dependency-plugin": "5.0.2", + "css-loader": "1.0.0", + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", + "eslint": "5.7.0", + "eslint-config-airbnb": "17.1.0", + "eslint-config-airbnb-base": "13.1.0", + "eslint-import-resolver-webpack": "0.10.1", + "eslint-plugin-import": "2.14.0", + "eslint-plugin-jsx-a11y": "6.1.2", + "eslint-plugin-react": "^7.12.4", + "eventsource-polyfill": "0.9.6", + "exports-loader": "0.7.0", + "file-loader": "1.1.11", + "html-loader": "0.5.5", + "html-webpack-plugin": "3.2.0", + "imports-loader": "0.8.0", + "lint-staged": "7.3.0", + "node-plop": "0.16.0", + "node-sass": "^4.7.2", + "null-loader": "0.1.1", + "plop": "2.1.0", + "postcss-flexbugs-fixes": "^4.1.0", + "postcss-loader": "^3.0.0", + "react-test-renderer": "^16.5.2", + "rimraf": "2.6.2", + "sass-loader": "^7.0.1", + "shelljs": "^0.8.1", + "style-loader": "0.23.1", + "terser-webpack-plugin": "1.1.0", + "url-loader": "1.1.2", + "webpack": "4.20.2", + "webpack-cli": "^3.1.2", + "webpack-dev-middleware": "3.4.0", + "webpack-hot-middleware": "2.24.3" + }, + "resolutions": { + "babel-core": "7.0.0-bridge.0" + }, + "engines": { + "node": ">=8.10.0", + "npm": ">=5" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} diff --git a/server/index.js b/server/index.js new file mode 100755 index 0000000..80f8858 --- /dev/null +++ b/server/index.js @@ -0,0 +1,30 @@ +/* eslint consistent-return:0 */ + +const express = require('express'); +const { resolve } = require('path'); +const logger = require('./util//logger'); + +const argv = require('./util/argv'); +const port = require('./util//port'); +const setup = require('./middlewares/frontendMiddleware'); + +const app = express(); + +// In production we need to pass these values in instead of relying on webpack +setup(app, { + outputPath: resolve(process.cwd(), 'build'), + publicPath: '/' +}); + +// get the intended host and port number, use localhost and port 3000 if not provided +const customHost = argv.host || process.env.HOST; +const host = customHost || null; // Let http.Server use its default IPv6/4 host +const prettyHost = customHost || 'localhost'; + +// Start your app. +app.listen(port, host, err => { + if (err) { + return logger.error(err.message); + } + logger.appStarted(port, prettyHost); +}); diff --git a/server/middlewares/addDevMiddlewares.js b/server/middlewares/addDevMiddlewares.js new file mode 100755 index 0000000..115f517 --- /dev/null +++ b/server/middlewares/addDevMiddlewares.js @@ -0,0 +1,36 @@ +const path = require('path'); +const webpack = require('webpack'); +const webpackDevMiddleware = require('webpack-dev-middleware'); +const webpackHotMiddleware = require('webpack-hot-middleware'); + +function createWebpackMiddleware(compiler, publicPath) { + return webpackDevMiddleware(compiler, { + noInfo: true, + publicPath, + silent: true, + stats: 'errors-only' + }); +} + +module.exports = function addDevMiddlewares(app, webpackConfig) { + const compiler = webpack(webpackConfig); + const middleware = createWebpackMiddleware( + compiler, + webpackConfig.output.publicPath + ); + + app.use(middleware); + app.use(webpackHotMiddleware(compiler)); + + const fs = middleware.fileSystem; + + app.get('*', (req, res) => { + fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { + if (err) { + res.sendStatus(404); + } else { + res.send(file.toString()); + } + }); + }); +}; diff --git a/server/middlewares/addProdMiddlewares.js b/server/middlewares/addProdMiddlewares.js new file mode 100755 index 0000000..0c56cfd --- /dev/null +++ b/server/middlewares/addProdMiddlewares.js @@ -0,0 +1,15 @@ +const path = require('path'); +const express = require('express'); +const compression = require('compression'); + +module.exports = function addProdMiddlewares(app, options) { + const publicPath = options.publicPath || '/'; + const outputPath = options.outputPath || path.resolve(process.cwd(), 'build'); + + app.use(compression()); + app.use(publicPath, express.static(outputPath)); + + app.get('*', (req, res) => + res.sendFile(path.resolve(outputPath, 'index.html')) + ); +}; diff --git a/server/middlewares/frontendMiddleware.js b/server/middlewares/frontendMiddleware.js new file mode 100755 index 0000000..0cb1306 --- /dev/null +++ b/server/middlewares/frontendMiddleware.js @@ -0,0 +1,16 @@ +/* eslint-disable global-require */ + +module.exports = (app, options) => { + const isProd = process.env.NODE_ENV === 'production'; + + if (isProd) { + const addProdMiddlewares = require('./addProdMiddlewares'); + addProdMiddlewares(app, options); + } else { + const webpackConfig = require('../../config/webpack.dev.babel'); + const addDevMiddlewares = require('./addDevMiddlewares'); + addDevMiddlewares(app, webpackConfig); + } + + return app; +}; diff --git a/server/util/argv.js b/server/util/argv.js new file mode 100755 index 0000000..be8c0b9 --- /dev/null +++ b/server/util/argv.js @@ -0,0 +1 @@ +module.exports = require('minimist')(process.argv.slice(2)); diff --git a/server/util/logger.js b/server/util/logger.js new file mode 100755 index 0000000..9dd9a90 --- /dev/null +++ b/server/util/logger.js @@ -0,0 +1,30 @@ +/* eslint-disable no-console */ + +const chalk = require('chalk'); +const ip = require('ip'); + +const divider = chalk.gray('\n-----------------------------------'); + +/** + * Logger middleware, you can customize it to make messages more personal + */ +const logger = { + // Called whenever there's an error on the server we want to print + error: err => { + console.error(chalk.red(err)); + }, + + // Called when express.js app starts on given port w/o errors + appStarted: (port, host) => { + console.log(`Server started ! ${chalk.green('✓')}`); + + console.log(` +${chalk.bold('Access URLs:')}${divider} +Localhost: ${chalk.magenta(`http://${host}:${port}`)} + LAN: ${chalk.magenta(`http://${ip.address()}:${port}`)}${divider} +${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)} + `); + } +}; + +module.exports = logger; diff --git a/server/util/port.js b/server/util/port.js new file mode 100755 index 0000000..a5f1179 --- /dev/null +++ b/server/util/port.js @@ -0,0 +1,3 @@ +const argv = require('./argv'); + +module.exports = parseInt(argv.port || process.env.PORT || '3000', 10); diff --git a/src/actions/apples.js b/src/actions/apples.js new file mode 100644 index 0000000..ca7adb5 --- /dev/null +++ b/src/actions/apples.js @@ -0,0 +1,19 @@ +import { DROP_APPLES, LOAD_APPLES, SHAKE_APPLES } from '../constants/apples'; + +export function dropApples() { + return { + type: DROP_APPLES + }; +} + +export function loadApples() { + return { + type: LOAD_APPLES + }; +} + +export function shakeApples() { + return { + type: SHAKE_APPLES + }; +} diff --git a/src/actions/basket.js b/src/actions/basket.js new file mode 100644 index 0000000..b51f6e6 --- /dev/null +++ b/src/actions/basket.js @@ -0,0 +1,7 @@ +import { ADD_APPLES } from '../constants/basket'; + +export function addBasket() { + return { + type: ADD_APPLES + }; +} diff --git a/src/actions/tree.js b/src/actions/tree.js new file mode 100644 index 0000000..116e243 --- /dev/null +++ b/src/actions/tree.js @@ -0,0 +1,7 @@ +import { SHAKE_TREE } from '../constants/tree'; + +export function shakeTree() { + return { + type: SHAKE_TREE + }; +} diff --git a/src/app.scss b/src/app.scss new file mode 100644 index 0000000..fe65689 --- /dev/null +++ b/src/app.scss @@ -0,0 +1,52 @@ +html, +body { + margin: 0; + padding: 0; + overflow: hidden; +} + +// translate3d +@mixin translate3d($x, $y, $z) { + -webkit-transform: translate3d($x, $y, $z); + -moz-transform: translate3d($x, $y, $z); + -o-transform: translate3d($x, $y, $z); + transform: translate3d($x, $y, $z); +} + +// transform +@mixin transform($transforms) { + -moz-transform: $transforms; + -o-transform: $transforms; + -ms-transform: $transforms; + -webkit-transform: $transforms; + transform: $transforms; +} + +// rotate +@mixin rotate ($deg) { + @include transform(rotate(#{$deg}deg)); +} + +// scale +@mixin scale($scale) { + @include transform(scale($scale)); +} + +// translate +@mixin translate ($x, $y) { + @include transform(translate($x, $y)); +} + +// skew +@mixin skew ($x, $y) { + @include transform(skew(#{$x}deg, #{$y}deg)); +} + +//transform origin +@mixin transform-origin ($origin) { + moz-transform-origin: $origin; + -o-transform-origin: $origin; + -ms-transform-origin: $origin; + -webkit-transform-origin: $origin; + transform-origin: $origin; +} \ No newline at end of file diff --git a/src/apples.json b/src/apples.json new file mode 100644 index 0000000..30d1b1c --- /dev/null +++ b/src/apples.json @@ -0,0 +1,52 @@ +[ + { + "x": 0, + "y1": 186, + "y2": 430 + }, + { + "x": 67, + "y1": 124, + "y2":450 + }, + { + "x": 107, + "y1": 175, + "y2": 430 + }, + { + "x": 200, + "y1": 124, + "y2": 420 + }, + { + "x": 220, + "y1": 0, + "y2": 430 + }, + { + "x": 274, + "y1": 218, + "y2": 440 + }, + { + "x": 306, + "y1": 9, + "y2": 430 + }, + { + "x": 328, + "y1": 175, + "y2": 450 + }, + { + "x": 393, + "y1": 255, + "y2": 460 + }, + { + "x": 442, + "y1": 174, + "y2": 490 + } +] \ No newline at end of file diff --git a/src/components/Apple/index.js b/src/components/Apple/index.js new file mode 100755 index 0000000..aa30a53 --- /dev/null +++ b/src/components/Apple/index.js @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DURATION_DROPPING_APPLE } from '../../config'; +import './style.scss'; + +function Apple({ x, y, index }) { + return ( + + + + + ); +} + +Apple.propTypes = { + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + index: PropTypes.number.isRequired +}; + +export default Apple; diff --git a/src/components/Apple/style.scss b/src/components/Apple/style.scss new file mode 100644 index 0000000..a3952bd --- /dev/null +++ b/src/components/Apple/style.scss @@ -0,0 +1,42 @@ +@import '../../app.scss'; + + +.apple-out { + visibility: hidden; + opacity: 0; + transition-property: visibility, opacity; + transition-duration: 0s, 1.0s; + transition-timing-function: ease, linear; + transition-delay: 2.5s; +} + +.shake-apples { + animation: shake-apples .5s; + transform: translate3d(200px, 174px, 0); + backface-visibility: hidden; + animation-iteration-count: 6; +} + + +.drop-apple { + transition-timing-function: linear; +} + + +@keyframes apple-out { + from { + opacity: 1 + } + + to { + opacity: 0; + } +} + + +@keyframes shake-apples { + 40%, + 60% { + @include translate3d(204px, 174px, 0); + } +} \ No newline at end of file diff --git a/src/components/Basket/index.js b/src/components/Basket/index.js new file mode 100755 index 0000000..783721e --- /dev/null +++ b/src/components/Basket/index.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './style.scss'; + +function Basket({ isAdded }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +} + +Basket.defaultProps = { + isAdded: false +}; + +Basket.propTypes = { + isAdded: PropTypes.bool +}; + +export default Basket; diff --git a/src/components/Basket/style.scss b/src/components/Basket/style.scss new file mode 100644 index 0000000..14522e9 --- /dev/null +++ b/src/components/Basket/style.scss @@ -0,0 +1,11 @@ +.basket-apple { + visibility: hidden; + opacity: 0; + transition-property: visibility, opacity; + transition-duration: .5s, 1.5s; + transition-timing-function: ease, linear; + &.added { + visibility: visible; + opacity: 1; + } +} diff --git a/src/components/Button/index.js b/src/components/Button/index.js new file mode 100755 index 0000000..f21f43b --- /dev/null +++ b/src/components/Button/index.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './style.scss'; + +function Button({ clickShake, clicked }) { + return ( + // eslint-disable-next-line react/button-has-type + + ); +} + +Button.propTypes = { + clickShake: PropTypes.func.isRequired, + clicked: PropTypes.bool.isRequired +}; + +export default Button; diff --git a/src/components/Button/style.scss b/src/components/Button/style.scss new file mode 100644 index 0000000..da70d84 --- /dev/null +++ b/src/components/Button/style.scss @@ -0,0 +1,54 @@ +@import '../../app.scss'; + +$shake-color: #a77600; + +.shake-button { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 2; + display: block; + width: 70px; + height: 70px; + font-size: 1.1em; + text-transform: uppercase; + text-align: center; + line-height: 70px; + letter-spacing: -1px; + color: #f6f0bf; + border: none; + border-radius: 50%; + box-shadow: 0 0 0 0 rgba($shake-color, .5); + background: $shake-color; + outline: none; + cursor: pointer; + -webkit-animation: pulse 1.5s infinite; + animation: pulse 1.5s infinite; + + &:hover { + -webkit-animation: none; + animation: none; + } + + &.clicked { + -webkit-animation: none; + animation: none; + } +} + + +@keyframes pulse { + 0% { + @include transform(scale(.9)); + } + + 70% { + @include transform(scale(1)); + box-shadow: 0 0 0 50px rgba($shake-color, 0); + } + + 100% { + @include transform(scale(.9)); + box-shadow: 0 0 0 0 rgba($shake-color, 0); + } +} diff --git a/src/components/Landscape/index.js b/src/components/Landscape/index.js new file mode 100644 index 0000000..9da034c --- /dev/null +++ b/src/components/Landscape/index.js @@ -0,0 +1,3337 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function Landscape({ children }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {children} + + + ); +} +Landscape.propTypes = { + children: PropTypes.node.isRequired +}; + +export default Landscape; diff --git a/src/components/Tree/index.js b/src/components/Tree/index.js new file mode 100755 index 0000000..9c10414 --- /dev/null +++ b/src/components/Tree/index.js @@ -0,0 +1,567 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './style.scss'; + +function Tree({ isShaked }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +Tree.defaultProps = { + isShaked: false +}; + +Tree.propTypes = { + isShaked: PropTypes.bool +}; + +export default Tree; diff --git a/src/components/Tree/style.scss b/src/components/Tree/style.scss new file mode 100644 index 0000000..9f57338 --- /dev/null +++ b/src/components/Tree/style.scss @@ -0,0 +1,32 @@ +@import '../../app.scss'; + +.shake-tree { + animation: shake 1s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; + @include translate3d(0, 49px, 0); + backface-visibility: hidden; + animation-iteration-count: 3; +} + +@keyframes shake { + + 10%, + 90% { + @include translate3d(-1px, 49px, 0); + } + + 20%, + 80% { + @include translate3d(2px, 49px, 0); + } + + 30%, + 50%, + 70% { + @include translate3d(-4px, 49px, 0); + } + + 40%, + 60% { + @include translate3d(4px, 49px, 0); + } +} \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..634f0f7 --- /dev/null +++ b/src/config.js @@ -0,0 +1,5 @@ +export const DURATION_TREE_SHAKE = 3000; // duration to shake the tree +export const DURATION_DROPPING_APPLE = 2000; // Random maximum fall time +export const DURATION_DROPPED_APPLE = + DURATION_TREE_SHAKE + DURATION_DROPPING_APPLE; // all apples fall time +export const DURATION_ADD_BASKET = DURATION_DROPPED_APPLE + 1000; // stopping time of apples diff --git a/src/configureStore.js b/src/configureStore.js new file mode 100644 index 0000000..401cebb --- /dev/null +++ b/src/configureStore.js @@ -0,0 +1,28 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import { fromJS } from 'immutable'; +import createReducer from './reducers'; + +export default function configureStore(initialState = {}) { + const middlewares = []; + const enhancers = [applyMiddleware(...middlewares)]; + + /* eslint-disable no-underscore-dangle, indent */ + // for redux devtools + const composeEnhancers = + process.env.NODE_ENV !== 'production' && + typeof window === 'object' && + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ + ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ + shouldHotReload: false + }) + : compose; + /* eslint-enable */ + + const store = createStore( + createReducer(), + fromJS(initialState), + composeEnhancers(...enhancers) + ); + + return store; +} diff --git a/src/constants/apples.js b/src/constants/apples.js new file mode 100644 index 0000000..dd96ca6 --- /dev/null +++ b/src/constants/apples.js @@ -0,0 +1,3 @@ +export const DROP_APPLES = 'container/apples/DROP_APPLES'; +export const LOAD_APPLES = 'container/apples/LOAD_APPLES'; +export const SHAKE_APPLES = 'container/apples/SHAKE_APPLES'; diff --git a/src/constants/basket.js b/src/constants/basket.js new file mode 100644 index 0000000..1b2002a --- /dev/null +++ b/src/constants/basket.js @@ -0,0 +1 @@ +export const ADD_APPLES = 'container/basket/ADD_APPLES'; diff --git a/src/constants/tree.js b/src/constants/tree.js new file mode 100644 index 0000000..3366fb8 --- /dev/null +++ b/src/constants/tree.js @@ -0,0 +1 @@ +export const SHAKE_TREE = 'container/tree/SHAKE_TREE'; diff --git a/src/containers/App/index.js b/src/containers/App/index.js new file mode 100755 index 0000000..3d13680 --- /dev/null +++ b/src/containers/App/index.js @@ -0,0 +1,21 @@ +import React from 'react'; +import Landscape from '../../components/Landscape'; +import TreeContainer from '../Tree'; +import BasketContainer from '../Basket'; +import ApplesContainer from '../Apples'; +import ShakeButtonContainer from '../ShakeButton'; + +function App() { + return ( + + + + + + + + + ); +} + +export default App; diff --git a/src/containers/Apples/index.js b/src/containers/Apples/index.js new file mode 100644 index 0000000..159f245 --- /dev/null +++ b/src/containers/Apples/index.js @@ -0,0 +1,65 @@ +/* eslint-disable react/no-array-index-key */ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import * as ApplesActions from '../../actions/apples'; + +import Apple from '../../components/Apple'; + +class ApplesContainer extends PureComponent { + componentDidMount() { + this.props.actions.loadApples(); + } + + render() { + const { apples, isDropped, isShaked } = this.props; + return ( + + {apples.map((apple, index) => ( + + ))} + + ); + } +} + +ApplesContainer.propTypes = { + actions: PropTypes.object.isRequired, + apples: PropTypes.array.isRequired, + isDropped: PropTypes.bool.isRequired, + isShaked: PropTypes.bool.isRequired +}; + +function mapStateToProps(state) { + return { + apples: state + .get('applesReducer') + .get('apples') + .toArray(), + isDropped: state.get('applesReducer').get('isDropped'), + isShaked: state.get('applesReducer').get('isShaked') + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators(ApplesActions, dispatch) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ApplesContainer); diff --git a/src/containers/Basket/index.js b/src/containers/Basket/index.js new file mode 100644 index 0000000..73a1e96 --- /dev/null +++ b/src/containers/Basket/index.js @@ -0,0 +1,34 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import * as BasketActions from '../../actions/basket'; + +import Basket from '../../components/Basket'; +class BasketContainer extends PureComponent { + render() { + const { isAdded } = this.props; + return ; + } +} + +BasketContainer.propTypes = { + isAdded: PropTypes.bool.isRequired +}; + +function mapStateToProps(state) { + return { + isAdded: state.get('basketReducer').get('isAdded') + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators(BasketActions, dispatch) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(BasketContainer); diff --git a/src/containers/ShakeButton/index.js b/src/containers/ShakeButton/index.js new file mode 100644 index 0000000..1754534 --- /dev/null +++ b/src/containers/ShakeButton/index.js @@ -0,0 +1,56 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import Button from '../../components/Button'; +import { dropApples, loadApples, shakeApples } from '../../actions/apples'; +import { addBasket } from '../../actions/basket'; +import { shakeTree } from '../../actions/tree'; +import { DURATION_TREE_SHAKE, DURATION_ADD_BASKET } from '../../config'; + +class ShareButtonContainer extends Component { + constructor(props) { + super(props); + this.state = { + clicked: false + }; + } + + handleClick = () => { + if (this.state.clicked) return; + + const { actions } = this.props; + this.setState({ clicked: true }); + actions.shakeTree(); + actions.shakeApples(); + setTimeout(() => actions.dropApples(), DURATION_TREE_SHAKE); + setTimeout(() => actions.addBasket(), DURATION_ADD_BASKET); + }; + + render() { + const { clicked } = this.state; + return