From a8b8378419da8f98313ab0a7bee10ada3b93e88f Mon Sep 17 00:00:00 2001 From: James Gareth Smith Date: Thu, 14 Mar 2019 20:58:19 +0200 Subject: [PATCH] feat: unskinned app (#3) --- .eslintrc.json | 7 +- package-lock.json | 40 +- package.json | 6 +- src/common/.gitkeep | 0 src/common/components/DappList/DappList.jsx | 14 + src/common/components/DappList/index.js | 3 + .../components/DappListItem/DappListItem.jsx | 11 + src/common/components/DappListItem/index.js | 3 + src/common/data/categories.js | 6 + src/common/data/dapps.js | 369 ++++++++++++++++++ src/common/redux/reducers.js | 11 + src/common/redux/store.js | 19 + src/common/utils/categories.js | 7 + src/common/utils/categories.test.js | 32 ++ src/common/utils/humanise.js | 7 + src/common/utils/humanise.test.js | 11 + src/common/utils/models.js | 12 + src/common/utils/reducer.js | 11 + src/common/utils/reducer.test.js | 50 +++ src/index.jsx | 14 +- src/modules/App/Router.jsx | 15 + src/modules/App/index.js | 3 + src/modules/App/index.jsx | 3 - .../Categories/Categories.container.js | 16 + src/modules/Categories/Categories.jsx | 28 ++ src/modules/Categories/index.js | 3 + .../CategorySelecter.container.js | 13 + .../CategorySelector/CategorySelector.jsx | 30 ++ .../CategorySelector.reducer.js | 18 + src/modules/CategorySelector/index.js | 3 + src/modules/Dapps/Dapps.container.js | 9 + src/modules/Dapps/Dapps.jsx | 29 ++ src/modules/Dapps/Dapps.reducer.js | 3 + src/modules/Dapps/Dapps.selector.js | 24 ++ src/modules/Dapps/Dapps.selector.test.js | 46 +++ src/modules/Dapps/index.js | 3 + src/modules/Filtered/Filtered.container.js | 9 + src/modules/Filtered/Filtered.jsx | 24 ++ src/modules/Filtered/Filtered.selector.js | 10 + .../Filtered/Filtered.selector.test.js | 38 ++ src/modules/Filtered/index.js | 3 + src/modules/Home/Home.container.js | 6 + src/modules/Home/Home.jsx | 11 + src/modules/Home/index.js | 3 + .../RecentlyAdded/RecentlyAdded.container.js | 9 + src/modules/RecentlyAdded/RecentlyAdded.jsx | 22 ++ .../RecentlyAdded/RecentlyAdded.selector.js | 11 + .../RecentlyAdded.selector.test.js | 37 ++ src/modules/RecentlyAdded/index.js | 3 + 49 files changed, 1055 insertions(+), 10 deletions(-) delete mode 100644 src/common/.gitkeep create mode 100644 src/common/components/DappList/DappList.jsx create mode 100644 src/common/components/DappList/index.js create mode 100644 src/common/components/DappListItem/DappListItem.jsx create mode 100644 src/common/components/DappListItem/index.js create mode 100644 src/common/data/categories.js create mode 100644 src/common/data/dapps.js create mode 100644 src/common/redux/reducers.js create mode 100644 src/common/redux/store.js create mode 100644 src/common/utils/categories.js create mode 100644 src/common/utils/categories.test.js create mode 100644 src/common/utils/humanise.js create mode 100644 src/common/utils/humanise.test.js create mode 100644 src/common/utils/models.js create mode 100644 src/common/utils/reducer.js create mode 100644 src/common/utils/reducer.test.js create mode 100644 src/modules/App/Router.jsx create mode 100644 src/modules/App/index.js delete mode 100644 src/modules/App/index.jsx create mode 100644 src/modules/Categories/Categories.container.js create mode 100644 src/modules/Categories/Categories.jsx create mode 100644 src/modules/Categories/index.js create mode 100644 src/modules/CategorySelector/CategorySelecter.container.js create mode 100644 src/modules/CategorySelector/CategorySelector.jsx create mode 100644 src/modules/CategorySelector/CategorySelector.reducer.js create mode 100644 src/modules/CategorySelector/index.js create mode 100644 src/modules/Dapps/Dapps.container.js create mode 100644 src/modules/Dapps/Dapps.jsx create mode 100644 src/modules/Dapps/Dapps.reducer.js create mode 100644 src/modules/Dapps/Dapps.selector.js create mode 100644 src/modules/Dapps/Dapps.selector.test.js create mode 100644 src/modules/Dapps/index.js create mode 100644 src/modules/Filtered/Filtered.container.js create mode 100644 src/modules/Filtered/Filtered.jsx create mode 100644 src/modules/Filtered/Filtered.selector.js create mode 100644 src/modules/Filtered/Filtered.selector.test.js create mode 100644 src/modules/Filtered/index.js create mode 100644 src/modules/Home/Home.container.js create mode 100644 src/modules/Home/Home.jsx create mode 100644 src/modules/Home/index.js create mode 100644 src/modules/RecentlyAdded/RecentlyAdded.container.js create mode 100644 src/modules/RecentlyAdded/RecentlyAdded.jsx create mode 100644 src/modules/RecentlyAdded/RecentlyAdded.selector.js create mode 100644 src/modules/RecentlyAdded/RecentlyAdded.selector.test.js create mode 100644 src/modules/RecentlyAdded/index.js diff --git a/.eslintrc.json b/.eslintrc.json index df40ff8..402a1dc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,6 +5,11 @@ "prettier/prettier": "error" }, "env": { - "browser": true + "browser": true, + "es6": true, + "jest": true + }, + "parserOptions": { + "ecmaVersion": 9 } } diff --git a/package-lock.json b/package-lock.json index c1cedf5..31635f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3685,6 +3685,15 @@ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==" }, + "connected-react-router": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.3.2.tgz", + "integrity": "sha512-YxrAfMExl/OBsi+ojA4ywZeC7cmQ52MnZ4bhzqLhhjuOiXcQogC4kW0kodouXAXrXDovb2l3yEhDfpH99/wYcw==", + "requires": { + "immutable": "^3.8.1", + "seamless-immutable": "^7.1.3" + } + }, "console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", @@ -7933,6 +7942,11 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" }, + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" + }, "import-cwd": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", @@ -10285,6 +10299,11 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -13882,10 +13901,18 @@ } } }, - "react-router-redux": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/react-router-redux/-/react-router-redux-4.0.8.tgz", - "integrity": "sha1-InQDWWtRUeGCN32rg1tdRfD4BU4=" + "react-router-dom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz", + "integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==", + "requires": { + "history": "^4.7.2", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.1", + "react-router": "^4.3.1", + "warning": "^4.0.1" + } }, "react-scripts": { "version": "2.1.8", @@ -15047,6 +15074,11 @@ "ajv-keywords": "^3.1.0" } }, + "seamless-immutable": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-7.1.4.tgz", + "integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==" + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", diff --git a/package.json b/package.json index 792553e..5cedd82 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,15 @@ "version": "0.1.0", "private": true, "dependencies": { + "connected-react-router": "^6.3.2", + "history": "^4.7.2", + "moment": "^2.24.0", + "prop-types": "^15.7.2", "react": "^16.8.4", "react-dom": "^16.8.4", "react-redux": "^6.0.1", "react-router": "^4.3.1", - "react-router-redux": "^4.0.8", + "react-router-dom": "^4.3.1", "react-scripts": "2.1.8", "redux": "^4.0.1", "reselect": "^4.0.0" diff --git a/src/common/.gitkeep b/src/common/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/common/components/DappList/DappList.jsx b/src/common/components/DappList/DappList.jsx new file mode 100644 index 0000000..afc81d3 --- /dev/null +++ b/src/common/components/DappList/DappList.jsx @@ -0,0 +1,14 @@ +import React from 'react' +import { DappListModel } from '../../utils/models' +import DappListItem from '../DappListItem' + +const DappList = props => { + const { dapps } = props + return dapps && dapps.map(dapp => ) +} + +DappList.propTypes = { + dapps: DappListModel.isRequired, +} + +export default DappList diff --git a/src/common/components/DappList/index.js b/src/common/components/DappList/index.js new file mode 100644 index 0000000..c43cdb9 --- /dev/null +++ b/src/common/components/DappList/index.js @@ -0,0 +1,3 @@ +import DappList from './DappList' + +export default DappList diff --git a/src/common/components/DappListItem/DappListItem.jsx b/src/common/components/DappListItem/DappListItem.jsx new file mode 100644 index 0000000..1e84c89 --- /dev/null +++ b/src/common/components/DappListItem/DappListItem.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import { DappModel } from '../../utils/models' + +const DappListItem = props => { + const { name } = props + return
{name}
+} + +DappListItem.propTypes = DappModel + +export default DappListItem diff --git a/src/common/components/DappListItem/index.js b/src/common/components/DappListItem/index.js new file mode 100644 index 0000000..4bfc56d --- /dev/null +++ b/src/common/components/DappListItem/index.js @@ -0,0 +1,3 @@ +import DappListItem from './DappListItem' + +export default DappListItem diff --git a/src/common/data/categories.js b/src/common/data/categories.js new file mode 100644 index 0000000..b42eae1 --- /dev/null +++ b/src/common/data/categories.js @@ -0,0 +1,6 @@ +export const EXCHANGES = 'EXCHANGES' +export const MARKETPLACES = 'MARKETPLACES' +export const GAMES = 'GAMES' +export const SOCIAL_NETWORKS = 'SOCIAL_NETWORKS' +export const MEDIA = 'MEDIA' +export const SOCIAL_UTILITIES = 'SOCIAL_UTILITIES' diff --git a/src/common/data/dapps.js b/src/common/data/dapps.js new file mode 100644 index 0000000..4a75c5a --- /dev/null +++ b/src/common/data/dapps.js @@ -0,0 +1,369 @@ +import * as Categories from './categories' + +const Dapps = [ + { + name: 'Airswap', + url: 'https://instant.airswap.io/', + image: null, + description: 'Meet the future of trading.', + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'Bancor', + url: 'https://www.bancor.network/', + image: null, + description: 'Bancor is a decentralized liquidity network', + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'ERC dEX', + url: 'https://app.ercdex.com/', + description: 'Trustless trading has arrived on Ethereum', + image: null, + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'Kyber', + url: 'https://web3.kyber.network', + description: + 'On-chain, instant and liquid platform for exchange and payment service', + image: null, + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'Oasis Direct', + url: 'https://oasis.direct/', + description: 'The first decentralized instant exchange', + image: null, + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'DAI by MakerDao', + url: 'https://dai.makerdao.com', + description: 'Stability for the blockchain', + image: null, + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'LocalEthereum', + url: 'https://localethereum.com/', + description: 'The smartest way to buy and sell Ether', + image: null, + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'Eth2phone', + url: 'https://eth2.io', + description: 'Send Ether by phone number', + image: null, + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'DDEX', + url: 'https://ddex.io/', + description: + 'Instant, real-time order matching with secure on-chain settlement', + image: null, + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'EasyTrade', + url: 'https://easytrade.io', + description: 'One exchange for every token', + image: null, + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'slow.trade', + url: 'https://slow.trade/', + description: + 'Trade fairly priced crypto assets on the first platform built with the DutchX protocol.', + image: null, + category: Categories.EXCHANGES, + dateAdded: null, + }, + { + name: 'blockimmo', + url: 'https://blockimmo.ch', + description: + 'blockimmo is a blockchain powered, regulated platform enabling shared property investments and ownership.', + image: null, + category: Categories.MARKETPLACES, + dateAdded: null, + }, + { + name: 'CryptoCribs', + url: 'https://cryptocribs.com', + description: 'Travel the globe. Pay in crypto.', + image: null, + category: Categories.MARKETPLACES, + dateAdded: null, + }, + { + name: 'Ethlance', + url: 'https://ethlance.com', + description: + 'The future of work is now. Hire people or work yourself in return for ETH.', + image: null, + category: Categories.MARKETPLACES, + dateAdded: null, + }, + { + name: 'OpenSea', + url: 'https://opensea.io', + description: 'The largest decentralized marketplace for cryptogoods', + image: null, + category: Categories.MARKETPLACES, + dateAdded: null, + }, + { + name: 'Name Bazaar', + url: 'https://namebazaar.io', + description: 'ENS name marketplace', + image: null, + category: Categories.MARKETPLACES, + dateAdded: null, + }, + { + name: 'The Bounties Network', + url: 'https://bounties.network/', + description: 'Bounties on any task, paid in any token', + image: null, + category: Categories.MARKETPLACES, + dateAdded: null, + }, + { + name: 'Emoon', + url: 'https://www.emoon.io/', + description: + 'A decentralized marketplace for buying & selling crypto assets', + image: null, + category: Categories.MARKETPLACES, + dateAdded: null, + }, + { + name: 'SuperRare', + url: 'https://superrare.co/market', + description: + 'Buy, sell and collect unique digital creations by artists around the world', + image: null, + category: Categories.MARKETPLACES, + dateAdded: null, + }, + { + name: 'CryptoKitties', + url: 'https://www.cryptokitties.co', + description: 'Collect and breed adorable digital cats.', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'CryptoFighters', + url: 'https://cryptofighters.io', + description: 'Collect train and fight digital fighters.', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'Cryptographics', + url: 'https://cryptographics.app/', + description: + 'Cryptographics is a digital art hub where artists, creators and collectors can submit asset packs, create unique cryptographics and trade them.', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'CryptoPunks', + url: 'https://www.larvalabs.com/cryptopunks', + description: '10,000 unique collectible punks', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'Crypto Takeovers', + url: 'https://cryptotakeovers.com/', + description: 'Predict and conquer the world. Make a crypto fortune.', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'Decentraland', + url: 'https://market.decentraland.org/', + description: + 'Decentraland is a virtual reality platform powered by the Ethereum blockchain.', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'Dragonereum', + url: 'https://dapp.dragonereum.io', + description: 'Own and trade dragons, fight with other players.', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'Etherbots', + url: 'https://etherbots.io/', + description: 'Robot wars on the Ethereum Platform', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'Etheremon', + url: 'https://www.etheremon.com/', + description: 'Decentralized World of Ether Monsters', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'CryptoStrikers', + url: 'https://www.cryptostrikers.com/', + description: 'The Beautiful (card) Game', + image: null, + category: Categories.GAMES, + dateAdded: null, + }, + { + name: 'Cent', + url: 'https://beta.cent.co/', + description: 'Get wisdom, get money', + image: null, + category: Categories.SOCIAL_NETWORKS, + dateAdded: null, + }, + { + name: 'Kickback', + url: 'https://kickback.events/', + description: + 'Event no shows? No problem. Kickback asks event attendees to put skin in the game with Ethereum.', + image: null, + category: Categories.SOCIAL_NETWORKS, + dateAdded: null, + }, + { + name: 'Peepeth', + url: 'https://peepeth.com/', + description: 'Blockchain-powered microblogging', + image: null, + category: Categories.SOCIAL_NETWORKS, + dateAdded: null, + }, + { + name: 'Purrbook', + url: 'https://cryptopurr.co/', + description: 'A social network for CryptoKitties', + image: null, + category: Categories.SOCIAL_NETWORKS, + dateAdded: null, + }, + { + name: 'livepeer.tv', + url: 'http://livepeer.tv/', + description: 'Decentralized video broadcasting', + image: null, + category: Categories.MEDIA, + dateAdded: null, + }, + { + name: '3Box', + url: 'https://3box.io/', + description: 'Create and manage your Ethereum Profile.', + image: null, + category: Categories.SOCIAL_UTILITIES, + dateAdded: null, + }, + + { + name: 'Aragon', + url: 'https://mainnet.aragon.org/', + description: 'Build unstoppable organizations on Ethereum.', + image: null, + category: Categories.SOCIAL_UTILITIES, + dateAdded: null, + }, + + { + name: 'Civitas', + url: 'https://communities.colu.com/', + description: 'Blockchain-powered local communities', + image: null, + category: Categories.SOCIAL_UTILITIES, + dateAdded: null, + }, + + { + name: 'ETHLend', + url: 'https://app.ethlend.io', + description: 'Decentralized lending on Ethereum', + image: null, + category: Categories.SOCIAL_UTILITIES, + dateAdded: null, + }, + + { + name: 'Hexel', + url: 'https://www.onhexel.com/', + description: 'Create your own cryptocurrency', + image: null, + category: Categories.SOCIAL_UTILITIES, + dateAdded: null, + }, + + { + name: 'Livepeer', + url: 'https://explorer.livepeer.org/', + description: 'Decentralized video broadcasting', + image: null, + category: Categories.SOCIAL_UTILITIES, + dateAdded: null, + }, + + { + name: 'Smartz', + url: 'https://smartz.io', + description: 'Easy smart contract management', + image: null, + category: Categories.SOCIAL_UTILITIES, + dateAdded: null, + }, + + { + name: 'SNT Voting DApp', + url: 'https://vote.status.im', + description: + 'Let your SNT be heard! Vote on decisions exclusive to SNT holders, or create a poll of your own.', + image: null, + category: Categories.SOCIAL_UTILITIES, + dateAdded: null, + }, + + { + name: 'Status Test DApp', + url: 'simpledapp.eth', + description: 'Request test assets and test basic web3 functionality.', + image: null, + category: Categories.SOCIAL_UTILITIES, + dateAdded: null, + }, +] + +export default Dapps diff --git a/src/common/redux/reducers.js b/src/common/redux/reducers.js new file mode 100644 index 0000000..d9c0024 --- /dev/null +++ b/src/common/redux/reducers.js @@ -0,0 +1,11 @@ +import { combineReducers } from 'redux' +import { connectRouter } from 'connected-react-router' +import dapps from '../../modules/Dapps/Dapps.reducer' +import selectedCategory from '../../modules/CategorySelector/CategorySelector.reducer' + +export default history => + combineReducers({ + router: connectRouter(history), + dapps, + selectedCategory, + }) diff --git a/src/common/redux/store.js b/src/common/redux/store.js new file mode 100644 index 0000000..73cfa7a --- /dev/null +++ b/src/common/redux/store.js @@ -0,0 +1,19 @@ +import { compose, createStore, applyMiddleware } from 'redux' +import { routerMiddleware } from 'connected-react-router' +import { createBrowserHistory } from 'history' +import reducer from './reducers' + +export const history = createBrowserHistory() + +const composeWithDevTools = + /* eslint-disable-next-line no-underscore-dangle */ + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose + +const configureStore = () => + createStore( + reducer(history), + {}, + composeWithDevTools(applyMiddleware(routerMiddleware(history))), + ) + +export default configureStore diff --git a/src/common/utils/categories.js b/src/common/utils/categories.js new file mode 100644 index 0000000..b109ebc --- /dev/null +++ b/src/common/utils/categories.js @@ -0,0 +1,7 @@ +import * as Categories from '../data/categories' +import humanise from './humanise' + +export default Object.entries(Categories).map(entry => ({ + key: entry[1], + value: humanise(entry[1]), +})) diff --git a/src/common/utils/categories.test.js b/src/common/utils/categories.test.js new file mode 100644 index 0000000..968d17d --- /dev/null +++ b/src/common/utils/categories.test.js @@ -0,0 +1,32 @@ +import categories from './categories' + +describe('categories', () => { + test('it should return the correct data structure of categories', () => { + expect(categories).toEqual([ + { + key: 'EXCHANGES', + value: 'Exchanges', + }, + { + key: 'MARKETPLACES', + value: 'Marketplaces', + }, + { + key: 'GAMES', + value: 'Games', + }, + { + key: 'SOCIAL_NETWORKS', + value: 'Social Networks', + }, + { + key: 'MEDIA', + value: 'Media', + }, + { + key: 'SOCIAL_UTILITIES', + value: 'Social Utilities', + }, + ]) + }) +}) diff --git a/src/common/utils/humanise.js b/src/common/utils/humanise.js new file mode 100644 index 0000000..f304016 --- /dev/null +++ b/src/common/utils/humanise.js @@ -0,0 +1,7 @@ +const humanise = value => + value + .split('_') + .map(word => `${word[0]}${word.slice(1).toLowerCase()}`) + .join(' ') + +export default humanise diff --git a/src/common/utils/humanise.test.js b/src/common/utils/humanise.test.js new file mode 100644 index 0000000..c46866a --- /dev/null +++ b/src/common/utils/humanise.test.js @@ -0,0 +1,11 @@ +import humanise from './humanise' + +describe('humanise', () => { + test('it should transform a constant string for human reading', () => { + const first = 'TEST' + const second = 'ANOTHER_TEST' + + expect(humanise(first)).toEqual('Test') + expect(humanise(second)).toEqual('Another Test') + }) +}) diff --git a/src/common/utils/models.js b/src/common/utils/models.js new file mode 100644 index 0000000..d832b4c --- /dev/null +++ b/src/common/utils/models.js @@ -0,0 +1,12 @@ +import PropTypes from 'prop-types' + +export const DappModel = { + name: PropTypes.string, + url: PropTypes.string, + image: PropTypes.string, + description: PropTypes.string, + category: PropTypes.string, + dateAdded: PropTypes.string, +} + +export const DappListModel = PropTypes.arrayOf(PropTypes.shape(DappModel)) diff --git a/src/common/utils/reducer.js b/src/common/utils/reducer.js new file mode 100644 index 0000000..e0c165b --- /dev/null +++ b/src/common/utils/reducer.js @@ -0,0 +1,11 @@ +export default (map, defaultState) => (currentState, action) => { + const state = !currentState ? defaultState : currentState + + if (!action) { + return state + } + + return Object.keys(map).includes(action.type) + ? map[action.type](state, action.payload) + : state +} diff --git a/src/common/utils/reducer.test.js b/src/common/utils/reducer.test.js new file mode 100644 index 0000000..ef5d06b --- /dev/null +++ b/src/common/utils/reducer.test.js @@ -0,0 +1,50 @@ +import util from './reducer' + +describe('reducer utility', () => { + const reducers = { + TEST_ACTION: (state, payload) => ({ + ...state, + ...payload, + }), + } + + const state = { foo: 'bar' } + const action = { type: 'TEST_ACTION', payload: { baz: true } } + const missingAction = { type: 'MISSING_ACTION' } + const reducer = util(reducers, state) + + test("it should return the existing state when the action doesn't exist", () => { + // Given an existing state + // And an action we can't reduce + + // We should expect the function to pass through the existing state + expect(reducer(state, missingAction)).toEqual({ foo: 'bar' }) + }) + + test('it should call the correct reducer when the action exists', () => { + // Given an existing state + // And an action we can reduce + + // We expect the function to return the correctly reduced new state + expect(reducer(state, action)).toEqual({ + foo: 'bar', + baz: true, + }) + }) + + test('it should return the default state if the existing state is null', () => { + // Given a null state + // And an action we can't reduce + + // We expect the default state + expect(reducer(null, missingAction)).toEqual({ foo: 'bar' }) + }) + + test('it should return the existing state if the action is null', () => { + // Given a null state + // And an action that is undefined + + // We expect the default state + expect(reducer(state, undefined)).toEqual({ foo: 'bar' }) + }) +}) diff --git a/src/index.jsx b/src/index.jsx index 0ae6a24..1c2dbb3 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,5 +1,17 @@ import React from 'react' import ReactDOM from 'react-dom' +import { Provider } from 'react-redux' +import { ConnectedRouter } from 'connected-react-router' import App from './modules/App' +import configureStore, { history } from './common/redux/store' -ReactDOM.render(, document.getElementById('root')) +const store = configureStore() + +ReactDOM.render( + + + + + , + document.getElementById('root'), +) diff --git a/src/modules/App/Router.jsx b/src/modules/App/Router.jsx new file mode 100644 index 0000000..8977975 --- /dev/null +++ b/src/modules/App/Router.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Route, Switch } from 'react-router-dom' +import Home from '../Home' +import Filtered from '../Filtered' +import RecentlyAdded from '../RecentlyAdded' +import Dapps from '../Dapps' + +export default () => ( + + + + + + +) diff --git a/src/modules/App/index.js b/src/modules/App/index.js new file mode 100644 index 0000000..d58f88a --- /dev/null +++ b/src/modules/App/index.js @@ -0,0 +1,3 @@ +import Router from './Router' + +export default Router diff --git a/src/modules/App/index.jsx b/src/modules/App/index.jsx deleted file mode 100644 index 311b334..0000000 --- a/src/modules/App/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react' - -export default () =>

The app

diff --git a/src/modules/Categories/Categories.container.js b/src/modules/Categories/Categories.container.js new file mode 100644 index 0000000..0bc5191 --- /dev/null +++ b/src/modules/Categories/Categories.container.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux' +import { push } from 'connected-react-router' +import { selectCategory } from '../CategorySelector/CategorySelector.reducer' +import Categories from './Categories' + +const mapDispatchToProps = dispatch => ({ + select: category => { + dispatch(push('/categories')) + dispatch(selectCategory(category)) + }, +}) + +export default connect( + null, + mapDispatchToProps, +)(Categories) diff --git a/src/modules/Categories/Categories.jsx b/src/modules/Categories/Categories.jsx new file mode 100644 index 0000000..c8b2b40 --- /dev/null +++ b/src/modules/Categories/Categories.jsx @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' +import categories from '../../common/utils/categories' + +const Categories = props => { + const { select } = props + const handleClick = category => select(category) + + return ( + <> + {categories.map(category => ( + + ))} + + ) +} + +Categories.propTypes = { + select: PropTypes.func.isRequired, +} + +export default Categories diff --git a/src/modules/Categories/index.js b/src/modules/Categories/index.js new file mode 100644 index 0000000..7505d3b --- /dev/null +++ b/src/modules/Categories/index.js @@ -0,0 +1,3 @@ +import Categories from './Categories.container' + +export default Categories diff --git a/src/modules/CategorySelector/CategorySelecter.container.js b/src/modules/CategorySelector/CategorySelecter.container.js new file mode 100644 index 0000000..1beba8c --- /dev/null +++ b/src/modules/CategorySelector/CategorySelecter.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import CategorySelector from './CategorySelector' +import { selectCategory } from './CategorySelector.reducer' + +const mapStateToProps = state => ({ category: state.selectedCategory }) +const mapDispatchToProps = dispatch => ({ + select: category => dispatch(selectCategory(category)), +}) + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(CategorySelector) diff --git a/src/modules/CategorySelector/CategorySelector.jsx b/src/modules/CategorySelector/CategorySelector.jsx new file mode 100644 index 0000000..ba0bc20 --- /dev/null +++ b/src/modules/CategorySelector/CategorySelector.jsx @@ -0,0 +1,30 @@ +import React from 'react' +import PropTypes from 'prop-types' +import categories from '../../common/utils/categories' + +const CategorySelector = props => { + const { category, select } = props + const handleChange = event => select(event.target.value) + + return ( + + ) +} + +CategorySelector.propTypes = { + category: PropTypes.string, + select: PropTypes.func.isRequired, +} + +CategorySelector.defaultProps = { + category: null, +} + +export default CategorySelector diff --git a/src/modules/CategorySelector/CategorySelector.reducer.js b/src/modules/CategorySelector/CategorySelector.reducer.js new file mode 100644 index 0000000..3e752f8 --- /dev/null +++ b/src/modules/CategorySelector/CategorySelector.reducer.js @@ -0,0 +1,18 @@ +import reducerUtil from '../../common/utils/reducer' + +const UPDATE_CATEGORY = 'UPDATE_CATEGORY' + +export const selectCategory = category => ({ + type: UPDATE_CATEGORY, + payload: category, +}) + +const initialState = null + +const categoryChange = (_, category) => category + +const map = { + [UPDATE_CATEGORY]: categoryChange, +} + +export default reducerUtil(map, initialState) diff --git a/src/modules/CategorySelector/index.js b/src/modules/CategorySelector/index.js new file mode 100644 index 0000000..81cb535 --- /dev/null +++ b/src/modules/CategorySelector/index.js @@ -0,0 +1,3 @@ +import CategorySelector from './CategorySelecter.container' + +export default CategorySelector diff --git a/src/modules/Dapps/Dapps.container.js b/src/modules/Dapps/Dapps.container.js new file mode 100644 index 0000000..cf36ecd --- /dev/null +++ b/src/modules/Dapps/Dapps.container.js @@ -0,0 +1,9 @@ +import { connect } from 'react-redux' +import Dapps from './Dapps' +import selector from './Dapps.selector' + +const mapStateToProps = state => ({ + categories: selector(state), +}) + +export default connect(mapStateToProps)(Dapps) diff --git a/src/modules/Dapps/Dapps.jsx b/src/modules/Dapps/Dapps.jsx new file mode 100644 index 0000000..5dd5da7 --- /dev/null +++ b/src/modules/Dapps/Dapps.jsx @@ -0,0 +1,29 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { DappListModel } from '../../common/utils/models' +import DappList from '../../common/components/DappList' +import humanise from '../../common/utils/humanise' + +const Dapps = props => { + const { categories } = props + + return ( + <> +

All Dapps

+ {categories.map(category => ( +
+

{humanise(category.category)}

+ +
+ ))} + + ) +} + +Dapps.propTypes = { + categories: PropTypes.arrayOf( + PropTypes.shape({ category: PropTypes.string, dapps: DappListModel }), + ).isRequired, +} + +export default Dapps diff --git a/src/modules/Dapps/Dapps.reducer.js b/src/modules/Dapps/Dapps.reducer.js new file mode 100644 index 0000000..ebd2e03 --- /dev/null +++ b/src/modules/Dapps/Dapps.reducer.js @@ -0,0 +1,3 @@ +import dapps from '../../common/data/dapps' + +export default (state = dapps) => state diff --git a/src/modules/Dapps/Dapps.selector.js b/src/modules/Dapps/Dapps.selector.js new file mode 100644 index 0000000..6860551 --- /dev/null +++ b/src/modules/Dapps/Dapps.selector.js @@ -0,0 +1,24 @@ +import { createSelector } from 'reselect' + +const getDapps = state => state.dapps + +const categorisedDapps = createSelector( + [getDapps], + dapps => + dapps.reduce((acc, current) => { + if (acc.some(i => i.category === current.category)) { + return acc.map(n => + n.category === current.category + ? { + ...n, + dapps: [...n.dapps, current], + } + : n, + ) + } + + return [...acc, { category: current.category, dapps: [current] }] + }, []), +) + +export default categorisedDapps diff --git a/src/modules/Dapps/Dapps.selector.test.js b/src/modules/Dapps/Dapps.selector.test.js new file mode 100644 index 0000000..5ff596e --- /dev/null +++ b/src/modules/Dapps/Dapps.selector.test.js @@ -0,0 +1,46 @@ +import selector from './Dapps.selector' + +describe('Dapp selector', () => { + test('it should group the dapps by categories', () => { + // Given state with dapps + const state = { + dapps: [ + { + name: 'DAPP_1', + category: 'CATEGORY_1', + }, + { + name: 'DAPP_2', + category: 'CATEGORY_1', + }, + { + name: 'DAPP_3', + category: 'CATEGORY_2', + }, + { + name: 'DAPP_4', + category: 'CATEGORY_3', + }, + ], + } + + // We should get back the dapps organised by category + expect(selector(state)).toEqual([ + { + category: 'CATEGORY_1', + dapps: [ + { name: 'DAPP_1', category: 'CATEGORY_1' }, + { name: 'DAPP_2', category: 'CATEGORY_1' }, + ], + }, + { + category: 'CATEGORY_2', + dapps: [{ name: 'DAPP_3', category: 'CATEGORY_2' }], + }, + { + category: 'CATEGORY_3', + dapps: [{ name: 'DAPP_4', category: 'CATEGORY_3' }], + }, + ]) + }) +}) diff --git a/src/modules/Dapps/index.js b/src/modules/Dapps/index.js new file mode 100644 index 0000000..76da169 --- /dev/null +++ b/src/modules/Dapps/index.js @@ -0,0 +1,3 @@ +import Dapps from './Dapps.container' + +export default Dapps diff --git a/src/modules/Filtered/Filtered.container.js b/src/modules/Filtered/Filtered.container.js new file mode 100644 index 0000000..76d3bb0 --- /dev/null +++ b/src/modules/Filtered/Filtered.container.js @@ -0,0 +1,9 @@ +import { connect } from 'react-redux' +import Filtered from './Filtered' +import filteredDapps from './Filtered.selector' + +const mapStateToProps = state => ({ + dapps: filteredDapps(state), +}) + +export default connect(mapStateToProps)(Filtered) diff --git a/src/modules/Filtered/Filtered.jsx b/src/modules/Filtered/Filtered.jsx new file mode 100644 index 0000000..74a06a3 --- /dev/null +++ b/src/modules/Filtered/Filtered.jsx @@ -0,0 +1,24 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import { DappListModel } from '../../common/utils/models' +import CategorySelector from '../CategorySelector' +import DappList from '../../common/components/DappList' + +const Filtered = props => { + const { dapps } = props + + return ( + <> +

Filtered

+ + View all + + + ) +} + +Filtered.propTypes = { + dapps: DappListModel.isRequired, +} + +export default Filtered diff --git a/src/modules/Filtered/Filtered.selector.js b/src/modules/Filtered/Filtered.selector.js new file mode 100644 index 0000000..9b375aa --- /dev/null +++ b/src/modules/Filtered/Filtered.selector.js @@ -0,0 +1,10 @@ +import { createSelector } from 'reselect' + +const getCategory = state => state.selectedCategory +const getDapps = state => state.dapps + +export default createSelector( + [getCategory, getDapps], + (category, dapps) => + category ? dapps.filter(dapp => dapp.category === category) : dapps, +) diff --git a/src/modules/Filtered/Filtered.selector.test.js b/src/modules/Filtered/Filtered.selector.test.js new file mode 100644 index 0000000..f7a0cab --- /dev/null +++ b/src/modules/Filtered/Filtered.selector.test.js @@ -0,0 +1,38 @@ +import filteredDapps from './Filtered.selector' + +describe('filteredDapps', () => { + const dapps = [ + { + name: 'DAPP_1', + category: 'CATEGORY_1', + }, + { + name: 'DAPP_2', + category: 'CATEGORY_2', + }, + ] + + test('it should return all the dapps when the category is not set', () => { + // Given a state where the selected category is null + const state = { + dapps, + selectedCategory: null, + } + + // We expect to get back all the dapps + expect(filteredDapps(state)).toEqual(dapps) + }) + + test('it should return only the matching dapps when the category is set', () => { + // Given a state where the selected category is set + const state = { + dapps, + selectedCategory: 'CATEGORY_1', + } + + // We expect to get back only the matching dapps + expect(filteredDapps(state)).toEqual([ + { name: 'DAPP_1', category: 'CATEGORY_1' }, + ]) + }) +}) diff --git a/src/modules/Filtered/index.js b/src/modules/Filtered/index.js new file mode 100644 index 0000000..05197fe --- /dev/null +++ b/src/modules/Filtered/index.js @@ -0,0 +1,3 @@ +import Filtered from './Filtered.container' + +export default Filtered diff --git a/src/modules/Home/Home.container.js b/src/modules/Home/Home.container.js new file mode 100644 index 0000000..ec93b99 --- /dev/null +++ b/src/modules/Home/Home.container.js @@ -0,0 +1,6 @@ +import { connect } from 'react-redux' +import Home from './Home' + +const mapStateToProps = state => state + +export default connect(mapStateToProps)(Home) diff --git a/src/modules/Home/Home.jsx b/src/modules/Home/Home.jsx new file mode 100644 index 0000000..40d1b62 --- /dev/null +++ b/src/modules/Home/Home.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import RecentlyAdded from '../RecentlyAdded' +import Categories from '../Categories' + +export default () => ( + <> +

Home

+ + + +) diff --git a/src/modules/Home/index.js b/src/modules/Home/index.js new file mode 100644 index 0000000..565aed9 --- /dev/null +++ b/src/modules/Home/index.js @@ -0,0 +1,3 @@ +import Home from './Home.container' + +export default Home diff --git a/src/modules/RecentlyAdded/RecentlyAdded.container.js b/src/modules/RecentlyAdded/RecentlyAdded.container.js new file mode 100644 index 0000000..d8dbb2d --- /dev/null +++ b/src/modules/RecentlyAdded/RecentlyAdded.container.js @@ -0,0 +1,9 @@ +import { connect } from 'react-redux' +import RecentlyAdded from './RecentlyAdded' +import recentDapps from './RecentlyAdded.selector' + +const mapStateToProps = state => ({ + dapps: recentDapps(state), +}) + +export default connect(mapStateToProps)(RecentlyAdded) diff --git a/src/modules/RecentlyAdded/RecentlyAdded.jsx b/src/modules/RecentlyAdded/RecentlyAdded.jsx new file mode 100644 index 0000000..994867d --- /dev/null +++ b/src/modules/RecentlyAdded/RecentlyAdded.jsx @@ -0,0 +1,22 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import { DappListModel } from '../../common/utils/models' +import DappList from '../../common/components/DappList' + +const RecentlyAdded = props => { + const { dapps } = props + + return ( + <> +

Recently Added

+ View all + + + ) +} + +RecentlyAdded.propTypes = { + dapps: DappListModel.isRequired, +} + +export default RecentlyAdded diff --git a/src/modules/RecentlyAdded/RecentlyAdded.selector.js b/src/modules/RecentlyAdded/RecentlyAdded.selector.js new file mode 100644 index 0000000..1f0173c --- /dev/null +++ b/src/modules/RecentlyAdded/RecentlyAdded.selector.js @@ -0,0 +1,11 @@ +import { createSelector } from 'reselect' +import moment from 'moment' + +const getDapps = state => state.dapps + +const recentDapps = createSelector( + [getDapps], + dapps => [...dapps].sort((a, b) => moment(b.dateAdded).diff(a.dateAdded)), +) + +export default recentDapps diff --git a/src/modules/RecentlyAdded/RecentlyAdded.selector.test.js b/src/modules/RecentlyAdded/RecentlyAdded.selector.test.js new file mode 100644 index 0000000..0956d1a --- /dev/null +++ b/src/modules/RecentlyAdded/RecentlyAdded.selector.test.js @@ -0,0 +1,37 @@ +import moment from 'moment' +import recentDapps from './RecentlyAdded.selector' + +describe('recentDapps', () => { + test('it should return the dapps ordered by most recently added', () => { + // Given a state with multiple dapps + const today = moment().format() + const yesterday = moment() + .subtract(1, 'days') + .format() + + const state = { + dapps: [ + { + name: 'DAPP_1', + dateAdded: yesterday, + }, + { + name: 'DAPP_2', + dateAdded: today, + }, + ], + } + + // We expect to get the apps ordered by the dateAdded + expect(recentDapps(state)).toEqual([ + { + name: 'DAPP_2', + dateAdded: today, + }, + { + name: 'DAPP_1', + dateAdded: yesterday, + }, + ]) + }) +}) diff --git a/src/modules/RecentlyAdded/index.js b/src/modules/RecentlyAdded/index.js new file mode 100644 index 0000000..b06074f --- /dev/null +++ b/src/modules/RecentlyAdded/index.js @@ -0,0 +1,3 @@ +import RecentlyAdded from './RecentlyAdded.container' + +export default RecentlyAdded