├── 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