diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..0ebb282c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+package.lock.json
+yarn.lock
+yarn-error.log
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 00000000..36222370
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,13 @@
+const presets = [
+ [
+ "@babel/preset-env",
+ {
+ useBuiltIns: "usage"
+ }
+ ],
+ "@babel/preset-react"
+];
+
+const plugins = ["@babel/plugin-proposal-class-properties"];
+
+module.exports = { presets, plugins };
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..e6db802a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "reactjs-card-challenge",
+ "version": "1.0.0",
+ "description": "A responsive card based design, to be implemented by React & Redux",
+ "main": "index.js",
+ "scripts": {
+ "dev": "webpack-dev-server --config webpack.dev.js",
+ "build": "webpack --config webpack.prod.js",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Am-Ta/reactjs-card-challenge.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/Am-Ta/reactjs-card-challenge/issues"
+ },
+ "homepage": "https://github.com/Am-Ta/reactjs-card-challenge#readme",
+ "devDependencies": {
+ "@babel/core": "^7.6.4",
+ "@babel/plugin-proposal-class-properties": "^7.5.5",
+ "@babel/preset-env": "^7.6.3",
+ "@babel/preset-react": "^7.6.3",
+ "babel-loader": "^8.0.6",
+ "clean-webpack-plugin": "^3.0.0",
+ "css-loader": "^3.2.0",
+ "file-loader": "^4.2.0",
+ "html-webpack-plugin": "^3.2.0",
+ "mini-css-extract-plugin": "^0.8.0",
+ "node-sass": "^4.13.0",
+ "sass-loader": "^8.0.0",
+ "style-loader": "^1.0.0",
+ "webpack": "^4.41.2",
+ "webpack-cli": "^3.3.9",
+ "webpack-dev-server": "^3.9.0"
+ },
+ "dependencies": {
+ "@babel/polyfill": "^7.6.0",
+ "prop-types": "^15.7.2",
+ "react": "^16.11.0",
+ "react-dom": "^16.11.0",
+ "react-redux": "^7.1.1",
+ "redux": "^4.0.4",
+ "redux-devtools-extension": "^2.13.8",
+ "redux-thunk": "^2.3.0"
+ }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 00000000..5f91cf6e
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ <%= htmlWebpackPlugin.options.title %>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..affe2c2b
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,38 @@
+import React, { useEffect } from "react";
+import PropTypes from "prop-types";
+
+import BtnRandGen from "./component/BtnRandGen";
+import CardBox from "./component/card/CardBox";
+
+import { connect } from "react-redux";
+import { fetchCards } from "./action/CardAction";
+
+import "./App.scss";
+
+const App = ({ cardRes: { card }, fetchCards }) => {
+ // Fetch the cards
+ useEffect(() => {
+ fetchCards();
+ }, []);
+
+ return (
+
+
+ {card && }
+
+ );
+};
+
+App.propTypes = {
+ cardRes: PropTypes.shape({ card: PropTypes.object }),
+ fetchCards: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => ({
+ cardRes: state.cardRes
+});
+
+export default connect(
+ mapStateToProps,
+ { fetchCards }
+)(App);
diff --git a/src/App.scss b/src/App.scss
new file mode 100644
index 00000000..440065cf
--- /dev/null
+++ b/src/App.scss
@@ -0,0 +1,239 @@
+// Simple css reset
+* {
+ margin: 0;
+ padding: 0;
+}
+
+// Define font
+@font-face {
+ font-family: "JosefinSans";
+ src: url("./assets/font/JosefinSans-Regular.ttf") format("truetype");
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "JosefinSans";
+ src: url("./assets/font/JosefinSans-Bold.ttf") format("truetype");
+ font-weight: 700;
+ font-style: normal;
+}
+
+// Fix the from element font
+input,
+button,
+textarea {
+ font-family: inherit;
+ font-size: inherit;
+ outline: none;
+}
+
+// Fix the clear
+html {
+ box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+ -webkit-box-sizing: inherit;
+ -moz-box-sizing: inherit;
+}
+
+// Variables
+$primary-color: #00a4e4;
+$success-color: #00c300;
+$danger-color: #ec1c24;
+$dark-color: #231f20;
+$light-color: #d7d7d8;
+$danger-color: #ff3d00;
+$font-stack: "JosefinSans", Arial, Helvetica, sans-serif;
+
+// Set the global font and color for whole app
+body {
+ font-family: $font-stack;
+ color: $dark-color;
+}
+
+// Container
+.container {
+ width: 90%;
+ min-height: 70vh;
+ margin: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ align-items: center;
+}
+
+// Btn Component
+.btn {
+ background-color: transparent;
+ border: none;
+ text-decoration: none;
+ cursor: pointer;
+ padding: 0.5em 1em;
+ border-radius: 3px;
+ transition: 0.5s;
+ &.btn_success {
+ background-color: $success-color;
+ color: #fff;
+ &:hover {
+ background-color: rgba($success-color, 0.8);
+ }
+ }
+ &.btn_primary {
+ background-color: $primary-color;
+ color: #fff;
+ &:hover {
+ background-color: rgba($primary-color, 0.8);
+ }
+ }
+ &.btn_dark {
+ background-color: $dark-color;
+ color: #fff;
+ &:hover {
+ background-color: rgba($dark-color, 0.9);
+ }
+ }
+ &.btn_block {
+ width: 100%;
+ }
+}
+
+// Card-box is wrapper for card item and card updateor
+.card-box {
+ width: 200px;
+}
+
+// Card Item Component
+.card {
+ text-align: center;
+ .card__img-box {
+ width: 100%;
+ .card__img {
+ display: block;
+ width: 100%;
+ }
+ }
+ &.card_show {
+ animation: shake 0.5s ease infinite;
+ -webkit-animation: shake 0.5s ease infinite;
+ -moz-animation: shake 0.5s ease infinite;
+ }
+
+ .card__info {
+ margin: 1em;
+ .card__title {
+ padding: 0.5em;
+ display: flex;
+ justify-content: space-around;
+
+ &.card_sport {
+ color: $success-color;
+ }
+ &.card_fun {
+ color: $primary-color;
+ }
+ &.card_art {
+ color: $danger-color;
+ }
+ }
+ }
+}
+
+// Form Component
+.form {
+ .form__item {
+ margin-bottom: 1em;
+ .form__label {
+ color: $success-color;
+ display: block;
+ padding: 0.25em 0;
+ }
+ .form__input {
+ width: 100%;
+ padding: 0.5em;
+ resize: none;
+ color: rgba($dark-color, 0.8);
+ border: 1px solid #aaa;
+ &:hover,
+ &:focus {
+ border-color: $success-color;
+ }
+ }
+ textarea.form__input {
+ height: 100px;
+ }
+ }
+}
+
+// Animation for cards with code equal to 1
+@keyframes shake {
+ 0% {
+ transform: translate(1px, 1px) rotate(0deg);
+ -webkit-transform: translate(1px, 1px) rotate(0deg);
+ -moz-transform: translate(1px, 1px) rotate(0deg);
+ }
+ 10% {
+ transform: translate(-1px, -2px) rotate(-1deg);
+ -webkit-transform: translate(-1px, -2px) rotate(-1deg);
+ -moz-transform: translate(-1px, -2px) rotate(-1deg);
+ }
+ 20% {
+ transform: translate(-3px, 0px) rotate(1deg);
+ -webkit-transform: translate(-3px, 0px) rotate(1deg);
+ -moz-transform: translate(-3px, 0px) rotate(1deg);
+ }
+ 30% {
+ transform: translate(3px, 2px) rotate(0deg);
+ -webkit-transform: translate(3px, 2px) rotate(0deg);
+ -moz-transform: translate(3px, 2px) rotate(0deg);
+ }
+ 40% {
+ transform: translate(1px, -1px) rotate(1deg);
+ -webkit-transform: translate(1px, -1px) rotate(1deg);
+ -moz-transform: translate(1px, -1px) rotate(1deg);
+ }
+ 50% {
+ transform: translate(-1px, 2px) rotate(-1deg);
+ -webkit-transform: translate(-1px, 2px) rotate(-1deg);
+ -moz-transform: translate(-1px, 2px) rotate(-1deg);
+ }
+ 60% {
+ transform: translate(-3px, 1px) rotate(0deg);
+ -webkit-transform: translate(-3px, 1px) rotate(0deg);
+ -moz-transform: translate(-3px, 1px) rotate(0deg);
+ }
+ 70% {
+ transform: translate(3px, 1px) rotate(-1deg);
+ -webkit-transform: translate(3px, 1px) rotate(-1deg);
+ -moz-transform: translate(3px, 1px) rotate(-1deg);
+ }
+ 80% {
+ transform: translate(-1px, -1px) rotate(1deg);
+ -webkit-transform: translate(-1px, -1px) rotate(1deg);
+ -moz-transform: translate(-1px, -1px) rotate(1deg);
+ }
+ 90% {
+ transform: translate(1px, 2px) rotate(0deg);
+ -webkit-transform: translate(1px, 2px) rotate(0deg);
+ -moz-transform: translate(1px, 2px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(1px, -2px) rotate(-1deg);
+ -webkit-transform: translate(1px, -2px) rotate(-1deg);
+ -moz-transform: translate(1px, -2px) rotate(-1deg);
+ }
+}
+
+// Screen min-width 40em
+@media screen and (min-width: 40em) {
+ .container {
+ width: 80%;
+ flex-direction: row;
+ }
+}
diff --git a/src/action/CardAction.js b/src/action/CardAction.js
new file mode 100644
index 00000000..2773b6ec
--- /dev/null
+++ b/src/action/CardAction.js
@@ -0,0 +1,44 @@
+import {
+ CARD_ERROR,
+ FETCH_CARDS,
+ UPDATE_CARD,
+ SELECT_CARD,
+ SET_CURRENT
+} from "./types";
+
+// Fetch the cards from the pushe api
+export const fetchCards = () => async dispatch => {
+ try {
+ const res = await fetch("http://static.pushe.co/challenge/json");
+ const data = await res.json();
+
+ dispatch({
+ type: FETCH_CARDS,
+ payload: data.cards
+ });
+ } catch (err) {
+ dispatch({
+ type: CARD_ERROR,
+ payload: err.response.data
+ });
+ }
+};
+
+// Select the card by random
+export const selectCard = () => {
+ let randomNum = Math.random();
+ return {
+ type: SELECT_CARD,
+ payload: randomNum
+ };
+};
+
+export const setCurrent = card => ({
+ type: SET_CURRENT,
+ payload: card
+});
+
+export const updateCard = (title, description) => ({
+ type: UPDATE_CARD,
+ payload: { title, description }
+});
diff --git a/src/action/types.js b/src/action/types.js
new file mode 100644
index 00000000..60272fc7
--- /dev/null
+++ b/src/action/types.js
@@ -0,0 +1,5 @@
+export const FETCH_CARDS = "fetchCards";
+export const CARD_ERROR = "cardError";
+export const SELECT_CARD = "selectCard";
+export const UPDATE_CARD = "updateCard";
+export const SET_CURRENT = "setCurrent";
diff --git a/src/assets/font/JosefinSans-Bold.ttf b/src/assets/font/JosefinSans-Bold.ttf
new file mode 100644
index 00000000..f4166a1e
Binary files /dev/null and b/src/assets/font/JosefinSans-Bold.ttf differ
diff --git a/src/assets/font/JosefinSans-Regular.ttf b/src/assets/font/JosefinSans-Regular.ttf
new file mode 100644
index 00000000..292dcb07
Binary files /dev/null and b/src/assets/font/JosefinSans-Regular.ttf differ
diff --git a/src/component/BtnRandGen.js b/src/component/BtnRandGen.js
new file mode 100644
index 00000000..35c7cbb1
--- /dev/null
+++ b/src/component/BtnRandGen.js
@@ -0,0 +1,34 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+import { selectCard } from "./../action/CardAction";
+
+const BtnRandGen = ({ cardRes: { current }, selectCard }) => {
+ // Handle the click for generate random card
+ const handleClick = () => {
+ !current && selectCard();
+ };
+
+ return (
+
+
+
+ );
+};
+
+BtnRandGen.propTypes = {
+ cardRes: PropTypes.shape({ current: PropTypes.object }),
+ selectCard: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => ({
+ cardRes: state.cardRes
+});
+
+export default connect(
+ mapStateToProps,
+ { selectCard }
+)(BtnRandGen);
diff --git a/src/component/card/CardBox.js b/src/component/card/CardBox.js
new file mode 100644
index 00000000..3ecc3347
--- /dev/null
+++ b/src/component/card/CardBox.js
@@ -0,0 +1,29 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+import CardItem from "./CardItem";
+import CardEdit from "./CardEdit";
+
+import { connect } from "react-redux";
+
+const CardBox = ({ cardRes: { current } }) => {
+ return (
+
+ {!current && }
+ {current && }
+
+ );
+};
+
+CardBox.propTypes = {
+ cardRes: PropTypes.shape({ current: PropTypes.object })
+};
+
+const mapStateToProps = state => ({
+ cardRes: state.cardRes
+});
+
+export default connect(
+ mapStateToProps,
+ {}
+)(CardBox);
diff --git a/src/component/card/CardEdit.js b/src/component/card/CardEdit.js
new file mode 100644
index 00000000..a8248e4a
--- /dev/null
+++ b/src/component/card/CardEdit.js
@@ -0,0 +1,73 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+import { updateCard } from "../../action/CardAction";
+
+const CardEdit = ({ cardRes: { current }, updateCard }) => {
+ const { title, description } = current;
+ const [form, setForm] = useState({ title: "", description: "" });
+
+ // Load the current title and description of card for the form state
+ useEffect(() => {
+ setForm({ title, description });
+ }, []);
+
+ // Handle the changes on the title and description of the card
+ const handleChange = e => {
+ setForm({ ...form, [e.target.name]: e.target.value });
+ };
+
+ // Handle the submit for new card
+ const handleSubmit = e => {
+ e.preventDefault();
+
+ updateCard(form.title, form.description);
+ };
+
+ return (
+
+ );
+};
+
+CardEdit.propTypes = {
+ cardRes: PropTypes.shape({ current: PropTypes.object }),
+ updateCard: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => ({
+ cardRes: state.cardRes
+});
+
+export default connect(
+ mapStateToProps,
+ { updateCard }
+)(CardEdit);
diff --git a/src/component/card/CardItem.js b/src/component/card/CardItem.js
new file mode 100644
index 00000000..04b591ae
--- /dev/null
+++ b/src/component/card/CardItem.js
@@ -0,0 +1,75 @@
+import React, { useEffect } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+import { setCurrent } from "../../action/CardAction";
+
+const CardItem = ({ cardRes: { card }, setCurrent }) => {
+ // Select icon relative than the card tag
+ const getIcon = () => {
+ switch (card.tag) {
+ case "sport":
+ return ;
+ case "art":
+ return ;
+ case "fun":
+ return ;
+ }
+ };
+
+ // To update the card
+ const handleClick = () => {
+ setCurrent(card);
+ };
+
+ return (
+ // The code to equal is 1. so the animation is runing
+
+ {/* The code to equal is 0. so the image is load */}
+ {card.code === 0 && (
+
+

+
+ )}
+
+ {/* The code to equal is 2. so the audio is play */}
+ {card.code === 2 && (
+
+ )}
+
+
+
+ {getIcon()}
+ {card.title}
+
+
{card.description}
+
+
+
+
+
+
+ );
+};
+
+CardItem.propTypes = {
+ cardRes: PropTypes.shape({ card: PropTypes.object }),
+ setCurrent: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => ({
+ cardRes: state.cardRes
+});
+
+export default connect(
+ mapStateToProps,
+ { setCurrent }
+)(CardItem);
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 00000000..9a85450a
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,12 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
+import { Provider } from "react-redux";
+import store from "./store";
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById("root")
+);
diff --git a/src/reducer/CardReducer.js b/src/reducer/CardReducer.js
new file mode 100644
index 00000000..cae93325
--- /dev/null
+++ b/src/reducer/CardReducer.js
@@ -0,0 +1,62 @@
+import {
+ CARD_ERROR,
+ FETCH_CARDS,
+ UPDATE_CARD,
+ SELECT_CARD,
+ SET_CURRENT
+} from "../action/types";
+
+const initialState = {
+ cards: [],
+ card: null,
+ index: -1,
+ current: null,
+ error: null
+};
+
+export default (state = initialState, action) => {
+ switch (action.type) {
+ case FETCH_CARDS:
+ return {
+ ...state,
+ cards: action.payload
+ };
+ case SELECT_CARD:
+ // For select random number
+ const randomNum = Math.round(
+ action.payload * (state.cards.length - 1)
+ );
+ console.log(randomNum);
+ return {
+ ...state,
+ card: state.cards[randomNum],
+ index: randomNum
+ };
+ case SET_CURRENT:
+ return {
+ ...state,
+ current: action.payload
+ };
+ case UPDATE_CARD:
+ // Create new card by the title and description that updated
+ const newCard = {
+ ...state.card,
+ ...action.payload
+ };
+ return {
+ ...state,
+ card: newCard,
+ current: null,
+ cards: state.cards.map((card, i) => {
+ return i === state.index ? newCard : card;
+ })
+ };
+ case CARD_ERROR:
+ return {
+ ...state,
+ error: action.payload
+ };
+ default:
+ return state;
+ }
+};
diff --git a/src/reducer/index.js b/src/reducer/index.js
new file mode 100644
index 00000000..13001c5c
--- /dev/null
+++ b/src/reducer/index.js
@@ -0,0 +1,6 @@
+import { combineReducers } from "redux";
+import CardReducer from "./CardReducer";
+
+export default combineReducers({
+ cardRes: CardReducer
+});
diff --git a/src/store.js b/src/store.js
new file mode 100644
index 00000000..8c6c5345
--- /dev/null
+++ b/src/store.js
@@ -0,0 +1,15 @@
+import { createStore, applyMiddleware } from "redux";
+import rootReducer from "./reducer";
+import { composeWithDevTools } from "redux-devtools-extension";
+import thunk from "redux-thunk";
+
+const middleware = [thunk];
+const initialState = {};
+
+const store = createStore(
+ rootReducer,
+ initialState,
+ composeWithDevTools(applyMiddleware(...middleware))
+);
+
+export default store;
diff --git a/webpack.dev.js b/webpack.dev.js
new file mode 100644
index 00000000..4c5903a9
--- /dev/null
+++ b/webpack.dev.js
@@ -0,0 +1,65 @@
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+
+module.exports = {
+ entry: "./src/index.js",
+ output: {
+ filename: "bundle.js",
+ path: path.join(__dirname, "dist"),
+ publicPath: "/"
+ },
+ mode: "development",
+ devServer: {
+ hot: true,
+ port: 3000,
+ contentBase: path.join(__dirname, "public"),
+ publicPath: "/",
+ historyApiFallback: true
+ },
+ module: {
+ rules: [
+ {
+ test: /\.scss$/,
+ use: ["style-loader", "css-loader", "sass-loader"]
+ },
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: "babel-loader"
+ },
+ {
+ test: /\.(ttf|woff|woff2)$/,
+ use: {
+ loader: "file-loader",
+ options: {
+ publicPath: "./assets/font",
+ outputPath: "./assets/font"
+ }
+ }
+ },
+ {
+ test: /\.(png|jpg|jpeg|gif)$/,
+ use: {
+ loader: "file-loader",
+ options: {
+ publicPath: "./assets/img",
+ outputPath: "./assets/img"
+ }
+ }
+ }
+ ]
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: "React Card Challenge",
+ meta: {
+ description:
+ "A responsive card based design, to be implemented by React/Redux",
+ author: "Amin Taghipour ",
+ "application-name": "HTML/CSS Challenge"
+ },
+ filename: "index.html",
+ template: "./public/index.html"
+ })
+ ]
+};
diff --git a/webpack.prod.js b/webpack.prod.js
new file mode 100644
index 00000000..3b03b1f1
--- /dev/null
+++ b/webpack.prod.js
@@ -0,0 +1,76 @@
+const path = require("path");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const { CleanWebpackPlugin } = require("clean-webpack-plugin");
+
+module.exports = {
+ entry: "./src/index.js",
+ output: {
+ filename: "bundle.[contenthash].js",
+ path: path.join(__dirname, "dist"),
+ publicPath: "./"
+ },
+ mode: "production",
+ module: {
+ rules: [
+ // handle the scss file in js
+ {
+ test: /\.scss$/,
+ loader: [
+ MiniCssExtractPlugin.loader,
+ "css-loader",
+ "sass-loader"
+ ]
+ },
+ // handle the ES6 js files
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: "babel-loader"
+ },
+ // hanlde the assets files
+ {
+ test: /\.(ttf|woff|woff2)$/,
+ use: {
+ loader: "file-loader",
+ options: {
+ publicPath: "./assets/font",
+ outputPath: "./assets/font"
+ }
+ }
+ },
+ {
+ test: /\.(png|jpg|jpeg|gif)$/,
+ use: {
+ loader: "file-loader",
+ options: {
+ publicPath: "./assets/img",
+ outputPath: "./assets/img"
+ }
+ }
+ }
+ ]
+ },
+ plugins: [
+ // Clean dist folder for each rebuild
+ new CleanWebpackPlugin(),
+
+ // Create the separate css file
+ new MiniCssExtractPlugin({
+ filename: "styles.[contenthash].css"
+ }),
+
+ // Config the html file
+ new HtmlWebpackPlugin({
+ title: "React Card Challenge",
+ meta: {
+ description:
+ "A responsive card based design, to be implemented by React & Redux",
+ author: "Amin Taghipour ",
+ "application-name": "HTML/CSS Challenge"
+ },
+ filename: "index.html",
+ template: "./public/index.html"
+ })
+ ]
+};