├── webapp ├── src │ ├── themes │ │ ├── components │ │ │ └── .gitkeep │ │ ├── index.ts │ │ └── variables.css │ ├── components │ │ ├── Page │ │ │ ├── Page.css │ │ │ ├── index.ts │ │ │ └── Page.tsx │ │ ├── HomePage │ │ │ ├── index.ts │ │ │ └── HomePage.tsx │ │ ├── DomainsPage │ │ │ ├── index.ts │ │ │ ├── DomainsPage.types.ts │ │ │ ├── DomainsPage.container.ts │ │ │ └── DomainsPage.tsx │ │ └── DomainDetailPage │ │ │ ├── index.ts │ │ │ ├── DomainDetailPage.types.ts │ │ │ ├── DomainDetailPage.tsx │ │ │ └── DomainDetailPage.container.ts │ ├── modules │ │ └── domain │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ ├── selectors.ts │ │ │ ├── sagas.ts │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ ├── index.css │ ├── locations.ts │ ├── contracts │ │ └── index.ts │ ├── lib │ │ ├── utils.ts │ │ └── api.ts │ ├── types.ts │ ├── index.tsx │ ├── reducer.ts │ ├── sagas.ts │ ├── Routes.tsx │ ├── store.ts │ └── logo.svg ├── tsconfig.prod.json ├── images.d.ts ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── tsconfig.test.json ├── .env.example ├── config │ ├── jest │ │ ├── typescriptTransform.js │ │ ├── fileTransform.js │ │ └── cssTransform.js │ ├── polyfills.js │ ├── paths.js │ ├── env.js │ ├── webpackDevServer.config.js │ ├── webpack.config.dev.js │ └── webpack.config.prod.js ├── .gitignore ├── tslint.json ├── scripts │ ├── test.js │ ├── start.js │ └── build.js ├── tsconfig.json └── package.json ├── .prettierignore ├── specs ├── .env.example ├── setup.ts ├── specs_setup.ts └── utils.ts ├── src ├── database │ ├── index.ts │ └── database.ts ├── lib │ ├── index.ts │ ├── blacklist.ts │ └── Router.ts ├── Domain │ ├── index.ts │ ├── Domain.types.ts │ ├── Domain.model.ts │ ├── Domain.router.ts │ └── Domain.spec.ts ├── tsconfig.json ├── Translation │ ├── index.ts │ ├── locales │ │ ├── en.json │ │ └── es.json │ ├── Translation.spec.ts │ ├── Translation.router.ts │ └── Translation.ts ├── .env.example └── server.ts ├── scripts ├── tsconfig.json ├── node-pg-migrate ├── utils.ts ├── migrate.ts ├── create-model.ts └── translate.ts ├── .vscode └── settings.json ├── .gitignore ├── tslint.json ├── tsconfig.json ├── migrations └── 1527529930376_domains-create.ts ├── README.md └── package.json /webapp/src/themes/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json -------------------------------------------------------------------------------- /webapp/src/components/Page/Page.css: -------------------------------------------------------------------------------- 1 | .Page { 2 | } 3 | -------------------------------------------------------------------------------- /specs/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | CONNECTION_STRING= 3 | -------------------------------------------------------------------------------- /webapp/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | export { database as db } from './database' 2 | -------------------------------------------------------------------------------- /webapp/src/themes/index.ts: -------------------------------------------------------------------------------- 1 | // Variables 2 | import './variables.css' 3 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Router' 2 | export * from './blacklist' 3 | -------------------------------------------------------------------------------- /src/Domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Domain.model' 2 | export * from './Domain.router' 3 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /webapp/src/components/Page/index.ts: -------------------------------------------------------------------------------- 1 | import Page from './Page' 2 | 3 | export default Page 4 | -------------------------------------------------------------------------------- /src/Translation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Translation' 2 | export * from './Translation.router' 3 | -------------------------------------------------------------------------------- /webapp/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | declare module '*.png' 3 | declare module '*.jpg' 4 | -------------------------------------------------------------------------------- /webapp/src/modules/domain/types.ts: -------------------------------------------------------------------------------- 1 | export interface Domain { 2 | id: string 3 | param: string 4 | } 5 | -------------------------------------------------------------------------------- /webapp/src/components/HomePage/index.ts: -------------------------------------------------------------------------------- 1 | import HomePage from './HomePage' 2 | 3 | export default HomePage 4 | -------------------------------------------------------------------------------- /webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/dapp-boilerplate/HEAD/webapp/public/favicon.ico -------------------------------------------------------------------------------- /specs/setup.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../src/database' 2 | 3 | before(() => db.connect()) 4 | after(() => db.close()) 5 | -------------------------------------------------------------------------------- /webapp/src/components/DomainsPage/index.ts: -------------------------------------------------------------------------------- 1 | import DomainsPage from './DomainsPage.container' 2 | 3 | export default DomainsPage 4 | -------------------------------------------------------------------------------- /webapp/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /webapp/src/components/DomainDetailPage/index.ts: -------------------------------------------------------------------------------- 1 | import DomainDetailPage from './DomainDetailPage.container' 2 | 3 | export default DomainDetailPage 4 | -------------------------------------------------------------------------------- /src/Domain/Domain.types.ts: -------------------------------------------------------------------------------- 1 | export interface DomainAttributes { 2 | id: number 3 | param: string 4 | created_at?: Date 5 | updated_at?: Date 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/blacklist.ts: -------------------------------------------------------------------------------- 1 | const timestamps = ['created_at', 'updated_at'] 2 | 3 | export const blacklist = Object.freeze({ 4 | domain: [...timestamps] 5 | }) 6 | -------------------------------------------------------------------------------- /scripts/node-pg-migrate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('ts-node').register(require('../tsconfig.json')) 4 | require('../node_modules/node-pg-migrate/bin/node-pg-migrate') 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.printWidth": 80, 3 | "prettier.singleQuote": true, 4 | "prettier.semi": false, 5 | "editor.tabSize": 2, 6 | "editor.formatOnSave": true 7 | } 8 | -------------------------------------------------------------------------------- /src/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | 3 | SERVER_PORT= 4 | TRANSLATIONS_PATH= 5 | 6 | CONNECTION_STRING= 7 | RPC_URL= 8 | 9 | LAND_REGISTRY_CONTRACT_ADDRESS= 10 | MANA_TOKEN_CONTRACT_ADDRESS= 11 | -------------------------------------------------------------------------------- /webapp/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | padding: 0; 13 | font-family: var(--font-family); 14 | } 15 | -------------------------------------------------------------------------------- /webapp/.env.example: -------------------------------------------------------------------------------- 1 | NODE_PATH= 2 | NODE_ENV= 3 | 4 | REACT_APP_PROVIDER_URL= 5 | 6 | REACT_APP_API_URL= 7 | REACT_APP_VERSION= 8 | 9 | REACT_APP_LAND_REGISTRY_CONTRACT_ADDRESS= 10 | REACT_APP_MANA_TOKEN_CONTRACT_ADDRESS= 11 | -------------------------------------------------------------------------------- /webapp/config/jest/typescriptTransform.js: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | 'use strict'; 4 | 5 | const tsJestPreprocessor = require('ts-jest/preprocessor'); 6 | 7 | module.exports = tsJestPreprocessor; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./data 2 | postgres_data 3 | out 4 | 5 | /node_modules/ 6 | /.idea/ 7 | /webapp/.idea/ 8 | 9 | newrelic_agent.log 10 | .DS_Store 11 | .idea 12 | .env 13 | *.todo 14 | *.TODO 15 | 16 | migrations/**/*.js* 17 | scripts/**/*.js* 18 | src/**/*.js* -------------------------------------------------------------------------------- /webapp/src/modules/domain/utils.ts: -------------------------------------------------------------------------------- 1 | import { Domain } from 'modules/domain/types' 2 | 3 | export function toDomainObject(domains: Domain[]): { [id: string]: Domain } { 4 | return domains.reduce((map, domain) => { 5 | map[domain.id] = domain 6 | return map 7 | }, {}) 8 | } 9 | -------------------------------------------------------------------------------- /webapp/src/locations.ts: -------------------------------------------------------------------------------- 1 | export interface Locations { 2 | [key: string]: (...args: any[]) => string 3 | } 4 | 5 | export const locations: Locations = { 6 | root: () => '/', 7 | 8 | domains: () => '/domains', 9 | 10 | domain: () => '/domains/:id', 11 | domainDetail: id => `/domains/${id}` 12 | } 13 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["dcl-tslint-config-standard", "tslint-plugin-prettier"], 3 | "rules": { 4 | "no-commented-out-code": false, 5 | "prettier": [true, { "printWidth": 80, "singleQuote": true, "semi": false }] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["./**/*.js"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /webapp/src/themes/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #ff4130; 3 | --secondary: #00dbef; 4 | 5 | --text: #ffffff; 6 | --background: #1f2333; 7 | --error: #ffb208; 8 | 9 | --font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', 10 | Roboto, 'Helvetica Neue', Arial, sans-serif; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/Router.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express' 2 | 3 | export class Router { 4 | protected app: express.Application | express.Router 5 | 6 | constructor(app: express.Application | express.Router) { 7 | this.app = app 8 | } 9 | 10 | mount(): void { 11 | throw new Error('Not implemented') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webapp/src/components/Page/Page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import './Page.css' 4 | 5 | export default class Page extends React.PureComponent { 6 | static defaultProps = { 7 | children: null 8 | } 9 | 10 | render() { 11 | const { children } = this.props 12 | 13 | return
{children}
14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webapp/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /webapp/src/contracts/index.ts: -------------------------------------------------------------------------------- 1 | import { contracts } from 'decentraland-eth' 2 | import { env } from 'decentraland-commons' 3 | 4 | const manaToken = new contracts.MANAToken( 5 | env.get('REACT_APP_MANA_TOKEN_CONTRACT_ADDRESS') 6 | ) 7 | 8 | const landRegistry = new contracts.LANDRegistry( 9 | env.get('REACT_APP_LAND_REGISTRY_CONTRACT_ADDRESS') 10 | ) 11 | 12 | export { manaToken, landRegistry } 13 | -------------------------------------------------------------------------------- /src/database/database.ts: -------------------------------------------------------------------------------- 1 | import { db } from 'decentraland-server' 2 | import { env } from 'decentraland-commons' 3 | 4 | const pg = db.clients.postgres 5 | 6 | export const database: typeof pg = Object.create(pg) 7 | 8 | database.connect = async () => { 9 | const CONNECTION_STRING = env.get('CONNECTION_STRING', null) 10 | this.client = await pg.connect(CONNECTION_STRING) 11 | return this 12 | } 13 | -------------------------------------------------------------------------------- /webapp/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | 20 | src/**/*.js.map 21 | src/**/*.js 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /webapp/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export function isMobile() { 2 | // WARN: Super naive mobile device check. 3 | // we're using it on low-stake checks, where failing to detect some browsers is not a big deal. 4 | // If you need more specificity you may want to change this implementation. 5 | const navigator = window.navigator 6 | 7 | return ( 8 | /Mobi/i.test(navigator.userAgent) || /Android/i.test(navigator.userAgent) 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/Translation/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": { 3 | "en": "English", 4 | "es": "Spanish" 5 | }, 6 | "global": { 7 | "domains": "Domains", 8 | "domain": "Domain", 9 | "loading": "Loading" 10 | }, 11 | "domains_page": { 12 | "title": "Domains Page", 13 | "empty_domains": "No domains yet" 14 | }, 15 | "domain_detail_page": { 16 | "title": "Domain Detail Page", 17 | "no_domain": "Domain does not exist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Translation/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": { 3 | "en": "Inglés", 4 | "es": "Español" 5 | }, 6 | "global": { 7 | "domain": "Dominio", 8 | "domains": "Dominios", 9 | "loading": "Cargando" 10 | }, 11 | "domains_page": { 12 | "title": "Página de dominios", 13 | "empty_domains": "Sin dominios todavía" 14 | }, 15 | "domain_detail_page": { 16 | "title": "Página de detalles del dominio", 17 | "no_domain": "El dominio no existe" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /webapp/src/components/DomainsPage/DomainsPage.types.ts: -------------------------------------------------------------------------------- 1 | import { Omit } from '@dapps/lib/types' 2 | import { DomainState } from 'modules/domain/reducer' 3 | import { fetchDomainsRequest } from 'modules/domain/actions' 4 | 5 | export type Props = { 6 | domains: DomainState['data'] | null 7 | isLoading: boolean 8 | onFetchDomains: typeof fetchDomainsRequest 9 | } 10 | 11 | export type MapStateProps = Omit 12 | export type MapDispatchProps = Pick 13 | -------------------------------------------------------------------------------- /webapp/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-react", 4 | "tslint-plugin-prettier", 5 | "dcl-tslint-config-standard" 6 | ], 7 | "rules": { 8 | "no-commented-out-code": false, 9 | "jsx-no-multiline-js": false, 10 | "prettier": [ 11 | true, 12 | { "printWidth": 80, "singleQuote": true, "semi": false } 13 | ], 14 | "quotemark": [true, "single", "jsx-double"] 15 | }, 16 | "linterOptions": { 17 | "exclude": ["config/**/*.js", "node_modules/**/*.ts", "./**/*.js"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Domain/Domain.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, SQL } from 'decentraland-server' 2 | import { DomainAttributes } from './Domain.types' 3 | 4 | export class Domain extends Model { 5 | static tableName = 'domains' 6 | 7 | static findByParam(param) { 8 | return this.findOne({ param }) 9 | } 10 | 11 | static findByComplexQuery(param: string) { 12 | return this.query(SQL` 13 | SELECT * 14 | FROM ${SQL.raw(this.tableName)} 15 | WHERE param = ${param}`) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webapp/src/modules/domain/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'types' 2 | import { DomainState } from 'modules/domain/reducer' 3 | 4 | export const getState: (state: RootState) => DomainState = state => state.domain 5 | 6 | export const getData: (state: RootState) => DomainState['data'] = state => 7 | getState(state).data 8 | 9 | export const isLoading: (state: RootState) => boolean = state => 10 | getState(state).loading.length > 0 11 | 12 | export const getError: (state: RootState) => DomainState['error'] = state => 13 | getState(state).error 14 | -------------------------------------------------------------------------------- /webapp/src/components/DomainDetailPage/DomainDetailPage.types.ts: -------------------------------------------------------------------------------- 1 | import { Omit } from '@dapps/lib/types' 2 | import { match } from 'react-router' 3 | import { Domain } from 'modules/domain/types' 4 | import { fetchDomainRequest } from 'modules/domain/actions' 5 | 6 | export interface URLParams { 7 | id: string 8 | } 9 | 10 | export type Props = { 11 | match: match 12 | domain: Domain | null 13 | isLoading: boolean 14 | onFetchDomain: typeof fetchDomainRequest 15 | } 16 | 17 | export type MapStateProps = Omit 18 | export type MapDispatchProps = Pick 19 | -------------------------------------------------------------------------------- /webapp/src/components/HomePage/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { t } from '@dapps/modules/translation/utils' 4 | import { locations } from 'locations' 5 | 6 | export default class HomePage extends React.PureComponent { 7 | render() { 8 | return ( 9 |
10 |

Home Page

11 | 12 |
13 | {t('global.domains')} 14 |
15 | 16 |
17 | {t('global.domain')} 1 18 |
19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webapp/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, Store } from 'redux' 2 | import { RouterState } from 'react-router-redux' 3 | 4 | import { DomainState } from 'modules/domain/reducer' 5 | import { TransactionState } from '@dapps/modules/transaction/reducer' 6 | import { TranslationState } from '@dapps/modules/translation/reducer' 7 | import { WalletState } from '@dapps/modules/wallet/reducer' 8 | 9 | export type RootState = { 10 | router: RouterState 11 | domain: DomainState 12 | transaction: TransactionState 13 | translation: TranslationState 14 | wallet: WalletState 15 | } 16 | 17 | export type RootStore = Store 18 | export type RootReducer = Reducer 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "diagnostics": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": false, 9 | "inlineSources": false, 10 | "sourceMap": true, 11 | "declaration": false, 12 | "moduleResolution": "node", 13 | "stripInternal": true, 14 | "pretty": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noUnusedParameters": true, 17 | "noUnusedLocals": true, 18 | "lib": ["es2017"], 19 | "plugins": [{ "name": "tslint-language-service" }] 20 | }, 21 | "exclude": ["node_modules", "test", "dist"] 22 | } 23 | -------------------------------------------------------------------------------- /migrations/1527529930376_domains-create.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate' 2 | import { Domain } from '../src/Domain' 3 | 4 | const tableName = Domain.tableName 5 | 6 | exports.up = (pgm: MigrationBuilder) => { 7 | pgm.createTable( 8 | tableName, 9 | { 10 | id: { type: 'INT', primaryKey: true, notNull: true, comment: null }, 11 | param: { type: 'TEXT', comment: null }, 12 | created_at: { type: 'TIMESTAMP', notNull: true, comment: null }, 13 | updated_at: { type: 'TIMESTAMP', comment: null } 14 | }, 15 | { ifNotExists: true, comment: null } 16 | ) 17 | 18 | pgm.createIndex(tableName, 'param') 19 | } 20 | 21 | exports.down = (pgm: MigrationBuilder) => { 22 | pgm.dropIndex(tableName, 'param') 23 | pgm.dropTable(tableName, {}) 24 | } 25 | -------------------------------------------------------------------------------- /webapp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | 4 | import { Provider } from 'react-redux' 5 | import { ConnectedRouter } from 'react-router-redux' 6 | import TranslationProvider from '@dapps/providers/TranslationProvider' 7 | import WalletProvider from '@dapps/providers/WalletProvider' 8 | 9 | import Routes from './Routes' 10 | import { store, history } from './store' 11 | 12 | import './index.css' 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | , 24 | document.getElementById('root') 25 | ) 26 | -------------------------------------------------------------------------------- /webapp/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as router } from 'react-router-redux' 3 | 4 | import { RootState } from 'types' 5 | import { domainReducer as domain } from 'modules/domain/reducer' 6 | import { transactionReducer as transaction } from '@dapps/modules/transaction/reducer' 7 | import { translationReducer as translation } from '@dapps/modules/translation/reducer' 8 | import { walletReducer as wallet } from '@dapps/modules/wallet/reducer' 9 | import { 10 | storageReducer as storage, 11 | storageReducerWrapper 12 | } from '@dapps/modules/storage/reducer' 13 | 14 | export const rootReducer = storageReducerWrapper( 15 | combineReducers({ 16 | domain, 17 | transaction, 18 | translation, 19 | router, 20 | storage, 21 | wallet 22 | }) 23 | ) 24 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'decentraland-commons' 2 | import * as path from 'path' 3 | 4 | export function loadEnv(envFilePath: string = '../src/.env') { 5 | env.load({ path: resolvePath(envFilePath) }) 6 | } 7 | 8 | export function runpsql(filename: string) { 9 | return `psql $CONNECTION_STRING -f ${resolvePath(filename)}` 10 | } 11 | 12 | export function resolvePath(destination: string) { 13 | return path.resolve(getDirname(), destination) 14 | } 15 | 16 | export function getDirname() { 17 | return path.dirname(require.main.filename) 18 | } 19 | 20 | export function parseCLICoords(coord: string) { 21 | if (!coord) throw new Error('You need to supply a coordinate') 22 | 23 | return coord 24 | .replace('(', '') 25 | .replace(')', '') 26 | .split(/\s*,\s*/) 27 | .map(coord => parseInt(coord.trim(), 10)) 28 | } 29 | -------------------------------------------------------------------------------- /webapp/src/components/DomainsPage/DomainsPage.container.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { RootDispatch } from '@dapps/types' 3 | import { RootState } from 'types' 4 | import { fetchDomainsRequest } from 'modules/domain/actions' 5 | import { getData, isLoading } from 'modules/domain/selectors' 6 | import { 7 | MapStateProps, 8 | MapDispatchProps 9 | } from 'components/DomainsPage/DomainsPage.types' 10 | 11 | import DomainsPage from './DomainsPage' 12 | 13 | const mapState = (state: RootState): MapStateProps => { 14 | return { 15 | domains: getData(state), 16 | isLoading: isLoading(state) 17 | } 18 | } 19 | 20 | const mapDispatch = (dispatch: RootDispatch): MapDispatchProps => ({ 21 | onFetchDomains: () => dispatch(fetchDomainsRequest()) 22 | }) 23 | 24 | export default connect(mapState, mapDispatch)(DomainsPage) 25 | -------------------------------------------------------------------------------- /src/Translation/Translation.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { Translation } from './Translation' 3 | 4 | const DEFAULT_LOCALE = Translation.DEFAULT_LOCALE 5 | const translation = new Translation() 6 | 7 | describe('Translation locales', async function() { 8 | const mainTranslations = await translation.fetch(DEFAULT_LOCALE) 9 | const availableLocales = await translation.getAvailableLocales() 10 | 11 | const mainKeys = Object.keys(mainTranslations) 12 | 13 | for (const locale of availableLocales) { 14 | if (locale !== DEFAULT_LOCALE) { 15 | it(`should have the same keys as the default locale. ${locale} = ${DEFAULT_LOCALE}`, async function() { 16 | const translations = await translation.fetch(locale) 17 | const keys = Object.keys(translations) 18 | expect(keys).to.deep.equal(mainKeys) 19 | }) 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/Translation/Translation.router.ts: -------------------------------------------------------------------------------- 1 | import { server } from 'decentraland-server' 2 | import * as express from 'express' 3 | 4 | import { Translation, TranslationData } from './Translation' 5 | import { Router } from '../lib' 6 | 7 | export class TranslationRouter extends Router { 8 | mount() { 9 | /** 10 | * Returns the translations for a given locale 11 | * @param {string} locale - locale name 12 | * @return {array} 13 | */ 14 | this.app.get( 15 | '/translations/:locale', 16 | server.handleRequest(this.getTranslations) 17 | ) 18 | } 19 | 20 | async getTranslations(req: express.Request): Promise { 21 | let locale = server.extractFromReq(req, 'locale') 22 | locale = locale.slice(0, 2) // We support base locales for now, like en, it, etc 23 | return new Translation().fetch(locale) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webapp/src/sagas.ts: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects' 2 | import { env } from 'decentraland-commons' 3 | import { eth } from 'decentraland-eth' 4 | 5 | import { domainSaga } from 'modules/domain/sagas' 6 | import { transactionSaga } from '@dapps/modules/transaction/sagas' 7 | import { createTranslationSaga } from '@dapps/modules/translation/sagas' 8 | import { createWalletSaga } from '@dapps/modules/wallet/sagas' 9 | import { manaToken, landRegistry } from 'contracts' 10 | import { api } from 'lib/api' 11 | 12 | const walletSaga = createWalletSaga({ 13 | provider: env.get('REACT_APP_PROVIDER_URL'), 14 | contracts: [manaToken, landRegistry], 15 | eth 16 | }) 17 | 18 | const translationSaga = createTranslationSaga({ 19 | getTranslation: locale => api.fetchTranslation(locale) 20 | }) 21 | 22 | export function* rootSaga() { 23 | yield all([domainSaga(), transactionSaga(), translationSaga(), walletSaga()]) 24 | } 25 | -------------------------------------------------------------------------------- /webapp/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /webapp/scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | let argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI, in coverage mode, or explicitly running all tests 22 | if ( 23 | !process.env.CI && 24 | argv.indexOf('--coverage') === -1 && 25 | argv.indexOf('--watchAll') === -1 26 | ) { 27 | argv.push('--watch'); 28 | } 29 | 30 | 31 | jest.run(argv); 32 | -------------------------------------------------------------------------------- /webapp/src/components/DomainDetailPage/DomainDetailPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { t } from '@dapps/modules/translation/utils' 3 | import { Props } from 'components/DomainDetailPage/DomainDetailPage.types' 4 | 5 | export default class DomainDetailPage extends React.PureComponent { 6 | static defaultProps = { 7 | domain: null 8 | } 9 | 10 | componentWillMount() { 11 | const { onFetchDomain, match } = this.props 12 | onFetchDomain(match.params.id) 13 | } 14 | 15 | render() { 16 | const { domain, isLoading } = this.props 17 | 18 | return ( 19 |
20 |

{t('domain_detail_page.title')}

21 | 22 | {isLoading ? ( 23 | t('global.loading') 24 | ) : ( 25 | 26 | {t('global.domain')}:{' '} 27 | {domain ? domain.param : t('domain_detail_page.no_domain')} 28 | 29 | )} 30 |
31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Switch, Route, Redirect } from 'react-router-dom' 3 | 4 | import { locations } from 'locations' 5 | 6 | import Page from 'components/Page' 7 | import HomePage from 'components/HomePage' 8 | import DomainDetailPage from 'components/DomainDetailPage' 9 | import DomainsPage from 'components/DomainsPage' 10 | 11 | export default class Routes extends React.Component { 12 | renderRoutes() { 13 | return ( 14 | 15 | 16 | 21 | 26 | 27 | 28 | ) 29 | } 30 | 31 | render() { 32 | return {this.renderRoutes()} 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webapp/src/components/DomainDetailPage/DomainDetailPage.container.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { RootDispatch } from '@dapps/types' 3 | import { RootState } from 'types' 4 | import { fetchDomainRequest } from 'modules/domain/actions' 5 | import { getData, isLoading } from 'modules/domain/selectors' 6 | import { 7 | Props, 8 | MapStateProps, 9 | MapDispatchProps 10 | } from 'components/DomainDetailPage/DomainDetailPage.types' 11 | 12 | import DomainDetailPage from './DomainDetailPage' 13 | 14 | const mapState = (state: RootState, ownProps: Props): MapStateProps => { 15 | const match = ownProps.match 16 | const domains = getData(state) 17 | const domainId = match.params.id 18 | 19 | return { 20 | domain: domains[domainId], 21 | isLoading: isLoading(state), 22 | match 23 | } 24 | } 25 | 26 | const mapDispatch = (dispatch: RootDispatch): MapDispatchProps => ({ 27 | onFetchDomain: (id: string) => dispatch(fetchDomainRequest(id)) 28 | }) 29 | 30 | export default connect(mapState, mapDispatch)(DomainDetailPage) 31 | -------------------------------------------------------------------------------- /specs/specs_setup.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import { env } from 'decentraland-commons' 3 | import { omitProps } from './utils' 4 | 5 | const Assertion = (chai as any).Assertion // necessary because @types/chai doesn't export chai.Assertion yet 6 | 7 | chai.use(require('chai-as-promised')) 8 | 9 | env.load({ path: './specs/.env' }) 10 | 11 | Assertion.addChainableMethod('equalRow', function(expectedRow: any) { 12 | const omittedProps = ['created_at', 'updated_at'] 13 | 14 | if (!expectedRow.id) { 15 | omittedProps.push('id') 16 | } 17 | const actualRow = omitProps(this._obj, omittedProps) 18 | 19 | return new Assertion(expectedRow).to.deep.equal(actualRow) 20 | }) 21 | 22 | Assertion.addChainableMethod('equalRows', function(expectedRows: any[]) { 23 | const omittedProps = ['created_at', 'updated_at'] 24 | 25 | if (expectedRows.every(row => !row.id)) { 26 | omittedProps.push('id') 27 | } 28 | 29 | const actualRows = this._obj.map(_obj => omitProps(_obj, omittedProps)) 30 | 31 | return new Assertion(expectedRows).to.deep.equal(actualRows) 32 | }) 33 | -------------------------------------------------------------------------------- /src/Domain/Domain.router.ts: -------------------------------------------------------------------------------- 1 | import { server } from 'decentraland-server' 2 | import { utils } from 'decentraland-commons' 3 | import * as express from 'express' 4 | 5 | import { Router, blacklist } from '../lib' 6 | import { Domain } from './Domain.model' 7 | import { DomainAttributes } from './Domain.types' 8 | 9 | export class DomainRouter extends Router { 10 | mount() { 11 | /** 12 | * Returns all domains 13 | * @return {array} 14 | */ 15 | this.app.get('/domains', server.handleRequest(this.getDomains)) 16 | 17 | /** 18 | * Returns the domains for a given param 19 | * @param {string} param 20 | * @return {array} 21 | */ 22 | this.app.get('/domains/:id', server.handleRequest(this.getDomain)) 23 | } 24 | 25 | async getDomains(): Promise { 26 | const domains = await Domain.find() 27 | return utils.mapOmit(domains, blacklist.domain) 28 | } 29 | 30 | async getDomain(req: express.Request): Promise { 31 | let id = server.extractFromReq(req, 'id') 32 | return Domain.findOne(id) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webapp/src/modules/domain/sagas.ts: -------------------------------------------------------------------------------- 1 | import { call, put, takeLatest } from 'redux-saga/effects' 2 | import { 3 | FETCH_DOMAINS_REQUEST, 4 | FETCH_DOMAIN_REQUEST, 5 | fetchDomainsSuccess, 6 | fetchDomainsFailure, 7 | fetchDomainFailure, 8 | fetchDomainSuccess, 9 | FetchDomainRequestAction 10 | } from 'modules/domain/actions' 11 | import { api } from 'lib/api' 12 | 13 | export function* domainSaga() { 14 | yield takeLatest(FETCH_DOMAINS_REQUEST, handleDomainsRequest) 15 | yield takeLatest(FETCH_DOMAIN_REQUEST, handleDomainRequest) 16 | } 17 | 18 | function* handleDomainsRequest() { 19 | try { 20 | const domains = yield call(() => api.fetchDomains()) 21 | yield put(fetchDomainsSuccess(domains)) 22 | } catch (error) { 23 | yield put(fetchDomainsFailure(error.message)) 24 | } 25 | } 26 | 27 | function* handleDomainRequest(action: FetchDomainRequestAction) { 28 | const id = action.payload.id 29 | try { 30 | const domain = yield call(() => api.fetchDomain(id)) 31 | if (!domain) throw new Error(`Couldn't find domain ${id}`) 32 | 33 | yield put(fetchDomainSuccess(domain)) 34 | } catch (error) { 35 | yield put(fetchDomainFailure(error.message)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Domain/Domain.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { db } from '../database' 3 | import { Domain } from './Domain.model' 4 | 5 | describe('Domain', function() { 6 | describe('.findByParam', function() { 7 | it('should only return enabled districts', async function() { 8 | const paramName = 'Param Name' 9 | const now = new Date() 10 | 11 | await Promise.all([ 12 | Domain.create({ 13 | id: 1, 14 | param: paramName, 15 | created_at: now, 16 | updated_at: now 17 | }), 18 | Domain.create({ 19 | id: 2, 20 | param: 'Disabled', 21 | created_at: now, 22 | updated_at: now 23 | }), 24 | Domain.create({ 25 | id: 3, 26 | param: 'Param 2', 27 | created_at: now, 28 | updated_at: now 29 | }) 30 | ]) 31 | 32 | const domains = await Domain.findByParam(paramName) 33 | 34 | expect(domains).to.be.deep.equal({ 35 | id: 1, 36 | param: paramName, 37 | created_at: now, 38 | updated_at: now 39 | }) 40 | }) 41 | }) 42 | 43 | afterEach(() => db.truncate(Domain.tableName)) 44 | }) 45 | -------------------------------------------------------------------------------- /webapp/src/components/DomainsPage/DomainsPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { t } from '@dapps/modules/translation/utils' 3 | import { Props } from 'components/DomainsPage/DomainsPage.types' 4 | 5 | export default class DomainDetailPage extends React.PureComponent { 6 | static defaultProps = { 7 | domains: null 8 | } 9 | 10 | componentWillMount() { 11 | this.props.onFetchDomains() 12 | } 13 | 14 | areDomainsEmpty() { 15 | const { domains } = this.props 16 | return !domains || Object.keys(domains).length <= 0 17 | } 18 | 19 | renderDomains() { 20 | const { domains } = this.props 21 | if (!domains) return null 22 | 23 | return Object.values(domains).map(domain => ( 24 |
25 | {t('global.domain')}: "{domain.param}" 26 |
27 | )) 28 | } 29 | 30 | render() { 31 | const { isLoading } = this.props 32 | 33 | return ( 34 |
35 |

{t('domains_page.title')}

36 | 37 | {isLoading 38 | ? t('global.loading') 39 | : this.areDomainsEmpty() 40 | ? t('domains_page.empty_domains') 41 | : this.renderDomains()} 42 |
43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "build/dist", 5 | "module": "commonjs", 6 | "target": "es6", 7 | "lib": ["es2017", "dom"], 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "rootDir": "src", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedParameters": true, 20 | "noUnusedLocals": true, 21 | "plugins": [{ "name": "tslint-language-service" }], 22 | "paths": { 23 | "@dapps/*": ["./node_modules/decentraland-dapps/dist/*"], 24 | "components/*": ["./src/components/*"], 25 | "lib/*": ["./src/lib/*"], 26 | "modules/*": ["./src/modules/*"], 27 | "themes/*": ["./src/themes/*"], 28 | "reducer": ["./src/reducer"], 29 | "types": ["./src/types"], 30 | "store": ["./src/store"], 31 | "locations": ["./src/locations"], 32 | "contracts": ["./src/contracts"], 33 | "translations": ["./src/translations"] 34 | } 35 | }, 36 | "exclude": [ 37 | "node_modules", 38 | "build", 39 | "scripts", 40 | "acceptance-tests", 41 | "webpack", 42 | "jest", 43 | "src/setupTests.ts" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /webapp/src/modules/domain/actions.ts: -------------------------------------------------------------------------------- 1 | import { action } from 'typesafe-actions' 2 | import { Domain } from 'modules/domain/types' 3 | 4 | // Fetch domains 5 | 6 | export const FETCH_DOMAINS_REQUEST = '[Request] Fetch Domains' 7 | export const FETCH_DOMAINS_SUCCESS = '[Success] Fetch Domains' 8 | export const FETCH_DOMAINS_FAILURE = '[Failure] Fetch Domains' 9 | 10 | export const fetchDomainsRequest = () => action(FETCH_DOMAINS_REQUEST) 11 | export const fetchDomainsSuccess = (domains: Domain[]) => 12 | action(FETCH_DOMAINS_SUCCESS, { domains }) 13 | export const fetchDomainsFailure = (error: string) => 14 | action(FETCH_DOMAINS_FAILURE, { error }) 15 | 16 | export type FetchDomainsRequestAction = ReturnType 17 | export type FetchDomainsSuccessAction = ReturnType 18 | export type FetchDomainsFailureAction = ReturnType 19 | 20 | // Fetch domain 21 | 22 | export const FETCH_DOMAIN_REQUEST = '[Request] Fetch Domain' 23 | export const FETCH_DOMAIN_SUCCESS = '[Success] Fetch Domain' 24 | export const FETCH_DOMAIN_FAILURE = '[Failure] Fetch Domain' 25 | 26 | export const fetchDomainRequest = (id: string) => 27 | action(FETCH_DOMAIN_REQUEST, { id }) 28 | export const fetchDomainSuccess = (domain: Domain) => 29 | action(FETCH_DOMAIN_SUCCESS, { domain }) 30 | export const fetchDomainFailure = (error: string) => 31 | action(FETCH_DOMAIN_FAILURE, { error }) 32 | 33 | export type FetchDomainRequestAction = ReturnType 34 | export type FetchDomainSuccessAction = ReturnType 35 | export type FetchDomainFailureAction = ReturnType 36 | -------------------------------------------------------------------------------- /scripts/migrate.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | 3 | import * as path from 'path' 4 | import { spawn } from 'child_process' 5 | import { env } from 'decentraland-commons' 6 | import { loadEnv, resolvePath } from './utils' 7 | 8 | export function migrate( 9 | commandArguments: string[], 10 | migrationsDir: string = __dirname 11 | ) { 12 | let CONNECTION_STRING = process.env.CONNECTION_STRING 13 | 14 | if (!CONNECTION_STRING) { 15 | loadEnv() 16 | 17 | CONNECTION_STRING = env.get('CONNECTION_STRING') 18 | if (!CONNECTION_STRING) { 19 | throw new Error( 20 | 'Please set a CONNECTION_STRING env variable before running migrations' 21 | ) 22 | } 23 | } 24 | 25 | const spawnArgs = [ 26 | '--database-url-var', 27 | 'CONNECTION_STRING', 28 | '--migration-file-language', 29 | 'ts', 30 | '--migrations-dir', 31 | migrationsDir, 32 | '--ignore-pattern', 33 | '\\..*|.*migrate', 34 | ...commandArguments 35 | ] 36 | const child = spawn(resolvePath(__dirname + '/node-pg-migrate'), spawnArgs, { 37 | env: { ...process.env, CONNECTION_STRING } 38 | }) 39 | 40 | console.log('Running command:') 41 | console.dir(`node-pg-migrate ${spawnArgs.join(' ')}`) 42 | 43 | child.on('error', function(error) { 44 | console.log(error.message) 45 | }) 46 | 47 | child.stdout.on('data', function(data) { 48 | process.stdout.write(data.toString()) 49 | }) 50 | 51 | child.stderr.on('data', function(data) { 52 | process.stdout.write(data.toString()) 53 | }) 54 | 55 | return child 56 | } 57 | 58 | if (require.main === module) { 59 | migrate(process.argv.slice(2), path.resolve(__dirname, '../migrations')) 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decentraland Dapps Boilerplate 2 | 3 | ## How to use 4 | 5 | ```bash 6 | # clone the repo 7 | # if you're building a pure dapp you can remove everything except the webapp/ folder 8 | 9 | $ npm install # in both src/ and webapp/ folders 10 | 11 | # create and fill .env files variables. Example below, based on the .env.example files 12 | 13 | # If you're using the server 14 | # create a pg database 15 | $ createuser dapp_user 16 | $ createdb -O dapp_user super_dapp 17 | $ npm run migrate up 18 | 19 | $ npm start # in both src/ and webapp/ folders 20 | ``` 21 | 22 | ## ENV example 23 | 24 | **src/** 25 | 26 | ``` 27 | NODE_ENV=development 28 | 29 | SERVER_PORT=5000 30 | 31 | CONNECTION_STRING="postgres://localhost:5432/super_app" 32 | 33 | # Ropsten 34 | MANA_TOKEN_CONTRACT_ADDRESS=0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb 35 | LAND_REGISTRY_CONTRACT_ADDRESS=0x7a73483784ab79257bb11b96fd62a2c3ae4fb75b 36 | 37 | # Mainnet 38 | # LAND_REGISTRY_CONTRACT_ADDRESS=0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d 39 | # MANA_TOKEN_CONTRACT_ADDRESS=0x0f5d2fb29fb7d3cfee444a200298f468908cc942 40 | ``` 41 | 42 | **webapp/** 43 | 44 | ``` 45 | NODE_PATH=src/ 46 | NODE_ENV=development 47 | 48 | REACT_APP_PROVIDER_URL=https://ropsten.infura.io/ 49 | 50 | REACT_APP_API_URL=http://localhost:5000 51 | 52 | # Ropsten 53 | REACT_APP_LAND_REGISTRY_CONTRACT_ADDRESS=0x7a73483784ab79257bb11b96fd62a2c3ae4fb75b 54 | REACT_APP_MANA_TOKEN_CONTRACT_ADDRESS=0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb 55 | 56 | # Mainnet 57 | # REACT_APP_LAND_REGISTRY_CONTRACT_ADDRESS=0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d 58 | # REACT_APP_MANA_TOKEN_CONTRACT_ADDRESS=0x0f5d2fb29fb7d3cfee444a200298f468908cc942 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /webapp/src/store.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'decentraland-commons' 2 | import { routerMiddleware } from 'react-router-redux' 3 | import { applyMiddleware, compose, createStore } from 'redux' 4 | import { createLogger } from 'redux-logger' 5 | import createHistory from 'history/createBrowserHistory' 6 | import createSagasMiddleware from 'redux-saga' 7 | import { createStorageMiddleware } from '@dapps/modules/storage/middleware' 8 | 9 | import { createTransactionMiddleware } from '@dapps/modules/transaction/middleware' 10 | import { rootReducer } from './reducer' 11 | import { rootSaga } from './sagas' 12 | 13 | const composeEnhancers = 14 | (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 15 | 16 | const history = createHistory() 17 | 18 | const historyMiddleware = routerMiddleware(history) 19 | const sagasMiddleware = createSagasMiddleware() 20 | 21 | const { storageMiddleware, loadStorageMiddleware } = createStorageMiddleware( 22 | 'dapp-boilerplate-storage-key' 23 | ) 24 | 25 | const loggerMiddleware = createLogger({ 26 | collapsed: () => true, 27 | predicate: (_: any, action) => 28 | env.isDevelopment() || action.type.includes('Failure') 29 | }) 30 | const transactionMiddleware = createTransactionMiddleware() 31 | 32 | const middleware = applyMiddleware( 33 | historyMiddleware, 34 | sagasMiddleware, 35 | loggerMiddleware, 36 | transactionMiddleware, 37 | storageMiddleware 38 | ) 39 | const enhancer = composeEnhancers(middleware) 40 | const store = createStore(rootReducer, enhancer) 41 | 42 | sagasMiddleware.run(rootSaga) 43 | loadStorageMiddleware(store) 44 | 45 | if (env.isDevelopment()) { 46 | const _window = window as any 47 | _window.getState = store.getState 48 | } 49 | 50 | export { history, store } 51 | -------------------------------------------------------------------------------- /webapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /webapp/src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse, AxiosError, AxiosRequestConfig } from 'axios' 2 | import { env } from 'decentraland-commons' 3 | 4 | const httpClient = axios.create() 5 | const URL = env.get('REACT_APP_API_URL', '') 6 | 7 | export interface APIParam { 8 | [key: string]: any 9 | } 10 | interface Response { 11 | ok: boolean 12 | data: any 13 | error: string 14 | } 15 | 16 | export class API { 17 | fetchDomains() { 18 | return this.request('get', '/domains', {}) 19 | } 20 | 21 | fetchDomain(id: string) { 22 | return this.request('get', `/domains/${id}`, {}) 23 | } 24 | 25 | fetchTranslation(locale: string) { 26 | return this.request('get', `/translations/${locale}`, {}) 27 | } 28 | 29 | request(method: string, path: string, params?: APIParam) { 30 | let options: AxiosRequestConfig = { 31 | method, 32 | url: this.getUrl(path) 33 | } 34 | 35 | if (params) { 36 | if (method === 'get') { 37 | options.params = params 38 | } else { 39 | options.data = params 40 | } 41 | } 42 | 43 | return httpClient 44 | .request(options) 45 | .then((response: AxiosResponse) => { 46 | const data = response.data 47 | const result = data.data // One for axios data, another for the servers data 48 | 49 | return data && !data.ok 50 | ? Promise.reject({ message: data.error, data: result }) 51 | : result 52 | }) 53 | .catch((error: AxiosError) => { 54 | console.warn(`[API] HTTP request failed: ${error.message || ''}`, error) 55 | return Promise.reject(error) 56 | }) 57 | } 58 | 59 | getUrl(path: string) { 60 | return `${URL}/v1${path}` 61 | } 62 | } 63 | 64 | export const api = new API() 65 | -------------------------------------------------------------------------------- /src/Translation/Translation.ts: -------------------------------------------------------------------------------- 1 | import { env, utils } from 'decentraland-commons' 2 | import * as flat from 'flat' 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | 6 | export interface TranslationData { 7 | [key: string]: string 8 | } 9 | export interface TranslationCache { 10 | locale?: TranslationData 11 | } 12 | 13 | export class Translation { 14 | static DEFAULT_LOCALE = 'en' 15 | 16 | localesPath: string 17 | cache: TranslationCache 18 | 19 | constructor() { 20 | this.localesPath = env.get( 21 | 'LOCALES_PATH', 22 | path.resolve(__dirname, './locales') 23 | ) 24 | this.cache = {} // {locale: translations} 25 | } 26 | 27 | async fetch(locale: string): Promise { 28 | if (!this.cache[locale]) { 29 | const availableLocales = await this.getAvailableLocales() 30 | 31 | if (availableLocales.includes(locale)) { 32 | this.cache[locale] = this.parse(await this.readFile(locale)) 33 | } 34 | } 35 | 36 | return this.cache[locale] || {} 37 | } 38 | 39 | async getAvailableLocales(): Promise { 40 | const files = await utils.promisify(fs.readdir)(this.localesPath) 41 | return files.map(filePath => 42 | path.basename(filePath, path.extname(filePath)) 43 | ) 44 | } 45 | 46 | parse(fileContents: string): TranslationData { 47 | // The translation lib ( https://github.com/yahoo/react-intl ) doesn't support nested values 48 | // So instead we flatten the structure to look like `{ 'nested.prop': 'value' }` 49 | const translations = JSON.parse(fileContents) 50 | return flat(translations) 51 | } 52 | 53 | async readFile(locale: string): Promise { 54 | return utils.promisify(fs.readFile)( 55 | path.resolve(this.localesPath, `${locale}.json`), 56 | 'utf8' 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser' 2 | import { env } from 'decentraland-commons' 3 | import { contracts, eth } from 'decentraland-eth' 4 | import * as express from 'express' 5 | import { DomainRouter } from './Domain' 6 | import { TranslationRouter } from './Translation' 7 | import { db } from './database' 8 | 9 | env.load() 10 | 11 | const SERVER_PORT = env.get('SERVER_PORT', 5000) 12 | 13 | const app = express() 14 | 15 | app.use(bodyParser.urlencoded({ extended: false, limit: '2mb' })) 16 | app.use(bodyParser.json()) 17 | 18 | if (env.isDevelopment()) { 19 | app.use(function(_, res, next) { 20 | res.setHeader('Access-Control-Allow-Origin', '*') 21 | res.setHeader('Access-Control-Request-Method', '*') 22 | res.setHeader( 23 | 'Access-Control-Allow-Methods', 24 | 'OPTIONS, GET, POST, PUT, DELETE' 25 | ) 26 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type') 27 | 28 | next() 29 | }) 30 | } 31 | 32 | const router = express.Router() 33 | app.use('/v1', router) 34 | 35 | new DomainRouter(router).mount() 36 | new TranslationRouter(router).mount() 37 | 38 | /* Start the server only if run directly */ 39 | if (require.main === module) { 40 | startServer().catch(console.error) 41 | } 42 | 43 | async function startServer() { 44 | console.log('Connecting database') 45 | await db.connect() 46 | 47 | console.log('Connecting to Ethereum node') 48 | await eth 49 | .connect({ 50 | contracts: [ 51 | new contracts.LANDRegistry(env.get('LAND_REGISTRY_CONTRACT_ADDRESS')) // Example use 52 | ], 53 | provider: env.get('RPC_URL') // defaults to localhost 54 | }) 55 | .catch(error => 56 | console.error( 57 | '\nCould not connect to the Ethereum node. Some endpoints may not work correctly.', 58 | '\nMake sure you have a node running on port 8545.', 59 | `\nError: "${error.message}"\n` 60 | ) 61 | ) 62 | 63 | return app.listen(SERVER_PORT, () => 64 | console.log('Server running on port', SERVER_PORT) 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /webapp/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right