feat: unskinned app (#3)
This commit is contained in:
parent
543c974802
commit
a8b8378419
|
@ -5,6 +5,11 @@
|
|||
"prettier/prettier": "error"
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jest": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 => <DappListItem {...dapp} key={dapp.name} />)
|
||||
}
|
||||
|
||||
DappList.propTypes = {
|
||||
dapps: DappListModel.isRequired,
|
||||
}
|
||||
|
||||
export default DappList
|
|
@ -0,0 +1,3 @@
|
|||
import DappList from './DappList'
|
||||
|
||||
export default DappList
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react'
|
||||
import { DappModel } from '../../utils/models'
|
||||
|
||||
const DappListItem = props => {
|
||||
const { name } = props
|
||||
return <div>{name}</div>
|
||||
}
|
||||
|
||||
DappListItem.propTypes = DappModel
|
||||
|
||||
export default DappListItem
|
|
@ -0,0 +1,3 @@
|
|||
import DappListItem from './DappListItem'
|
||||
|
||||
export default DappListItem
|
|
@ -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'
|
|
@ -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
|
|
@ -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,
|
||||
})
|
|
@ -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
|
|
@ -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]),
|
||||
}))
|
|
@ -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',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
const humanise = value =>
|
||||
value
|
||||
.split('_')
|
||||
.map(word => `${word[0]}${word.slice(1).toLowerCase()}`)
|
||||
.join(' ')
|
||||
|
||||
export default humanise
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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))
|
|
@ -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
|
||||
}
|
|
@ -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' })
|
||||
})
|
||||
})
|
|
@ -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(<App />, document.getElementById('root'))
|
||||
const store = configureStore()
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<App />
|
||||
</ConnectedRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
)
|
||||
|
|
|
@ -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 () => (
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="/categories" component={Filtered} />
|
||||
<Route path="/all" component={Dapps} />
|
||||
<Route path="/recently-added" component={RecentlyAdded} />
|
||||
</Switch>
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
import Router from './Router'
|
||||
|
||||
export default Router
|
|
@ -1,3 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
export default () => <h1>The app</h1>
|
|
@ -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)
|
|
@ -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 => (
|
||||
<button
|
||||
key={category.key}
|
||||
type="button"
|
||||
onClick={handleClick.bind(this, category.key)}
|
||||
>
|
||||
{category.value}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Categories.propTypes = {
|
||||
select: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default Categories
|
|
@ -0,0 +1,3 @@
|
|||
import Categories from './Categories.container'
|
||||
|
||||
export default Categories
|
|
@ -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)
|
|
@ -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 (
|
||||
<select value={category || ''} onChange={handleChange}>
|
||||
<option style={{ display: 'none' }} />
|
||||
{categories.map(c => (
|
||||
<option key={c.key} value={c.key}>
|
||||
{c.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
CategorySelector.propTypes = {
|
||||
category: PropTypes.string,
|
||||
select: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
CategorySelector.defaultProps = {
|
||||
category: null,
|
||||
}
|
||||
|
||||
export default CategorySelector
|
|
@ -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)
|
|
@ -0,0 +1,3 @@
|
|||
import CategorySelector from './CategorySelecter.container'
|
||||
|
||||
export default CategorySelector
|
|
@ -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)
|
|
@ -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 (
|
||||
<>
|
||||
<h1>All Dapps</h1>
|
||||
{categories.map(category => (
|
||||
<div key={category.category}>
|
||||
<h2>{humanise(category.category)}</h2>
|
||||
<DappList dapps={category.dapps} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Dapps.propTypes = {
|
||||
categories: PropTypes.arrayOf(
|
||||
PropTypes.shape({ category: PropTypes.string, dapps: DappListModel }),
|
||||
).isRequired,
|
||||
}
|
||||
|
||||
export default Dapps
|
|
@ -0,0 +1,3 @@
|
|||
import dapps from '../../common/data/dapps'
|
||||
|
||||
export default (state = dapps) => state
|
|
@ -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
|
|
@ -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' }],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,3 @@
|
|||
import Dapps from './Dapps.container'
|
||||
|
||||
export default Dapps
|
|
@ -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)
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Filtered</h1>
|
||||
<CategorySelector />
|
||||
<Link to="/all">View all</Link>
|
||||
<DappList dapps={dapps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Filtered.propTypes = {
|
||||
dapps: DappListModel.isRequired,
|
||||
}
|
||||
|
||||
export default Filtered
|
|
@ -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,
|
||||
)
|
|
@ -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' },
|
||||
])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,3 @@
|
|||
import Filtered from './Filtered.container'
|
||||
|
||||
export default Filtered
|
|
@ -0,0 +1,6 @@
|
|||
import { connect } from 'react-redux'
|
||||
import Home from './Home'
|
||||
|
||||
const mapStateToProps = state => state
|
||||
|
||||
export default connect(mapStateToProps)(Home)
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react'
|
||||
import RecentlyAdded from '../RecentlyAdded'
|
||||
import Categories from '../Categories'
|
||||
|
||||
export default () => (
|
||||
<>
|
||||
<h1>Home</h1>
|
||||
<Categories />
|
||||
<RecentlyAdded />
|
||||
</>
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
import Home from './Home.container'
|
||||
|
||||
export default Home
|
|
@ -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)
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Recently Added</h1>
|
||||
<Link to="/all">View all</Link>
|
||||
<DappList dapps={dapps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
RecentlyAdded.propTypes = {
|
||||
dapps: DappListModel.isRequired,
|
||||
}
|
||||
|
||||
export default RecentlyAdded
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,3 @@
|
|||
import RecentlyAdded from './RecentlyAdded.container'
|
||||
|
||||
export default RecentlyAdded
|
Loading…
Reference in New Issue