├── .ci ├── .gitignore ├── Pulumi.yaml ├── .env.dev ├── .env.prd ├── .env.stg ├── package.json ├── tsconfig.json ├── Pulumi.website-agora-dev.yaml ├── Pulumi.website-agora-prd.yaml ├── Pulumi.website-agora-stg.yaml └── index.ts ├── .prettierignore ├── src ├── App │ ├── index.ts │ └── App.router.ts ├── Token │ ├── DistrictToken │ │ ├── index.ts │ │ ├── DistrictToken.types.ts │ │ └── DistrictToken.ts │ ├── Token.types.ts │ ├── index.ts │ ├── Token.spec.ts │ ├── Token.model.ts │ └── Token.router.ts ├── database │ ├── index.ts │ └── database.ts ├── tsconfig.json ├── Translation │ ├── index.ts │ ├── Translation.router.ts │ ├── Translation.spec.ts │ ├── locales │ │ ├── zh.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── en.json │ │ ├── fr.json │ │ └── es.json │ └── Translation.ts ├── Option │ ├── index.ts │ ├── Option.types.ts │ ├── Option.model.ts │ ├── Option.spec.ts │ └── Option.router.ts ├── Receipt │ ├── index.ts │ ├── Receipt.types.ts │ ├── Receipt.router.ts │ └── Receipt.model.ts ├── Poll │ ├── index.ts │ ├── Poll.spec.ts │ ├── Poll.types.ts │ ├── Poll.router.ts │ ├── PollRequestFilters.ts │ ├── Poll.queries.ts │ └── Poll.model.ts ├── Vote │ ├── index.ts │ ├── Vote.spec.ts │ ├── Vote.queries.ts │ ├── Vote.types.ts │ ├── Vote.model.ts │ └── Vote.router.ts ├── AccountBalance │ ├── index.ts │ ├── AccountBalance.types.ts │ ├── AccountBalance.spec.ts │ ├── AccountBalance.model.ts │ └── AccountBalance.router.ts ├── lib │ ├── types.ts │ ├── blacklist.ts │ ├── index.ts │ ├── Router.ts │ ├── Model.queries.ts │ ├── ModelWithCallbacks.ts │ └── extractFromReq.ts ├── .env.example └── server.ts ├── specs ├── .env.example ├── setup.ts ├── specs_setup.ts └── utils.ts ├── webapp ├── tsconfig.prod.json ├── src │ ├── modules.d.ts │ ├── components │ │ ├── Token │ │ │ ├── index.ts │ │ │ ├── Token.types.ts │ │ │ ├── Token.css │ │ │ └── Token.tsx │ │ ├── Hero │ │ │ ├── index.ts │ │ │ ├── Hero.css │ │ │ └── Hero.tsx │ │ ├── VotePage │ │ │ ├── VoteLabel.css │ │ │ ├── index.ts │ │ │ ├── VoteLabel.tsx │ │ │ ├── VotePage.types.ts │ │ │ ├── VotePage.css │ │ │ └── VotePage.container.ts │ │ ├── HomePage │ │ │ ├── PollCards │ │ │ │ ├── index.ts │ │ │ │ ├── PollCards.types.ts │ │ │ │ ├── PollCards.css │ │ │ │ └── PollCards.tsx │ │ │ ├── index.ts │ │ │ ├── HomePage.types.ts │ │ │ ├── HomePage.css │ │ │ ├── HomePage.container.ts │ │ │ └── HomePage.tsx │ │ ├── PollDetailPage │ │ │ ├── OptionOrb │ │ │ │ ├── OptionOrb.types.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── OptionOrb.tsx │ │ │ │ └── OptionOrb.css │ │ │ ├── YourVote │ │ │ │ ├── index.ts │ │ │ │ ├── YourVote.types.ts │ │ │ │ ├── YourVote.css │ │ │ │ └── YourVote.tsx │ │ │ ├── OptionBar │ │ │ │ ├── index.ts │ │ │ │ ├── OptionBar.types.ts │ │ │ │ ├── OptionBar.tsx │ │ │ │ └── OptionBar.css │ │ │ ├── PollRanking │ │ │ │ ├── index.ts │ │ │ │ ├── PollRanking.types.ts │ │ │ │ ├── PollRanking.css │ │ │ │ └── PollRanking.tsx │ │ │ ├── CastYourVote │ │ │ │ ├── index.ts │ │ │ │ ├── CastYourVote.types.ts │ │ │ │ ├── CastYourVote.css │ │ │ │ └── CastYourVote.tsx │ │ │ ├── PollProgress │ │ │ │ ├── index.ts │ │ │ │ ├── PollOption │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── PollOptions.types.ts │ │ │ │ │ └── PollOption.tsx │ │ │ │ ├── PollProgress.types.ts │ │ │ │ ├── PollProgress.tsx │ │ │ │ └── PollProgress.css │ │ │ ├── index.ts │ │ │ ├── PollDetailPage.types.ts │ │ │ ├── PollDetailPage.container.ts │ │ │ └── PollDetailPage.css │ │ └── PollsTable │ │ │ ├── index.ts │ │ │ ├── PollsTable.types.ts │ │ │ ├── PollsTable.css │ │ │ └── PollsTable.container.ts │ ├── modules │ │ ├── option │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ ├── selectors.ts │ │ │ ├── sagas.ts │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ │ ├── accountBalance │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ ├── selectors.ts │ │ │ ├── sagas.ts │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ │ ├── wallet │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ ├── actions.ts │ │ │ └── sagas.ts │ │ ├── ui │ │ │ ├── table │ │ │ │ ├── types.ts │ │ │ │ └── reducer.ts │ │ │ ├── reducer.ts │ │ │ ├── districtPolls │ │ │ │ ├── selectors.ts │ │ │ │ └── reducer.ts │ │ │ ├── decentralandPolls │ │ │ │ ├── selectors.ts │ │ │ │ └── reducer.ts │ │ │ └── polls │ │ │ │ ├── reducer.ts │ │ │ │ └── selectors.ts │ │ ├── translation │ │ │ └── sagas.ts │ │ ├── token │ │ │ ├── types.ts │ │ │ ├── district_token │ │ │ │ └── utils.ts │ │ │ ├── actions.ts │ │ │ ├── sagas.ts │ │ │ ├── selectors.ts │ │ │ └── reducer.ts │ │ ├── vote │ │ │ ├── utils.ts │ │ │ ├── types.ts │ │ │ ├── selectors.ts │ │ │ ├── actions.ts │ │ │ ├── sagas.ts │ │ │ └── reducer.ts │ │ └── poll │ │ │ ├── utils.ts │ │ │ ├── types.ts │ │ │ ├── sagas.ts │ │ │ ├── selectors.ts │ │ │ └── actions.ts │ ├── index.css │ ├── contracts.ts │ ├── assets │ │ └── pyramid.svg │ ├── analytics.ts │ ├── locations.ts │ ├── sagas.ts │ ├── index.tsx │ ├── lib │ │ └── api.ts │ ├── reducer.ts │ ├── types.ts │ ├── store.ts │ ├── Routes.tsx │ └── logo.svg ├── images.d.ts ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── tsconfig.test.json ├── config │ ├── jest │ │ ├── typescriptTransform.js │ │ ├── fileTransform.js │ │ └── cssTransform.js │ ├── polyfills.js │ └── paths.js ├── .env.example ├── .gitignore ├── tslint.json ├── scripts │ └── test.js └── tsconfig.json ├── entrypoint.sh ├── scripts ├── tsconfig.json ├── node-pg-migrate ├── utils.ts ├── migrate.ts ├── seed.ts └── initDb.ts ├── migrations ├── 1527706973000_uuid-extension-create.ts ├── 1530201653559_votes-add-timestamp.ts ├── 1527706973005_tokens-create.ts ├── 1527707071189_options-create.ts ├── 1527707064752_accounts-create.ts ├── 1527706985005_polls-create.ts ├── 1529057165916_receipt-create.ts └── 1527707259148_votes-create.ts ├── .vscode └── settings.json ├── .gitignore ├── tslint.json ├── etc └── supervisor │ ├── monitor.conf │ └── app.conf ├── Dockerfile ├── LICENSE ├── tsconfig.json ├── .gitlab-ci.yml └── package.json /.ci/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json -------------------------------------------------------------------------------- /src/App/index.ts: -------------------------------------------------------------------------------- 1 | export * from './App.router' 2 | -------------------------------------------------------------------------------- /specs/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | CONNECTION_STRING= 3 | -------------------------------------------------------------------------------- /src/Token/DistrictToken/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DistrictToken' 2 | -------------------------------------------------------------------------------- /webapp/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | export { database as db } from './database' 2 | -------------------------------------------------------------------------------- /webapp/src/modules.d.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | declare module 'react-linkify' 3 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm run build:tsc || exit 1 4 | npm run start || exit 1 5 | -------------------------------------------------------------------------------- /webapp/src/components/Token/index.ts: -------------------------------------------------------------------------------- 1 | import Token from './Token' 2 | export default Token 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/Hero/index.ts: -------------------------------------------------------------------------------- 1 | import Hero from './Hero' 2 | 3 | export default Hero 4 | -------------------------------------------------------------------------------- /.ci/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: webiste-agora 2 | runtime: nodejs 3 | description: Agora UI + DB + Server 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/components/VotePage/VoteLabel.css: -------------------------------------------------------------------------------- 1 | .VoteLabel span { 2 | color: var(--primary); 3 | } 4 | -------------------------------------------------------------------------------- /webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/agora/HEAD/webapp/public/favicon.ico -------------------------------------------------------------------------------- /webapp/src/components/HomePage/PollCards/index.ts: -------------------------------------------------------------------------------- 1 | import PollCards from './PollCards' 2 | export default PollCards 3 | -------------------------------------------------------------------------------- /specs/setup.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../src/database' 2 | 3 | before(() => db.connect()) 4 | after(() => db.close()) 5 | -------------------------------------------------------------------------------- /webapp/src/components/HomePage/index.ts: -------------------------------------------------------------------------------- 1 | import HomePage from './HomePage.container' 2 | 3 | export default HomePage 4 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/OptionOrb/OptionOrb.types.ts: -------------------------------------------------------------------------------- 1 | export type Props = { 2 | position: number 3 | } 4 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/YourVote/index.ts: -------------------------------------------------------------------------------- 1 | import YourVote from './YourVote' 2 | export default YourVote 3 | -------------------------------------------------------------------------------- /webapp/src/components/VotePage/index.ts: -------------------------------------------------------------------------------- 1 | import VotePage from './VotePage.container' 2 | 3 | export default VotePage 4 | -------------------------------------------------------------------------------- /src/Option/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Option.model' 2 | export * from './Option.router' 3 | export * from './Option.types' 4 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/OptionBar/index.ts: -------------------------------------------------------------------------------- 1 | import OptionBar from './OptionBar' 2 | export default OptionBar 3 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/OptionOrb/index.tsx: -------------------------------------------------------------------------------- 1 | import OptionOrb from './OptionOrb' 2 | export default OptionOrb 3 | -------------------------------------------------------------------------------- /webapp/src/components/PollsTable/index.ts: -------------------------------------------------------------------------------- 1 | import PollsTable from './PollsTable.container' 2 | 3 | export default PollsTable 4 | -------------------------------------------------------------------------------- /src/Receipt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Receipt.model' 2 | export * from './Receipt.router' 3 | export * from './Receipt.types' 4 | -------------------------------------------------------------------------------- /src/Token/Token.types.ts: -------------------------------------------------------------------------------- 1 | export interface TokenAttributes { 2 | address: string 3 | name: string 4 | symbol: string 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollRanking/index.ts: -------------------------------------------------------------------------------- 1 | import PollRanking from './PollRanking' 2 | export default PollRanking 3 | -------------------------------------------------------------------------------- /webapp/src/modules/option/types.ts: -------------------------------------------------------------------------------- 1 | export interface Option { 2 | id: string 3 | value: string 4 | poll_id: number 5 | } 6 | -------------------------------------------------------------------------------- /webapp/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/CastYourVote/index.ts: -------------------------------------------------------------------------------- 1 | import CastYourVote from './CastYourVote' 2 | export default CastYourVote 3 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollProgress/index.ts: -------------------------------------------------------------------------------- 1 | import PollProgress from './PollProgress' 2 | export default PollProgress 3 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollProgress/PollOption/index.ts: -------------------------------------------------------------------------------- 1 | import PollOption from './PollOption' 2 | export default PollOption 3 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/OptionBar/OptionBar.types.ts: -------------------------------------------------------------------------------- 1 | export type Props = { 2 | position: number 3 | percentage: number 4 | } 5 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/index.ts: -------------------------------------------------------------------------------- 1 | import PollDetailPage from './PollDetailPage.container' 2 | 3 | export default PollDetailPage 4 | -------------------------------------------------------------------------------- /src/Poll/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Poll.model' 2 | export * from './Poll.router' 3 | export * from './Poll.queries' 4 | export * from './Poll.types' 5 | -------------------------------------------------------------------------------- /src/Vote/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Vote.model' 2 | export * from './Vote.router' 3 | export * from './Vote.queries' 4 | export * from './Vote.types' 5 | -------------------------------------------------------------------------------- /src/AccountBalance/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AccountBalance.model' 2 | export * from './AccountBalance.router' 3 | export * from './AccountBalance.types' 4 | -------------------------------------------------------------------------------- /src/Token/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Token.model' 2 | export * from './Token.router' 3 | export * from './Token.types' 4 | 5 | export * from './DistrictToken' 6 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type Omit = Pick> 2 | export type Overwrite = Pick> & T2 3 | -------------------------------------------------------------------------------- /src/Token/DistrictToken/DistrictToken.types.ts: -------------------------------------------------------------------------------- 1 | import { TokenAttributes } from '../Token.types' 2 | 3 | export interface DistrictTokenAttributes extends TokenAttributes {} 4 | -------------------------------------------------------------------------------- /src/lib/blacklist.ts: -------------------------------------------------------------------------------- 1 | const timestamps = ['created_at', 'updated_at'] 2 | 3 | export const blacklist = Object.freeze({ 4 | poll: [...timestamps], 5 | token: [] 6 | }) 7 | -------------------------------------------------------------------------------- /webapp/src/modules/accountBalance/types.ts: -------------------------------------------------------------------------------- 1 | export interface AccountBalance { 2 | id: string 3 | address: string 4 | token_address: string 5 | balance: number 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Option/Option.types.ts: -------------------------------------------------------------------------------- 1 | export interface OptionAttributes { 2 | id?: string 3 | value: string 4 | poll_id: number 5 | created_at?: Date 6 | updated_at?: Date 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './blacklist' 2 | export * from './Model.queries' 3 | export * from './ModelWithCallbacks' 4 | export * from './Router' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /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 | } 14 | -------------------------------------------------------------------------------- /src/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | 3 | SERVER_PORT= 4 | TRANSLATIONS_PATH= 5 | MONITOR_BALANCES_DELAY= 6 | 7 | CONNECTION_STRING= 8 | RPC_URL= 9 | 10 | MANA_TOKEN_CONTRACT_ADDRESS= 11 | -------------------------------------------------------------------------------- /webapp/src/components/Token/Token.types.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'modules/token/types' 2 | 3 | export type Props = { 4 | token: Token 5 | amount?: number 6 | cell?: boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/AccountBalance/AccountBalance.types.ts: -------------------------------------------------------------------------------- 1 | export interface AccountBalanceAttributes { 2 | id?: string 3 | address: string 4 | token_address: string 5 | balance: string // DECIMAL 6 | } 7 | -------------------------------------------------------------------------------- /webapp/src/components/Token/Token.css: -------------------------------------------------------------------------------- 1 | .Token.cell, 2 | .Token.cell.ui.header, 3 | .Token.cell.ui.header.small { 4 | font-size: 14px; 5 | font-weight: normal; 6 | display: inline; 7 | } 8 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollProgress/PollProgress.types.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'components/PollDetailPage/PollDetailPage.types' 2 | 3 | export type Props = { 4 | results: Result[] 5 | } 6 | -------------------------------------------------------------------------------- /.ci/.env.dev: -------------------------------------------------------------------------------- 1 | REACT_APP_MANA_TOKEN_CONTRACT_ADDRESS=0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb 2 | 3 | REACT_APP_API_URL=https://agora-api.decentraland.io 4 | 5 | REACT_APP_LOCAL_STORAGE_KEY=decentraland-agora -------------------------------------------------------------------------------- /.ci/.env.prd: -------------------------------------------------------------------------------- 1 | REACT_APP_MANA_TOKEN_CONTRACT_ADDRESS=0x0f5d2fb29fb7d3cfee444a200298f468908cc942 2 | 3 | REACT_APP_API_URL=https://agora-api.decentraland.org 4 | 5 | REACT_APP_LOCAL_STORAGE_KEY=decentraland-agora -------------------------------------------------------------------------------- /.ci/.env.stg: -------------------------------------------------------------------------------- 1 | REACT_APP_MANA_TOKEN_CONTRACT_ADDRESS=0x0f5d2fb29fb7d3cfee444a200298f468908cc942 2 | 3 | REACT_APP_API_URL=https://agora-api.decentraland.net 4 | 5 | REACT_APP_LOCAL_STORAGE_KEY=decentraland-agora -------------------------------------------------------------------------------- /.ci/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website-agora-build", 3 | "devDependencies": { 4 | "@types/node": "^14.6.2" 5 | }, 6 | "dependencies": { 7 | "dcl-ops-lib": "^4.16.0" 8 | } 9 | } -------------------------------------------------------------------------------- /webapp/src/modules/wallet/types.ts: -------------------------------------------------------------------------------- 1 | import { BaseWallet } from '@dapps/modules/wallet/types' 2 | 3 | export interface Wallet extends BaseWallet { 4 | balances: { 5 | [address: string]: number 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /migrations/1527706973000_uuid-extension-create.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate' 2 | 3 | export const up = (pgm: MigrationBuilder) => { 4 | pgm.createExtension('uuid-ossp', { ifNotExists: true }) 5 | } 6 | -------------------------------------------------------------------------------- /src/Option/Option.model.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'decentraland-server' 2 | import { OptionAttributes } from './Option.types' 3 | 4 | export class Option extends Model { 5 | static tableName = 'options' 6 | } 7 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/CastYourVote/CastYourVote.types.ts: -------------------------------------------------------------------------------- 1 | import { PollWithAssociations } from 'modules/poll/types' 2 | 3 | export type Props = { 4 | poll: PollWithAssociations 5 | isConnected: boolean 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | "typescript.tsdk": "node_modules/typescript/lib" 8 | } 9 | -------------------------------------------------------------------------------- /webapp/src/contracts.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'decentraland-commons' 2 | import { contracts } from 'decentraland-eth' 3 | const MANAToken = new contracts.MANAToken( 4 | env.get('REACT_APP_MANA_TOKEN_CONTRACT_ADDRESS') 5 | ) 6 | export { MANAToken } 7 | -------------------------------------------------------------------------------- /src/Poll/Poll.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { Poll } from './Poll.model' 3 | 4 | describe('Poll', function() { 5 | it('should set the correct table name', function() { 6 | expect(Poll.tableName).to.equal('polls') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /src/Vote/Vote.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { Vote } from './Vote.model' 3 | 4 | describe('Vote', function() { 5 | it('should set the correct table name', function() { 6 | expect(Vote.tableName).to.equal('votes') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /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_LOCAL_STORAGE_KEY= 10 | REACT_APP_SEGMENT_API_KEY= 11 | REACT_APP_MANA_TOKEN_CONTRACT_ADDRESS= 12 | -------------------------------------------------------------------------------- /webapp/src/modules/wallet/utils.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from 'modules/wallet/types' 2 | import { Poll } from 'modules/poll/types' 3 | 4 | export function getBalanceInPoll(wallet: Wallet, poll: Poll) { 5 | return wallet.balances[poll.token_address] 6 | } 7 | -------------------------------------------------------------------------------- /.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* -------------------------------------------------------------------------------- /src/Token/Token.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { Token } from './Token.model' 3 | 4 | describe('Token', function() { 5 | it('should set the correct table name', function() { 6 | expect(Token.tableName).to.equal('tokens') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/YourVote/YourVote.types.ts: -------------------------------------------------------------------------------- 1 | import { Vote } from 'modules/vote/types' 2 | import { PollWithAssociations } from 'modules/poll/types' 3 | 4 | export type Props = { 5 | vote: Vote | null 6 | poll: PollWithAssociations 7 | } 8 | -------------------------------------------------------------------------------- /src/Option/Option.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { Option } from './Option.model' 3 | 4 | describe('Option', function() { 5 | it('should set the correct table name', function() { 6 | expect(Option.tableName).to.equal('options') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollRanking/PollRanking.types.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'components/PollDetailPage/PollDetailPage.types' 2 | 3 | export type Props = { 4 | results: Result[] 5 | } 6 | 7 | export type State = { 8 | activePage: number 9 | } 10 | -------------------------------------------------------------------------------- /webapp/src/modules/ui/table/types.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux' 2 | 3 | export type ArrayGetter = (action: AnyAction) => any[] 4 | export type IdGetter = (arrayElement: any, action: AnyAction) => string 5 | export type TotalGetter = (action: AnyAction) => number 6 | -------------------------------------------------------------------------------- /webapp/src/modules/translation/sagas.ts: -------------------------------------------------------------------------------- 1 | import { createTranslationSaga } from '@dapps/modules/translation/sagas' 2 | import { api } from 'lib/api' 3 | 4 | export const translationSaga = createTranslationSaga({ 5 | getTranslation: locale => api.fetchTranslations(locale) 6 | }) 7 | -------------------------------------------------------------------------------- /src/lib/Router.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express' 2 | 3 | export class Router { 4 | protected app: express.Application 5 | 6 | constructor(app: express.Application) { 7 | this.app = app 8 | } 9 | 10 | mount(): void { 11 | throw new Error('Not implemented') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webapp/src/modules/token/types.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from 'typesafe-actions' 2 | import * as actions from 'modules/token/actions' 3 | 4 | export type TokenActions = ActionType 5 | 6 | export interface Token { 7 | address: string 8 | symbol: string 9 | name: string 10 | } 11 | -------------------------------------------------------------------------------- /webapp/src/modules/accountBalance/utils.ts: -------------------------------------------------------------------------------- 1 | import { AccountBalance } from 'modules/accountBalance/types' 2 | 3 | export function buildId( 4 | accountBalance: AccountBalance | { address: string; token_address: string } 5 | ): string { 6 | return `${accountBalance.address}-${accountBalance.token_address}` 7 | } 8 | -------------------------------------------------------------------------------- /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/components/HomePage/PollCards/PollCards.types.ts: -------------------------------------------------------------------------------- 1 | import { PollWithAssociations } from 'modules/poll/types' 2 | 3 | export type Props = { 4 | polls: PollWithAssociations[] 5 | title: string 6 | meta: string 7 | onClick: (poll: PollWithAssociations) => void 8 | onViewMore: () => void 9 | } 10 | -------------------------------------------------------------------------------- /src/AccountBalance/AccountBalance.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { AccountBalance } from './AccountBalance.model' 3 | 4 | describe('AccountBalance', function() { 5 | it('should set the correct table name', function() { 6 | expect(AccountBalance.tableName).to.equal('account_balances') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /webapp/src/modules/vote/utils.ts: -------------------------------------------------------------------------------- 1 | import { Vote } from 'modules/vote/types' 2 | import { BaseWallet } from '@dapps/modules/wallet/types' 3 | 4 | export function findWalletVote(wallet: BaseWallet, votes: Vote[]): Vote | null { 5 | const vote = votes.find(vote => vote.account_address === wallet.address) 6 | return vote || null 7 | } 8 | -------------------------------------------------------------------------------- /src/Receipt/Receipt.types.ts: -------------------------------------------------------------------------------- 1 | export interface ReceiptAttributes { 2 | id?: string 3 | server_signature: string 4 | server_message: string 5 | account_message: string 6 | account_signature: string 7 | account_address: string 8 | option_value: string 9 | vote_id: string 10 | nonce: number // Added by default by the DB, serial 11 | } 12 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollProgress/PollOption/PollOptions.types.ts: -------------------------------------------------------------------------------- 1 | import { Option } from 'modules/option/types' 2 | import { Token } from 'modules/token/types' 3 | 4 | export type Props = { 5 | option: Option 6 | winner: boolean 7 | percentage: number 8 | position: number 9 | total: number 10 | votes: number 11 | token?: Token 12 | } 13 | -------------------------------------------------------------------------------- /etc/supervisor/monitor.conf: -------------------------------------------------------------------------------- 1 | [program:monitor] 2 | numprocs=1 3 | process_name=monitor 4 | command=npm run monitor-balances 5 | autostart=true 6 | autorestart=true 7 | environment=NODE_ENV="production" 8 | directory=/home/ubuntu/app 9 | stderr_logfile=/var/log/monitor.err.log 10 | stdout_logfile=/var/log/monitor.out.log 11 | user=ubuntu 12 | stopasgroup=true 13 | stopsignal=KILL -------------------------------------------------------------------------------- /webapp/src/assets/pyramid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Token/Token.model.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'decentraland-server' 2 | import { TokenAttributes } from './Token.types' 3 | 4 | // If the Token model starts to receive external inserts, we should lowercase the address 5 | export class Token extends Model { 6 | static tableName = 'tokens' 7 | static primaryKey = 'address' 8 | static withTimestamps = false 9 | } 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/App/App.router.ts: -------------------------------------------------------------------------------- 1 | import { server } from 'decentraland-server' 2 | import * as express from 'express' 3 | 4 | import { Router } from '../lib' 5 | 6 | export class AppRouter extends Router { 7 | mount() { 8 | this.app.get('/status', server.handleRequest(this.getAppStatus)) 9 | } 10 | 11 | async getAppStatus(_: express.Request) { 12 | return { status: 200 } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /migrations/1530201653559_votes-add-timestamp.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate' 2 | import { Vote } from '../src/Vote' 3 | 4 | const tableName = Vote.tableName 5 | 6 | export const up = (pgm: MigrationBuilder) => { 7 | pgm.addColumns(tableName, { 8 | timestamp: { 9 | type: 'DECIMAL', 10 | notNull: true, 11 | comment: null 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /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/src/analytics.ts: -------------------------------------------------------------------------------- 1 | import { add } from '@dapps/modules/analytics/utils' 2 | import { 3 | CREATE_VOTE_SUCCESS, 4 | CreateVoteSuccessAction 5 | } from 'modules/vote/actions' 6 | 7 | add(CREATE_VOTE_SUCCESS, 'Vote', (action: CreateVoteSuccessAction) => ({ 8 | poll_id: action.payload.vote.poll_id, 9 | option_id: action.payload.vote.option_id, 10 | address: action.payload.wallet.address 11 | })) 12 | -------------------------------------------------------------------------------- /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', undefined) 10 | this.client = await pg.connect(CONNECTION_STRING) 11 | return this 12 | } 13 | -------------------------------------------------------------------------------- /webapp/src/modules/option/utils.ts: -------------------------------------------------------------------------------- 1 | import { Option } from 'modules/option/types' 2 | import { Vote } from 'modules/vote/types' 3 | 4 | export function getVoteOptionValue(options: Option[], vote: Vote): string { 5 | // TODO: Consider getting options from the store and accessing via options[option_id] 6 | const option = options.find(option => option.id === vote.option_id) 7 | return option ? option.value : '' 8 | } 9 | -------------------------------------------------------------------------------- /etc/supervisor/app.conf: -------------------------------------------------------------------------------- 1 | [program:app] 2 | numprocs=1 3 | process_name=%(process_num)s 4 | command=npm run start 5 | autostart=true 6 | autorestart=true 7 | environment=NODE_ENV="production";SERVER_PORT=500%(process_num)s 8 | directory=/home/ubuntu/app 9 | stderr_logfile=/var/log/app-%(process_num)s.err.log 10 | stdout_logfile=/var/log/app-%(process_num)s.out.log 11 | user=ubuntu 12 | stopasgroup=true 13 | stopsignal=QUIT 14 | -------------------------------------------------------------------------------- /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/components/PollDetailPage/PollRanking/PollRanking.css: -------------------------------------------------------------------------------- 1 | .PollRanking { 2 | width: 100%; 3 | } 4 | 5 | .PollRanking .ui.table td.ranking { 6 | padding-right: 1em; 7 | } 8 | 9 | .PollRanking .RankingPagination { 10 | text-align: center; 11 | margin-top: 40px; 12 | } 13 | 14 | .PollRanking .ui.table td a { 15 | color: var(--primary); 16 | } 17 | 18 | .PollRanking .ui.table td a:hover { 19 | color: var(--primary-dark); 20 | } 21 | -------------------------------------------------------------------------------- /src/Vote/Vote.queries.ts: -------------------------------------------------------------------------------- 1 | import { SQL, SQLStatement, raw } from 'decentraland-server' 2 | import { Poll } from '../Poll' 3 | 4 | export const VoteQueries = Object.freeze({ 5 | sumAccountBalanceForPollSubquery: ( 6 | tableName: string = Poll.tableName 7 | ): SQLStatement => 8 | // prettier-ignore 9 | SQL`SELECT SUM(account_balance) balance 10 | FROM votes v 11 | WHERE v.poll_id = ${raw(tableName)}.id 12 | GROUP BY v.poll_id` 13 | }) 14 | -------------------------------------------------------------------------------- /webapp/src/modules/vote/types.ts: -------------------------------------------------------------------------------- 1 | import { Overwrite } from '@dapps/lib/types' 2 | 3 | // Interface and type definitions 4 | 5 | export interface Vote { 6 | id: string 7 | account_address: string 8 | account_balance: number 9 | poll_id: string 10 | option_id: string 11 | timestamp: number 12 | message: string 13 | signature: string 14 | } 15 | 16 | export interface NewVote 17 | extends Overwrite {} 18 | -------------------------------------------------------------------------------- /webapp/src/modules/token/district_token/utils.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'modules/token/types' 2 | 3 | const DISTRICT_TOKEN = Object.freeze({ 4 | address: 'district-token-address', 5 | name: 'DistrictToken', 6 | symbol: 'DT' 7 | }) 8 | 9 | export function isDistrictToken(token: Token) { 10 | return isDistrictTokenAddress(token.address) 11 | } 12 | 13 | export function isDistrictTokenAddress(address: string): boolean { 14 | return address.search(DISTRICT_TOKEN.address) !== -1 15 | } 16 | -------------------------------------------------------------------------------- /.ci/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "bin", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "experimentalDecorators": true, 11 | "pretty": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitReturns": true, 14 | "forceConsistentCasingInFileNames": true 15 | }, 16 | "include": ["*.ts", "**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/CastYourVote/CastYourVote.css: -------------------------------------------------------------------------------- 1 | .CastYourVote { 2 | display: flex; 3 | flex-flow: row nowrap; 4 | align-items: center; 5 | margin-left: 80px; 6 | } 7 | 8 | .CastYourVote .disabled { 9 | cursor: not-allowed; 10 | } 11 | 12 | @media (max-width: 768px) { 13 | .CastYourVote { 14 | margin-left: 0px; 15 | margin-top: 48px; 16 | margin-bottom: 48px; 17 | } 18 | 19 | .CastYourVote .ui.button { 20 | height: 48px; 21 | width: 191px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUN 2 | 3 | FROM node:12 as builder 4 | 5 | WORKDIR /app 6 | 7 | COPY package.json /app/package.json 8 | COPY package-lock.json /app/package-lock.json 9 | COPY tsconfig.json /app/tsconfig.json 10 | 11 | RUN apt-get update 12 | RUN apt-get -y -qq install python-setuptools python-dev build-essential 13 | RUN npm install 14 | 15 | COPY . /app 16 | 17 | RUN npm run build 18 | 19 | FROM node:12 20 | 21 | WORKDIR /app 22 | 23 | COPY --from=builder /app /app 24 | 25 | ENTRYPOINT [ "./entrypoint.sh" ] 26 | -------------------------------------------------------------------------------- /webapp/src/components/HomePage/PollCards/PollCards.css: -------------------------------------------------------------------------------- 1 | .PollCards + .PollCards { 2 | margin-top: 36px; 3 | } 4 | 5 | .PollCards .ui.cards > .card.finished > .content > .header:not(.ui) { 6 | color: var(--secondary-text); 7 | font-weight: normal; 8 | letter-spacing: 0px; 9 | } 10 | 11 | .PollCards .ui.cards > .card.finished:hover > .content > .header:not(.ui) { 12 | color: var(--text); 13 | } 14 | 15 | .PollCards .ui.cards > .card .meta .poll-meta { 16 | display: flex; 17 | justify-content: space-between; 18 | } 19 | -------------------------------------------------------------------------------- /webapp/src/modules/vote/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'types' 2 | import { VoteState } from 'modules/vote/reducer' 3 | 4 | export const getState: (state: RootState) => VoteState = state => state.vote 5 | 6 | export const getData: (state: RootState) => VoteState['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) => VoteState['error'] = state => 13 | getState(state).error 14 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/OptionOrb/OptionOrb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './OptionOrb.css' 3 | import { Props } from './OptionOrb.types' 4 | 5 | export default class OptionOrb extends React.PureComponent { 6 | render() { 7 | const { children, position } = this.props 8 | let classes = `OptionOrb color-${position % 5}` 9 | return ( 10 |
11 | 12 | {children} 13 |
14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webapp/src/components/Hero/Hero.css: -------------------------------------------------------------------------------- 1 | /* Pyramids */ 2 | 3 | .pyramid { 4 | background: url('../../assets/pyramid.svg'); 5 | background-size: contain; 6 | background-repeat: no-repeat; 7 | } 8 | 9 | .pyramid.small { 10 | width: 400px; 11 | height: 400px; 12 | margin-left: 10%; 13 | margin-top: 100px; 14 | } 15 | 16 | .pyramid.large { 17 | width: 1000px; 18 | height: 1000px; 19 | margin-top: -200px; 20 | margin-left: 55%; 21 | } 22 | 23 | .dcl.hero .dcl.parallax { 24 | position: absolute; 25 | top: 0px; 26 | left: 0px; 27 | } 28 | -------------------------------------------------------------------------------- /webapp/src/modules/option/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'types' 2 | import { OptionState } from 'modules/option/reducer' 3 | 4 | export const getState: (state: RootState) => OptionState = state => state.option 5 | 6 | export const getData: (state: RootState) => OptionState['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) => OptionState['error'] = state => 13 | getState(state).error 14 | -------------------------------------------------------------------------------- /webapp/src/components/VotePage/VoteLabel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactMarkdown from 'react-markdown' 3 | import './VoteLabel.css' 4 | 5 | export default function VoteLabel(value?: string) { 6 | return (_: string, props: Record) => { 7 | return ( 8 | 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 | "ban-types": [true, "Number", "Boolean", "String", "Object"], 11 | "prettier": [ 12 | true, 13 | { "printWidth": 80, "singleQuote": true, "semi": false } 14 | ], 15 | "quotemark": [true, "single", "jsx-double"] 16 | }, 17 | "linterOptions": { 18 | "exclude": ["config/**/*.js", "node_modules/**/*.ts", "./**/*.js"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webapp/src/components/HomePage/HomePage.types.ts: -------------------------------------------------------------------------------- 1 | import { fetchPollsRequest } from 'modules/poll/actions' 2 | import { PollWithAssociations } from 'modules/poll/types' 3 | 4 | export interface Props { 5 | decentralandPolls: PollWithAssociations[] 6 | districtPolls: PollWithAssociations[] 7 | isLoading: boolean 8 | onFetchPolls: typeof fetchPollsRequest 9 | onNavigate: (location: string) => void 10 | } 11 | 12 | export type MapStateProps = Pick< 13 | Props, 14 | 'isLoading' | 'decentralandPolls' | 'districtPolls' 15 | > 16 | export type MapDispatchProps = Pick 17 | -------------------------------------------------------------------------------- /src/AccountBalance/AccountBalance.model.ts: -------------------------------------------------------------------------------- 1 | import { AccountBalanceAttributes } from './AccountBalance.types' 2 | import { ModelWithCallbacks } from '../lib' 3 | import { db } from 'decentraland-server' 4 | 5 | export class AccountBalance extends ModelWithCallbacks< 6 | AccountBalanceAttributes 7 | > { 8 | static tableName = 'account_balances' 9 | static withTimestamps = false 10 | 11 | static beforeModify( 12 | row: U 13 | ) { 14 | return row['address'] 15 | ? Object.assign({}, row, { address: row['address'].toLowerCase() }) 16 | : row 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webapp/src/modules/accountBalance/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'types' 2 | import { AccountBalanceState } from 'modules/accountBalance/reducer' 3 | 4 | export const getState: (state: RootState) => AccountBalanceState = state => 5 | state.accountBalance 6 | 7 | export const getData: ( 8 | state: RootState 9 | ) => AccountBalanceState['data'] = state => getState(state).data 10 | 11 | export const isLoading: (state: RootState) => boolean = state => 12 | getState(state).loading.length > 0 13 | 14 | export const getError: ( 15 | state: RootState 16 | ) => AccountBalanceState['error'] = state => getState(state).error 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Metaverse Holdings Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use files included in this repository except in compliance 5 | with the License. You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /webapp/src/locations.ts: -------------------------------------------------------------------------------- 1 | import { FilterType, FilterStatus } from 'modules/poll/types' 2 | 3 | export const locations = { 4 | root: () => '/', 5 | 6 | polls: () => '/polls', 7 | pollsTable: ( 8 | page: number = 1, 9 | type: FilterType = 'decentraland', 10 | status: FilterStatus = 'all' 11 | ) => `/polls?page=${page}&type=${type}&status=${status}`, 12 | 13 | poll: () => '/polls/:id', 14 | pollDetail: (id: string) => `/polls/${id}`, 15 | 16 | vote: () => '/polls/:id/vote', 17 | voteDetail: (id: string) => `/polls/${id}/vote`, 18 | 19 | signIn: () => '/sign-in' 20 | } 21 | 22 | export const STATIC_PAGES: string[] = [] 23 | -------------------------------------------------------------------------------- /src/Token/Token.router.ts: -------------------------------------------------------------------------------- 1 | import { server } from 'decentraland-server' 2 | import { utils } from 'decentraland-commons' 3 | 4 | import { Router, blacklist } from '../lib' 5 | import { Token } from './Token.model' 6 | import { TokenAttributes } from './Token.types' 7 | 8 | export class TokenRouter extends Router { 9 | mount() { 10 | /** 11 | * Returns all tokens 12 | */ 13 | this.app.get('/tokens', server.handleRequest(this.getTokens)) 14 | } 15 | 16 | async getTokens(): Promise { 17 | const tokens = await Token.find() 18 | return utils.mapOmit(tokens, blacklist.token) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webapp/src/modules/ui/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { decentralandPollsReducer as decentralandPolls } from 'modules/ui/decentralandPolls/reducer' 3 | import { districtPollsReducer as districtPolls } from 'modules/ui/districtPolls/reducer' 4 | import { pollsReducer as polls } from 'modules/ui/polls/reducer' 5 | import { TableState } from 'modules/ui/table/reducer' 6 | 7 | export type UIState = { 8 | decentralandPolls: TableState 9 | districtPolls: TableState 10 | polls: TableState 11 | } 12 | 13 | export const uiReducer = combineReducers({ 14 | decentralandPolls, 15 | districtPolls, 16 | polls 17 | }) 18 | -------------------------------------------------------------------------------- /webapp/src/modules/poll/utils.ts: -------------------------------------------------------------------------------- 1 | import { Poll, PollWithPointers } from 'modules/poll/types' 2 | import { Token } from 'modules/token/types' 3 | import { Vote } from 'modules/vote/types' 4 | import { Option } from 'modules/option/types' 5 | 6 | export function isFinished(poll: Poll) { 7 | return poll.closes_at < Date.now() 8 | } 9 | 10 | export function buildPoll( 11 | poll: Poll, 12 | token: Token, 13 | votes: Vote[], 14 | options: Option[] 15 | ): PollWithPointers { 16 | return { 17 | ...poll, 18 | token_address: token.address, 19 | vote_ids: votes.map(vote => vote.id), 20 | option_ids: options.map(option => option.id) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Option/Option.router.ts: -------------------------------------------------------------------------------- 1 | import { server } from 'decentraland-server' 2 | import * as express from 'express' 3 | 4 | import { Router } from '../lib' 5 | import { Option } from './Option.model' 6 | import { OptionAttributes } from './Option.types' 7 | 8 | export class OptionRouter extends Router { 9 | mount() { 10 | /** 11 | * Returns the votes for a poll 12 | */ 13 | this.app.get('/polls/:id/options', server.handleRequest(this.getPollVotes)) 14 | } 15 | 16 | async getPollVotes(req: express.Request) { 17 | const pollId = server.extractFromReq(req, 'id') 18 | return Option.find({ poll_id: Number(pollId) }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webapp/src/modules/ui/districtPolls/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'types' 2 | import { createSelector } from 'reselect' 3 | import { getPolls } from 'modules/poll/selectors' 4 | import { TableState } from 'modules/ui/table/reducer' 5 | import { PollWithAssociations } from 'modules/poll/types' 6 | import { ModelById } from '@dapps/lib/types' 7 | 8 | export const getState = (state: RootState) => state.ui.districtPolls 9 | export const getDistrictPolls = createSelector< 10 | RootState, 11 | TableState, 12 | ModelById, 13 | PollWithAssociations[] 14 | >(getState, getPolls, (table, polls) => { 15 | return table.rows.map(id => polls[id]) 16 | }) 17 | -------------------------------------------------------------------------------- /.ci/Pulumi.website-agora-dev.yaml: -------------------------------------------------------------------------------- 1 | encryptionsalt: v1:3dnmscRFvUE=:v1:WHqX1oGzexFtwlX5:XHwiRG1VN+LOHVYGvWDAuTmG/3/q0w== 2 | config: 3 | webiste-agora:CONNECTION_STRING: 4 | secure: v1:iZgzRyZ710JcfYSD:mx1s6VSCdU27O71dEye9e2Z4wc7PLzvAQY6tHp+j9WMdAI2wS98SSWsIID9iYiXGuYj4v4gA95AHYjo1kSmVt31fEt/1+vXUmQHqBhqb747G1mw5JOmBJs2iR/Lk1JxxJv0V1Z1MshxhvZeLs/Db 5 | webiste-agora:RPC_URL: 6 | secure: v1:YUpeJizWefBvZicC:CPsftc3zNxY9r16z45HgOwG/B0ccyZniS5cbczgkdcDUl+7kBCRYca3h7Oh6jZ39W4hmRrG6jjPhcpIe4Z4qzdiN/7H6P0IvNjwaA0s= 7 | webiste-agora:SERVER_SIGNING_KEY: 8 | secure: v1:AhWIUu/ibe7Ub5B9:+japaTVpcGNKgGNBObxYKL7muF4LZ5d3aMjojqwY7pjZsYWwgMFQH2BW1rAmXCm6kxbzbzhdK1+4cDrdUmby4YbdY3wNGjgc2LgaDXOtLL0= 9 | -------------------------------------------------------------------------------- /.ci/Pulumi.website-agora-prd.yaml: -------------------------------------------------------------------------------- 1 | encryptionsalt: v1:UYDPnHn1rFg=:v1:B31okGNlaORKO3aD:cKhmwECklMmsSGTqlKrBJwH3RREWQQ== 2 | config: 3 | webiste-agora:CONNECTION_STRING: 4 | secure: v1:7CS6Wo/LJb2StZZk:LPz2KqheD9hTeJnpJc9FrdYuEeeYOe5weBA1BVKv1s0l9L9JW4Xz1KKLCf3k4G8yWY2BYgNACIy55D8lUniFnZVXnMNVR7geOdxx+zmpcvGth9xB3Kq0+FvfY/aJ1QuFBpoH7/YkurXCK6ogrZk9 5 | webiste-agora:RPC_URL: 6 | secure: v1:37yP2GdEH9AjOWfj:R9vRR67ObOpNmIMAUUYzFVH42pqyS6URjpFoF5YUT+vzPOWr4rdrFZ8clbDe+KqAeA1n9z1+CLnRAV8sJtjAQqFcArLY5tGodV/SXmU= 7 | webiste-agora:SERVER_SIGNING_KEY: 8 | secure: v1:0K+/i5mgnKFxH8rg:LNr0qEGSKX1aoPy8IbKwTcjqZIAB9n30LmPupHBykK4TDYSedr+ETm3cFD9ujyNBsOmlawtGW1KHT0ITvM/rAViA6E2lrdb24WHMb4muG94= 9 | -------------------------------------------------------------------------------- /.ci/Pulumi.website-agora-stg.yaml: -------------------------------------------------------------------------------- 1 | encryptionsalt: v1:f0tC9Er78vs=:v1:HvBjQaGKUmxZTHRr:FNKwzxihR56lsgGGZsCkv7k4IP9jjQ== 2 | config: 3 | webiste-agora:CONNECTION_STRING: 4 | secure: v1:o0EOUFFpz4NGuQAP:J/sUlebJfIAvDRvxS8tCVXP2y24V2730kOKl0Hmlhui1lvsw/Isx/yQgBjrf3aAHJJfmDRCjU1A7rJuTooxoibStwZm75wLIs0e7VrazyssLuzjbuVS58CvTwqjvy1J9LEaFLGXt9QxvPcj0uNxZ 5 | webiste-agora:RPC_URL: 6 | secure: v1:U3On5uEOYwr1PvnS:pDOQyESyESa6jbWucPzRDpHlyhfMIQjrFZst0HIF7Bh8/HJ7zN1R38Up5aEjH9o/vLtnetE6EvvWXjcVGMrk7Bsb0T17L02R42QkL4Y= 7 | webiste-agora:SERVER_SIGNING_KEY: 8 | secure: v1:tEg21kPNPPPU+IoZ:5dJXl/kCGy9TRUfrkgizJ4QLrBvqx8gZNZWNjSUSc11qISCHiSGI1ha2LzUiAiRUxCq1OGBXgQFoIs5TQtRFN5zcXYsdvBxn4qlyTkIPpNs= 9 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/OptionBar/OptionBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './OptionBar.css' 3 | import { Props } from './OptionBar.types' 4 | 5 | export default class OptionBar extends React.PureComponent { 6 | render() { 7 | const { children, percentage, position } = this.props 8 | let classes = `OptionBar color-${position % 5}` 9 | return ( 10 |
11 |
12 |
13 | {children} 14 |  ( 15 | {percentage} 16 | %) 17 |
18 |
19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webapp/src/modules/ui/decentralandPolls/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'types' 2 | import { createSelector } from 'reselect' 3 | import { getPolls } from 'modules/poll/selectors' 4 | import { TableState } from 'modules/ui/table/reducer' 5 | import { PollWithAssociations } from 'modules/poll/types' 6 | import { ModelById } from '@dapps/lib/types' 7 | 8 | export const getState = (state: RootState) => state.ui.decentralandPolls 9 | export const getDecentralandPolls = createSelector< 10 | RootState, 11 | TableState, 12 | ModelById, 13 | PollWithAssociations[] 14 | >(getState, getPolls, (table, polls) => { 15 | return table.rows.map(id => polls[id]) 16 | }) 17 | -------------------------------------------------------------------------------- /webapp/src/modules/ui/polls/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INITIAL_STATE, 3 | createTableReducer, 4 | TableState 5 | } from 'modules/ui/table/reducer' 6 | import { 7 | FetchPollsSuccessAction, 8 | FETCH_POLLS_SUCCESS 9 | } from 'modules/poll/actions' 10 | 11 | const tableReducer = createTableReducer( 12 | (action: FetchPollsSuccessAction) => action.payload.polls 13 | ) 14 | 15 | export function pollsReducer( 16 | state: TableState = INITIAL_STATE, 17 | action: FetchPollsSuccessAction 18 | ) { 19 | switch (action.type) { 20 | case FETCH_POLLS_SUCCESS: { 21 | return tableReducer(state, action) 22 | } 23 | default: { 24 | return state 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollProgress/PollProgress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './PollProgress.css' 3 | import { Props } from './PollProgress.types' 4 | import PollOption from './PollOption' 5 | 6 | export default class PollProgress extends React.PureComponent { 7 | render() { 8 | const { results } = this.props 9 | 10 | return ( 11 |
12 | {results.map((result, index) => ( 13 | 19 | ))} 20 |
21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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 | "strictNullChecks": true, 17 | "noUnusedParameters": true, 18 | "noUnusedLocals": true, 19 | "lib": ["es2017"], 20 | "plugins": [{ "name": "tslint-language-service" }] 21 | }, 22 | "exclude": ["node_modules", "test", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/OptionBar/OptionBar.css: -------------------------------------------------------------------------------- 1 | .OptionBar { 2 | margin-bottom: 24px; 3 | } 4 | 5 | .OptionBar .bar { 6 | height: 24px; 7 | min-width: 16px; 8 | border-radius: 8px; 9 | margin-bottom: 4px; 10 | } 11 | 12 | .OptionBar .label { 13 | font-size: 12px; 14 | font-weight: bold; 15 | } 16 | 17 | .OptionBar.color-0 .bar { 18 | background: var(--night-time); 19 | } 20 | 21 | .OptionBar.color-1 .bar { 22 | background: var(--luisxvi-violet); 23 | } 24 | 25 | .OptionBar.color-2 .bar { 26 | background: var(--candy-purple); 27 | } 28 | 29 | .OptionBar.color-3 .bar { 30 | background: var(--summer-red); 31 | } 32 | 33 | .OptionBar.color-4 .bar { 34 | background: var(--oj-not-simpson); 35 | } 36 | -------------------------------------------------------------------------------- /migrations/1527706973005_tokens-create.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate' 2 | import { Token } from '../src/Token' 3 | 4 | const tableName = Token.tableName 5 | 6 | export const up = (pgm: MigrationBuilder) => { 7 | pgm.createTable( 8 | tableName, 9 | { 10 | address: { type: 'TEXT', primaryKey: true, notNull: true, comment: null }, 11 | name: { type: 'TEXT', notNull: true, comment: null }, 12 | symbol: { type: 'TEXT', notNull: true, comment: null } 13 | }, 14 | { ifNotExists: true, comment: null } 15 | ) 16 | 17 | pgm.createIndex(tableName, 'symbol', { unique: true }) 18 | } 19 | 20 | export const down = (pgm: MigrationBuilder) => { 21 | pgm.dropTable(tableName, {}) 22 | } 23 | -------------------------------------------------------------------------------- /webapp/src/modules/wallet/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux' 2 | import { 3 | walletReducer as baseWallerReducer, 4 | INITIAL_STATE as BASE_INITIAL_STATE, 5 | WalletState as BaseWalletState, 6 | WalletReducerAction as BaseWalletReducerAction 7 | } from '@dapps/modules/wallet/reducer' 8 | import { Wallet } from './types' 9 | 10 | export interface WalletState extends BaseWalletState { 11 | data: Partial 12 | } 13 | 14 | const INITIAL_STATE: WalletState = { 15 | ...BASE_INITIAL_STATE 16 | } 17 | 18 | export function walletReducer(state = INITIAL_STATE, action: AnyAction) { 19 | switch (action.type) { 20 | default: 21 | return baseWallerReducer(state, action as BaseWalletReducerAction) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webapp/src/modules/ui/polls/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'types' 2 | import { createSelector } from 'reselect' 3 | import { getPolls as getAllPolls } from 'modules/poll/selectors' 4 | import { TableState } from 'modules/ui/table/reducer' 5 | import { PollWithAssociations } from 'modules/poll/types' 6 | import { ModelById } from '@dapps/lib/types' 7 | 8 | export const getState = (state: RootState) => state.ui.polls 9 | export const getTotal = (state: RootState) => getState(state).total 10 | export const getPolls = createSelector< 11 | RootState, 12 | TableState, 13 | ModelById, 14 | PollWithAssociations[] 15 | >(getState, getAllPolls, (table, polls) => { 16 | return table.rows.map(id => polls[id]) 17 | }) 18 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/YourVote/YourVote.css: -------------------------------------------------------------------------------- 1 | .YourVote { 2 | display: inline-block; 3 | margin-left: 1em; 4 | text-transform: uppercase; 5 | font-size: 12px; 6 | color: var(--text); 7 | font-weight: bold; 8 | } 9 | 10 | .YourVote .time-ago { 11 | color: var(--secondary-text); 12 | text-transform: uppercase; 13 | } 14 | 15 | @media (max-width: 768px) { 16 | .YourVote { 17 | position: fixed; 18 | left: 0px; 19 | bottom: 0px; 20 | margin: 0px; 21 | height: 64px; 22 | width: 100%; 23 | text-align: center; 24 | background: var(--secondary); 25 | padding: 24px; 26 | white-space: nowrap; 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | z-index: 999; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/AccountBalance/AccountBalance.router.ts: -------------------------------------------------------------------------------- 1 | import { server } from 'decentraland-server' 2 | import * as express from 'express' 3 | 4 | import { Router } from '../lib' 5 | import { AccountBalance } from './AccountBalance.model' 6 | import { AccountBalanceAttributes } from './AccountBalance.types' 7 | 8 | export class AccountBalanceRouter extends Router { 9 | mount() { 10 | /** 11 | * Returns the votes for a poll 12 | */ 13 | this.app.get( 14 | '/accountBalances/:address', 15 | server.handleRequest(this.getAccountBalances) 16 | ) 17 | } 18 | 19 | async getAccountBalances(req: express.Request) { 20 | const address = server.extractFromReq(req, 'address') 21 | return AccountBalance.find({ address }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webapp/src/modules/ui/districtPolls/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INITIAL_STATE, 3 | createTableReducer, 4 | TableState 5 | } from 'modules/ui/table/reducer' 6 | import { 7 | FetchPollsSuccessAction, 8 | FETCH_POLLS_SUCCESS 9 | } from 'modules/poll/actions' 10 | 11 | const tableReducer = createTableReducer( 12 | (action: FetchPollsSuccessAction) => action.payload.polls 13 | ) 14 | 15 | export function districtPollsReducer( 16 | state: TableState = INITIAL_STATE, 17 | action: FetchPollsSuccessAction 18 | ) { 19 | switch (action.type) { 20 | case FETCH_POLLS_SUCCESS: { 21 | return action.payload.filters.type === 'district' 22 | ? tableReducer(state, action) 23 | : state 24 | } 25 | default: { 26 | return state 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Vote/Vote.types.ts: -------------------------------------------------------------------------------- 1 | import { OptionAttributes } from '../Option' 2 | import { PollAttributes } from '../Poll' 3 | import { TokenAttributes } from '../Token' 4 | import { Omit } from '../lib/types' 5 | 6 | export interface VoteAttributes { 7 | id?: string 8 | account_address: string 9 | account_balance: string // DECIMAL 10 | poll_id: string 11 | option_id: string 12 | timestamp: string // DECIMAL 13 | message: string 14 | signature: string 15 | created_at?: Date 16 | updated_at?: Date 17 | } 18 | 19 | export interface CastVoteOption extends Omit { 20 | id: string 21 | option: OptionAttributes 22 | } 23 | 24 | export interface CastVote extends Omit { 25 | id: string 26 | poll: PollAttributes 27 | token: TokenAttributes 28 | } 29 | -------------------------------------------------------------------------------- /webapp/src/modules/ui/decentralandPolls/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INITIAL_STATE, 3 | createTableReducer, 4 | TableState 5 | } from 'modules/ui/table/reducer' 6 | import { 7 | FetchPollsSuccessAction, 8 | FETCH_POLLS_SUCCESS 9 | } from 'modules/poll/actions' 10 | 11 | const tableReducer = createTableReducer( 12 | (action: FetchPollsSuccessAction) => action.payload.polls 13 | ) 14 | 15 | export function decentralandPollsReducer( 16 | state: TableState = INITIAL_STATE, 17 | action: FetchPollsSuccessAction 18 | ) { 19 | switch (action.type) { 20 | case FETCH_POLLS_SUCCESS: { 21 | return action.payload.filters.type === 'decentraland' 22 | ? tableReducer(state, action) 23 | : state 24 | } 25 | default: { 26 | return state 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Poll/Poll.types.ts: -------------------------------------------------------------------------------- 1 | import { TokenAttributes } from '../Token' 2 | import { VoteAttributes } from '../Vote' 3 | import { OptionAttributes } from '../Option' 4 | import { AccountBalanceAttributes } from '../AccountBalance' 5 | 6 | interface Poll { 7 | id?: string 8 | title: string 9 | description?: string 10 | balance: string // DECIMAL 11 | token_address: string 12 | submitter: string 13 | closes_at: number 14 | created_at?: Date 15 | updated_at?: Date 16 | } 17 | 18 | export interface PollAttributes extends Poll { 19 | token?: TokenAttributes 20 | votes?: VoteAttributes[] 21 | options?: OptionAttributes[] 22 | accounts?: AccountBalanceAttributes[] 23 | } 24 | 25 | export interface PollWithPointers extends Poll { 26 | votes_ids: number[] 27 | options_ids: number[] 28 | } 29 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/OptionOrb/OptionOrb.css: -------------------------------------------------------------------------------- 1 | .OptionOrb { 2 | display: flex; 3 | align-items: center; 4 | margin-right: 18px; 5 | margin-bottom: 18px; 6 | } 7 | 8 | .OptionOrb .orb { 9 | display: inline-block; 10 | width: 16px; 11 | height: 16px; 12 | border-radius: 16px; 13 | margin-right: 8px; 14 | } 15 | 16 | .OptionOrb .label { 17 | font-weight: bold; 18 | color: var(--text); 19 | } 20 | 21 | .OptionOrb.color-0 .orb { 22 | background: var(--night-time); 23 | } 24 | 25 | .OptionOrb.color-1 .orb { 26 | background: var(--luisxvi-violet); 27 | } 28 | 29 | .OptionOrb.color-2 .orb { 30 | background: var(--candy-purple); 31 | } 32 | 33 | .OptionOrb.color-3 .orb { 34 | background: var(--summer-red); 35 | } 36 | 37 | .OptionOrb.color-4 .orb { 38 | background: var(--oj-not-simpson); 39 | } 40 | -------------------------------------------------------------------------------- /webapp/src/components/HomePage/HomePage.css: -------------------------------------------------------------------------------- 1 | .HomePage { 2 | margin-bottom: 96px; 3 | } 4 | 5 | .HomePage .cards .card .meta .Token, 6 | .HomePage .cards .card .meta .Token .symbol { 7 | font-size: 15px; 8 | line-height: 24px; 9 | font-weight: normal; 10 | font-style: normal; 11 | font-stretch: normal; 12 | letter-spacing: normal; 13 | color: var(--secondary-text); 14 | font-family: var(--font-family); 15 | display: inline; 16 | } 17 | 18 | .HomePage .cards .card:hover .meta .Token, 19 | .HomePage .cards .card:hover .meta .Token .symbol { 20 | color: var(--primary); 21 | } 22 | 23 | .HomePage .ui.loader { 24 | margin-top: 128px; 25 | } 26 | 27 | .dcl.navbar .dcl.hero { 28 | background-color: #f2f2f5; 29 | } 30 | 31 | @media (max-width: 768px) { 32 | .pyramid.small { 33 | margin-left: 0px; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/YourVote/YourVote.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { getVoteOptionValue } from 'modules/option/utils' 3 | import { distanceInWordsToNow } from '@dapps/lib/utils' 4 | import { t } from '@dapps/modules/translation/utils' 5 | import { Props } from './YourVote.types' 6 | import './YourVote.css' 7 | 8 | export default class YourVote extends React.PureComponent { 9 | render() { 10 | const { vote, poll } = this.props 11 | return vote ? ( 12 | 13 | {t('poll_detail_page.you_voted', { 14 | option: getVoteOptionValue(poll.options, vote) 15 | })} 16 | .{' '} 17 | 18 | {distanceInWordsToNow(vote.timestamp)}. 19 | 20 | 21 | ) : null 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/Model.queries.ts: -------------------------------------------------------------------------------- 1 | import { SQL, SQLStatement, raw } from 'decentraland-server' 2 | 3 | export interface JSONAggParams { 4 | columnName?: string 5 | filterColumn?: string 6 | orderColumn?: string 7 | } 8 | 9 | export const ModelQueries = Object.freeze({ 10 | jsonAgg: (tableName: string, params: JSONAggParams = {}): SQLStatement => { 11 | const { columnName, filterColumn, orderColumn } = Object.assign( 12 | { columnName: '*', filterColumn: 'id', orderColumn: 'id' }, 13 | params 14 | ) 15 | const column = raw(`${tableName}.${columnName}`) 16 | const filter = raw(`${tableName}.${filterColumn}`) 17 | const order = raw(`${tableName}.${orderColumn}`) 18 | 19 | return SQL`COALESCE( 20 | json_agg(${column} ORDER BY ${order}) FILTER (WHERE ${filter} IS NOT NULL), 21 | '[]' 22 | )` 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /webapp/src/sagas.ts: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects' 2 | import { analyticsSaga } from '@dapps/modules/analytics/sagas' 3 | import { locationSaga } from '@dapps/modules/location/sagas' 4 | import { accountBalanceSaga } from 'modules/accountBalance/sagas' 5 | import { optionSaga } from 'modules/option/sagas' 6 | import { pollSaga } from 'modules/poll/sagas' 7 | import { voteSaga } from 'modules/vote/sagas' 8 | import { tokenSaga } from 'modules/token/sagas' 9 | import { translationSaga } from 'modules/translation/sagas' 10 | import { walletSaga } from 'modules/wallet/sagas' 11 | 12 | export function* rootSaga() { 13 | yield all([ 14 | accountBalanceSaga(), 15 | analyticsSaga(), 16 | locationSaga(), 17 | optionSaga(), 18 | pollSaga(), 19 | voteSaga(), 20 | tokenSaga(), 21 | translationSaga(), 22 | walletSaga() 23 | ]) 24 | } 25 | -------------------------------------------------------------------------------- /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 ? require.main.filename : __dirname) 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/modules/option/sagas.ts: -------------------------------------------------------------------------------- 1 | import { call, put, takeLatest } from 'redux-saga/effects' 2 | import { 3 | fetchOptionsByPollIdSuccess, 4 | fetchOptionsByPollIdFailure, 5 | FETCH_POLL_OPTIONS_REQUEST, 6 | FetchPollOptionsRequestAction 7 | } from 'modules/option/actions' 8 | import { Option } from 'modules/option/types' 9 | import { api } from 'lib/api' 10 | 11 | export function* optionSaga() { 12 | yield takeLatest(FETCH_POLL_OPTIONS_REQUEST, handlePollOptionsRequest) 13 | } 14 | 15 | function* handlePollOptionsRequest(action: FetchPollOptionsRequestAction) { 16 | try { 17 | const pollId = action.payload.pollId 18 | const options: Option[] = yield call(() => api.fetchPollOptions(pollId)) 19 | 20 | yield put(fetchOptionsByPollIdSuccess(options, pollId)) 21 | } catch (error) { 22 | yield put(fetchOptionsByPollIdFailure(error.message)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /webapp/src/modules/token/actions.ts: -------------------------------------------------------------------------------- 1 | import { action } from 'typesafe-actions' 2 | import { Token } from 'modules/token/types' 3 | 4 | // Fetch Tokens 5 | 6 | export const FETCH_TOKENS_REQUEST = '[Request] Fetch Tokens' 7 | export const FETCH_TOKENS_SUCCESS = '[Success] Fetch Tokens' 8 | export const FETCH_TOKENS_FAILURE = '[Failure] Fetch Tokens' 9 | 10 | export const fetchTokensRequest = () => action(FETCH_TOKENS_REQUEST, {}) 11 | export const fetchTokensSuccess = (tokens: Token[]) => 12 | action(FETCH_TOKENS_SUCCESS, { tokens }) 13 | export const fetchTokensFailure = (error: string) => 14 | action(FETCH_TOKENS_FAILURE, { error }) 15 | 16 | export type FetchTokensRequestAction = ReturnType 17 | export type FetchTokensSuccessAction = ReturnType 18 | export type FetchTokensFailureAction = ReturnType 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.sort()).to.deep.equal(mainKeys.sort()) 19 | }) 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /webapp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill' 2 | import * as React from 'react' 3 | import * as ReactDOM from 'react-dom' 4 | import 'decentraland-ui/lib/styles.css' 5 | import './analytics' 6 | 7 | import { Provider } from 'react-redux' 8 | import { ConnectedRouter } from 'react-router-redux' 9 | import TranslationProvider from '@dapps/providers/TranslationProvider' 10 | import WalletProvier from '@dapps/providers/WalletProvider' 11 | 12 | import Routes from './Routes' 13 | import { store, history } from './store' 14 | 15 | import './index.css' 16 | 17 | ReactDOM.render( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | , 27 | document.getElementById('root') 28 | ) 29 | -------------------------------------------------------------------------------- /webapp/src/components/Hero/Hero.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Hero as HeroComponent, Parallax } from 'decentraland-ui' 4 | import { t } from '@dapps/modules/translation/utils' 5 | 6 | import './Hero.css' 7 | 8 | export default class Hero extends React.PureComponent { 9 | render() { 10 | return ( 11 | 12 | {t('homepage.title')} 13 | 14 | {t('homepage.subtitle')} 15 | 16 | 17 | 18 |
19 | 20 | 21 |
22 | 23 | 24 | 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /webapp/src/modules/wallet/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from 'types' 3 | import { Wallet } from 'modules/wallet/types' 4 | import { AccountBalanceState } from 'modules/accountBalance/reducer' 5 | import { getData as getAccountBalances } from 'modules/accountBalance/selectors' 6 | import { getData } from '@dapps/modules/wallet/selectors' 7 | import { WalletState } from '@dapps/modules/wallet/reducer' 8 | 9 | export const getWallet = createSelector< 10 | RootState, 11 | WalletState['data'], 12 | AccountBalanceState['data'], 13 | Partial 14 | >(getData, getAccountBalances, (wallet, accountBalances) => { 15 | const balances = {} 16 | 17 | for (const accountBalance of Object.values(accountBalances)) { 18 | balances[accountBalance.token_address] = Number(accountBalance.balance) 19 | } 20 | 21 | return { 22 | ...wallet, 23 | balances 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /webapp/src/modules/token/sagas.ts: -------------------------------------------------------------------------------- 1 | import { call, put, takeLatest } from 'redux-saga/effects' 2 | import { 3 | fetchTokensSuccess, 4 | fetchTokensFailure, 5 | FETCH_TOKENS_REQUEST, 6 | FETCH_TOKENS_SUCCESS 7 | } from 'modules/token/actions' 8 | import { Token } from 'modules/token/types' 9 | import { computeBalancesRequest } from 'modules/wallet/actions' 10 | import { api } from 'lib/api' 11 | 12 | export function* tokenSaga() { 13 | yield takeLatest(FETCH_TOKENS_REQUEST, handleTokensRequest) 14 | yield takeLatest(FETCH_TOKENS_SUCCESS, handleTokensSuccess) 15 | } 16 | 17 | function* handleTokensRequest() { 18 | try { 19 | const tokens: Token[] = yield call(() => api.fetchTokens()) 20 | yield put(fetchTokensSuccess(tokens)) 21 | } catch (error) { 22 | yield put(fetchTokensFailure(error.message)) 23 | } 24 | } 25 | 26 | function* handleTokensSuccess() { 27 | yield put(computeBalancesRequest()) 28 | } 29 | -------------------------------------------------------------------------------- /webapp/src/components/VotePage/VotePage.types.ts: -------------------------------------------------------------------------------- 1 | import { match } from 'react-router' 2 | import { PollWithAssociations } from 'modules/poll/types' 3 | import { Vote } from 'modules/vote/types' 4 | import { Wallet } from 'modules/wallet/types' 5 | 6 | export type URLParams = { 7 | id: string 8 | } 9 | 10 | export type Props = { 11 | match: match 12 | pollId: string 13 | poll: PollWithAssociations | null 14 | wallet: Wallet 15 | currentVote: Vote | null 16 | isLoading: boolean 17 | isConnected: boolean 18 | onFetchPoll: Function 19 | onCreateVote: Function 20 | onNavigate: Function 21 | } 22 | 23 | export type State = { 24 | selectedOptionId: string 25 | } 26 | 27 | export type MapStateProps = Pick< 28 | Props, 29 | 'pollId' | 'poll' | 'wallet' | 'currentVote' | 'isLoading' | 'isConnected' 30 | > 31 | 32 | export type MapDispatchProps = Pick< 33 | Props, 34 | 'onFetchPoll' | 'onCreateVote' | 'onNavigate' 35 | > 36 | -------------------------------------------------------------------------------- /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/modules/accountBalance/sagas.ts: -------------------------------------------------------------------------------- 1 | import { call, put, takeLatest } from 'redux-saga/effects' 2 | import { 3 | fetchAccountBalancesSuccess, 4 | fetchAccountBalancesFailure, 5 | FETCH_ACCOUNT_BALANCES_REQUEST, 6 | FetchAccountBalancesRequestAction 7 | } from 'modules/accountBalance/actions' 8 | import { AccountBalance } from 'modules/accountBalance/types' 9 | import { api } from 'lib/api' 10 | 11 | export function* accountBalanceSaga() { 12 | yield takeLatest(FETCH_ACCOUNT_BALANCES_REQUEST, handleAccountsRequest) 13 | } 14 | 15 | function* handleAccountsRequest(action: FetchAccountBalancesRequestAction) { 16 | try { 17 | const address = action.payload.address 18 | const accounts: AccountBalance[] = yield call(() => 19 | api.fetchAccountBalances(address) 20 | ) 21 | 22 | yield put(fetchAccountBalancesSuccess(accounts)) 23 | } catch (error) { 24 | yield put(fetchAccountBalancesFailure(error.message)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /migrations/1527707071189_options-create.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate' 2 | import { Option } from '../src/Option' 3 | import { Poll } from '../src/Poll' 4 | 5 | const tableName = Option.tableName 6 | 7 | export const up = (pgm: MigrationBuilder) => { 8 | pgm.createTable( 9 | tableName, 10 | { 11 | id: { 12 | type: 'UUID', 13 | default: pgm.func('uuid_generate_v4()'), 14 | primaryKey: true, 15 | notNull: true, 16 | comment: null 17 | }, 18 | value: { type: 'TEXT', notNull: true, comment: null }, 19 | poll_id: { type: 'UUID', references: Poll.tableName, comment: null }, 20 | created_at: { type: 'TIMESTAMP', notNull: true, comment: null }, 21 | updated_at: { type: 'TIMESTAMP', comment: null } 22 | }, 23 | { ifNotExists: true, comment: null } 24 | ) 25 | } 26 | 27 | export const down = (pgm: MigrationBuilder) => { 28 | pgm.dropTable(tableName, {}) 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/ModelWithCallbacks.ts: -------------------------------------------------------------------------------- 1 | import { Model, db } from 'decentraland-server' 2 | 3 | export class ModelWithCallbacks extends Model { 4 | static async insert( 5 | row: U, 6 | onConflict: db.OnConflict 7 | ) { 8 | row = this.beforeInsert(row, !!onConflict) 9 | row = this.beforeModify(row) as U 10 | return super.insert(row, onConflict) 11 | } 12 | 13 | static update( 14 | changes: Partial, 15 | conditions: Partial

16 | ) { 17 | changes = this.beforeUpdate(changes) 18 | changes = this.beforeModify(changes) 19 | return super.update(changes, conditions) 20 | } 21 | 22 | protected static beforeInsert(row: U, _isUpsert: boolean): U { 23 | return row 24 | } 25 | 26 | protected static beforeUpdate(changes: Partial): Partial { 27 | return changes 28 | } 29 | 30 | protected static beforeModify(changes: U | Partial) { 31 | return changes 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/src/modules/wallet/actions.ts: -------------------------------------------------------------------------------- 1 | import { action } from 'typesafe-actions' 2 | import { Wallet } from 'modules/wallet/types' 3 | 4 | export const COMPUTE_BALANCES_REQUEST = '[Request] Compute Wallet Balances' 5 | export const COMPUTE_BALANCES_SUCCESS = '[Success] Compute Wallet Balances' 6 | export const COMPUTE_BALANCES_FAILURE = '[Failure] Compute Wallet Balances' 7 | 8 | export const computeBalancesRequest = () => action(COMPUTE_BALANCES_REQUEST, {}) 9 | export const computeBalancesSuccess = ( 10 | address: string, 11 | balances: Wallet['balances'] 12 | ) => action(COMPUTE_BALANCES_SUCCESS, { address, balances }) 13 | export const computeBalancesFailure = (error: string) => 14 | action(COMPUTE_BALANCES_FAILURE, { error }) 15 | 16 | export type ComputeBalancesRequestAction = ReturnType< 17 | typeof computeBalancesRequest 18 | > 19 | export type ComputeBalancesSuccessAction = ReturnType< 20 | typeof computeBalancesSuccess 21 | > 22 | export type ComputeBalancesFailureAction = ReturnType< 23 | typeof computeBalancesFailure 24 | > 25 | -------------------------------------------------------------------------------- /webapp/src/components/PollsTable/PollsTable.types.ts: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from 'react-router' 2 | import { 3 | PollWithAssociations, 4 | FilterStatus, 5 | FilterType, 6 | PollsRequestFilters 7 | } from 'modules/poll/types' 8 | 9 | export type QueryParams = { 10 | type: FilterType 11 | status: FilterStatus 12 | page: number 13 | } 14 | 15 | export type Props = RouteComponentProps<{}> & { 16 | title: React.ReactNode 17 | polls: PollWithAssociations[] 18 | status: FilterStatus 19 | type: FilterType 20 | page: number 21 | rowsPerPage: number 22 | totalRows: number 23 | onPageChange: (page: number) => void 24 | onStatusChange: (status: FilterStatus) => void 25 | onFetchPolls: (pagination: PollsRequestFilters) => void 26 | onNavigate: (location: string) => void 27 | } 28 | 29 | export type MapStateProps = Pick< 30 | Props, 31 | 'polls' | 'status' | 'type' | 'page' | 'totalRows' 32 | > 33 | export type MapDispatchProps = Pick< 34 | Props, 35 | 'onPageChange' | 'onStatusChange' | 'onFetchPolls' | 'onNavigate' 36 | > 37 | -------------------------------------------------------------------------------- /webapp/src/modules/token/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from 'types' 3 | import { isDistrictTokenAddress } from 'modules/token/district_token/utils' 4 | import { TokenState } from 'modules/token/reducer' 5 | 6 | export const getState: (state: RootState) => TokenState = state => state.token 7 | 8 | export const getData: (state: RootState) => TokenState['data'] = state => 9 | getState(state).data 10 | 11 | export const isLoading: (state: RootState) => boolean = state => 12 | getState(state).loading.length > 0 13 | 14 | export const getError: (state: RootState) => TokenState['error'] = state => 15 | getState(state).error 16 | 17 | export const getContractTokens = createSelector< 18 | RootState, 19 | TokenState['data'], 20 | TokenState['data'] 21 | >(getData, tokens => 22 | Object.keys(tokens) 23 | .filter(address => !isDistrictTokenAddress(address)) 24 | .reduce((contractTokens, address) => { 25 | contractTokens[address] = tokens[address] 26 | return contractTokens 27 | }, {}) 28 | ) 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /webapp/src/modules/ui/table/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux' 2 | import { IdGetter, ArrayGetter, TotalGetter } from 'modules/ui/table/types' 3 | 4 | export type TableState = { 5 | rows: string[] 6 | total: number 7 | } 8 | 9 | export const DEFAULT_ID_GETTER: IdGetter = (element: { id: string }) => 10 | element.id 11 | export const DEFAULT_TOTAL_GETTER: TotalGetter = (action: AnyAction) => 12 | action.payload.total 13 | 14 | export const INITIAL_STATE: TableState = { 15 | rows: [], 16 | total: 0 17 | } 18 | 19 | export function createTableReducer( 20 | arrayGetter: ArrayGetter, 21 | idGetter: IdGetter = DEFAULT_ID_GETTER, 22 | totalGetter: TotalGetter = DEFAULT_TOTAL_GETTER 23 | ) { 24 | return function tableReducer( 25 | state: TableState = INITIAL_STATE, 26 | action: AnyAction 27 | ) { 28 | const array = arrayGetter(action) 29 | if (array) { 30 | return { 31 | ...state, 32 | rows: array.map((element: any) => idGetter(element, action)), 33 | total: totalGetter(action) 34 | } 35 | } 36 | return state 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /migrations/1527707064752_accounts-create.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate' 2 | import { AccountBalance } from '../src/AccountBalance' 3 | import { Token } from '../src/Token' 4 | 5 | const tableName = AccountBalance.tableName 6 | 7 | export const up = (pgm: MigrationBuilder) => { 8 | pgm.createTable( 9 | tableName, 10 | { 11 | id: { 12 | type: 'UUID', 13 | default: pgm.func('uuid_generate_v4()'), 14 | primaryKey: true, 15 | notNull: true, 16 | comment: null 17 | }, 18 | address: { type: 'TEXT', notNull: true, comment: null }, 19 | token_address: { 20 | type: 'TEXT', 21 | notNull: true, 22 | references: Token.tableName, 23 | comment: null 24 | }, 25 | balance: { type: 'DECIMAL', notNull: true, default: '0', comment: null } 26 | }, 27 | { ifNotExists: true, comment: null } 28 | ) 29 | 30 | pgm.createIndex(tableName, ['address', 'token_address'], { unique: true }) 31 | } 32 | 33 | export const down = (pgm: MigrationBuilder) => { 34 | pgm.dropTable(tableName, {}) 35 | } 36 | -------------------------------------------------------------------------------- /webapp/src/modules/option/actions.ts: -------------------------------------------------------------------------------- 1 | import { action } from 'typesafe-actions' 2 | import { Option } from 'modules/option/types' 3 | 4 | // Fetch Poll Options 5 | 6 | export const FETCH_POLL_OPTIONS_REQUEST = '[Request] Fetch Poll Options' 7 | export const FETCH_POLL_OPTIONS_SUCCESS = '[Success] Fetch Poll Options' 8 | export const FETCH_POLL_OPTIONS_FAILURE = '[Failure] Fetch Poll Options' 9 | 10 | export const fetchOptionsByPollIdRequest = (pollId: string) => 11 | action(FETCH_POLL_OPTIONS_REQUEST, { pollId }) 12 | export const fetchOptionsByPollIdSuccess = ( 13 | options: Option[], 14 | pollId: string 15 | ) => action(FETCH_POLL_OPTIONS_SUCCESS, { options, pollId }) 16 | export const fetchOptionsByPollIdFailure = (error: string) => 17 | action(FETCH_POLL_OPTIONS_FAILURE, { error }) 18 | 19 | export type FetchPollOptionsRequestAction = ReturnType< 20 | typeof fetchOptionsByPollIdRequest 21 | > 22 | export type FetchPollOptionsSuccessAction = ReturnType< 23 | typeof fetchOptionsByPollIdSuccess 24 | > 25 | export type FetchPollOptionsFailureAction = ReturnType< 26 | typeof fetchOptionsByPollIdFailure 27 | > 28 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/CastYourVote/CastYourVote.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { isMobile } from 'decentraland-dapps/dist/lib/utils' 3 | 4 | import { isFinished } from 'modules/poll/utils' 5 | import { Link } from 'react-router-dom' 6 | import { locations } from 'locations' 7 | import { Button } from 'decentraland-ui' 8 | import { t } from '@dapps/modules/translation/utils' 9 | import { Props } from './CastYourVote.types' 10 | import './CastYourVote.css' 11 | 12 | export default class CastYourVote extends React.PureComponent { 13 | render() { 14 | const { poll, isConnected } = this.props 15 | 16 | return isFinished(poll) && isMobile() ? null : ( 17 |

18 | {!isFinished(poll) && isConnected ? ( 19 | 20 | 21 | 22 | ) : ( 23 | 26 | )} 27 |
28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Poll/Poll.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 { Poll } from './Poll.model' 7 | import { PollAttributes } from './Poll.types' 8 | import { PollRequestFilters } from './PollRequestFilters' 9 | 10 | export class PollRouter extends Router { 11 | mount() { 12 | /** 13 | * Returns all polls 14 | */ 15 | this.app.get('/polls', server.handleRequest(this.getPolls)) 16 | 17 | /** 18 | * Return a poll by id 19 | * @param {string} id 20 | */ 21 | this.app.get('/polls/:id', server.handleRequest(this.getPoll)) 22 | } 23 | 24 | async getPolls( 25 | req: express.Request 26 | ): Promise<{ polls: PollAttributes[]; total: number }> { 27 | const filters = new PollRequestFilters(req) 28 | return Poll.filter(filters) 29 | } 30 | 31 | async getPoll(req: express.Request) { 32 | const id = server.extractFromReq(req, 'id') 33 | const poll = await Poll.findByIdWithAssociations(id) 34 | return utils.omit(poll, blacklist.poll) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /webapp/src/modules/accountBalance/actions.ts: -------------------------------------------------------------------------------- 1 | import { action } from 'typesafe-actions' 2 | import { AccountBalance } from 'modules/accountBalance/types' 3 | 4 | // Fetch Account Balances 5 | 6 | export const FETCH_ACCOUNT_BALANCES_REQUEST = '[Request] Fetch Account Balances' 7 | export const FETCH_ACCOUNT_BALANCES_SUCCESS = '[Success] Fetch Account Balances' 8 | export const FETCH_ACCOUNT_BALANCES_FAILURE = '[Failure] Fetch Account Balances' 9 | 10 | export const fetchAccountBalancesRequest = (address: string) => 11 | action(FETCH_ACCOUNT_BALANCES_REQUEST, { address }) 12 | export const fetchAccountBalancesSuccess = ( 13 | accountBalances: AccountBalance[] 14 | ) => action(FETCH_ACCOUNT_BALANCES_SUCCESS, { accountBalances }) 15 | export const fetchAccountBalancesFailure = (error: string) => 16 | action(FETCH_ACCOUNT_BALANCES_FAILURE, { error }) 17 | 18 | export type FetchAccountBalancesRequestAction = ReturnType< 19 | typeof fetchAccountBalancesRequest 20 | > 21 | export type FetchAccountBalancesSuccessAction = ReturnType< 22 | typeof fetchAccountBalancesSuccess 23 | > 24 | export type FetchAccountBalancesFailureAction = ReturnType< 25 | typeof fetchAccountBalancesFailure 26 | > 27 | -------------------------------------------------------------------------------- /webapp/src/components/VotePage/VotePage.css: -------------------------------------------------------------------------------- 1 | .VotePage .description { 2 | font-size: 17px; 3 | line-height: 26px; 4 | letter-spacing: -0.2px; 5 | margin-top: 8px; 6 | overflow: hidden; 7 | text-overflow: ellipsis; 8 | } 9 | 10 | .VotePage .ui.header.sub.description a { 11 | color: var(--primary); 12 | } 13 | 14 | .VotePage .ui.header.sub.description a:hover { 15 | color: var(--primary-dark); 16 | } 17 | 18 | .VotePage .options { 19 | margin-top: 40px; 20 | } 21 | 22 | .VotePage .options.many .ui.radio.checkbox { 23 | width: 100%; 24 | } 25 | 26 | .VotePage .voting-with { 27 | margin-top: 40px; 28 | display: block; 29 | } 30 | 31 | .VotePage .vote { 32 | margin-top: 40px; 33 | } 34 | 35 | .VotePage .no-balance { 36 | margin-top: 16px; 37 | color: var(--secondary-text); 38 | } 39 | 40 | @media (min-width: 768px) { 41 | .VotePage .vote .ui.button { 42 | min-width: 200px; 43 | } 44 | } 45 | 46 | @media (max-width: 768px) { 47 | .VotePage { 48 | margin-bottom: 16px; 49 | } 50 | 51 | .VotePage .vote { 52 | display: flex; 53 | flex-flow: row nowrap; 54 | } 55 | 56 | .VotePage .vote .ui.button { 57 | flex: 1 0 auto; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "outDir": "build/dist", 5 | "module": "commonjs", 6 | "target": "es5", 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 | "components/*": ["./src/components/*"], 24 | "lib/*": ["./src/lib/*"], 25 | "modules/*": ["./src/modules/*"], 26 | "themes/*": ["./src/themes/*"], 27 | "reducer": ["./src/reducer"], 28 | "types": ["./src/types"], 29 | "store": ["./src/store"], 30 | "locations": ["./src/locations"], 31 | "contracts": ["./src/contracts"], 32 | "@dapps/*": ["./node_modules/decentraland-dapps/dist/*"] 33 | } 34 | }, 35 | "include": ["./src"] 36 | } 37 | -------------------------------------------------------------------------------- /webapp/src/components/HomePage/HomePage.container.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { RootState } from 'types' 3 | import { fetchPollsRequest } from 'modules/poll/actions' 4 | import { isLoading } from 'modules/poll/selectors' 5 | import HomePage from './HomePage' 6 | import { MapStateProps, MapDispatchProps } from './HomePage.types' 7 | import { AnyAction, Dispatch } from 'redux' 8 | import { getDecentralandPolls } from 'modules/ui/decentralandPolls/selectors' 9 | import { getDistrictPolls } from 'modules/ui/districtPolls/selectors' 10 | import { PollsRequestFilters } from 'modules/poll/types' 11 | import { push } from 'react-router-redux' 12 | 13 | const mapState = (state: RootState): MapStateProps => ({ 14 | isLoading: isLoading(state), 15 | decentralandPolls: getDecentralandPolls(state), 16 | districtPolls: getDistrictPolls(state) 17 | }) 18 | 19 | const mapDispatch = (dispatch: Dispatch): MapDispatchProps => ({ 20 | onFetchPolls: (pagination: PollsRequestFilters) => 21 | dispatch(fetchPollsRequest(pagination)), 22 | onNavigate: (location: string) => dispatch(push(location)) 23 | }) 24 | 25 | export default connect( 26 | mapState, 27 | mapDispatch 28 | )(HomePage) 29 | -------------------------------------------------------------------------------- /webapp/src/components/Token/Token.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { formatNumber } from '@dapps/lib/utils' 3 | import { Mana, Header, ManaProps } from 'decentraland-ui' 4 | import { Props } from './Token.types' 5 | import './Token.css' 6 | import { isDistrictToken } from 'modules/token/district_token/utils' 7 | import { t } from '@dapps/modules/translation/utils' 8 | 9 | export default class Token extends React.PureComponent { 10 | render() { 11 | const { token, amount, cell } = this.props 12 | const text = 13 | amount === undefined 14 | ? token.symbol 15 | : isDistrictToken(token) 16 | ? `${formatNumber(amount)} ${t('global.contributions')}` 17 | : token.symbol === 'MANA' 18 | ? formatNumber(amount) 19 | : `${formatNumber(amount)} ${token.symbol}` 20 | const className = cell ? 'Token cell' : 'Token text' 21 | const manaProps = cell ? ({ size: 'small', text: true } as ManaProps) : {} 22 | return token.symbol === 'MANA' ? ( 23 | 24 | {text} 25 | 26 | ) : ( 27 |
{text}
28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webapp/src/modules/poll/types.ts: -------------------------------------------------------------------------------- 1 | import { Option } from 'modules/option/types' 2 | import { Token } from 'modules/token/types' 3 | import { Vote } from 'modules/vote/types' 4 | import { Overwrite } from '@dapps/lib/types' 5 | 6 | export type FilterStatus = 'active' | 'expired' | 'all' 7 | export type FilterType = 'decentraland' | 'district' | 'all' 8 | 9 | export interface PollsRequestFilters { 10 | limit?: number 11 | offset?: number 12 | status?: FilterStatus 13 | type?: FilterType 14 | } 15 | 16 | export interface Poll { 17 | id: string 18 | title: string 19 | balance: number 20 | description?: string 21 | token_address: string 22 | submitter: string 23 | closes_at: number 24 | } 25 | 26 | export interface PollWithPointers extends Poll { 27 | vote_ids: string[] 28 | option_ids: string[] 29 | } 30 | 31 | export interface PollWithAssociations extends Poll { 32 | token: Token 33 | votes: Vote[] 34 | options: Option[] 35 | } 36 | 37 | export interface PollResponse 38 | extends Overwrite< 39 | PollWithAssociations, 40 | { balance: string; closes_at: string } 41 | > {} 42 | 43 | export interface PollsResponse { 44 | polls: PollResponse[] 45 | total: number 46 | } 47 | -------------------------------------------------------------------------------- /src/Poll/PollRequestFilters.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | import { getNumber, getString } from '../lib/extractFromReq' 3 | 4 | export const DEFAULT_LIMIT = 20 5 | export const MAX_LIMIT = 100 6 | export const DEFAULT_OFFSET = 0 7 | export const DEFAULT_STATUS = 'all' 8 | export const DEFAULT_TYPE = 'all' 9 | 10 | export type FilterStatus = 'active' | 'expired' | 'all' 11 | export type FilterType = 'district' | 'decentraland' | 'all' 12 | 13 | export const DEFAULT_FILTERS: FilterOptions = { 14 | limit: DEFAULT_LIMIT, 15 | offset: DEFAULT_OFFSET, 16 | status: DEFAULT_STATUS, 17 | type: DEFAULT_TYPE 18 | } 19 | 20 | export type FilterOptions = { 21 | limit?: number 22 | offset?: number 23 | status?: FilterStatus 24 | type?: FilterType 25 | } 26 | 27 | export class PollRequestFilters { 28 | req: Request 29 | 30 | constructor(req: Request) { 31 | this.req = req 32 | } 33 | 34 | sanitize() { 35 | return { 36 | limit: getNumber(this.req, 'limit', DEFAULT_LIMIT, 0, MAX_LIMIT), 37 | offset: getNumber(this.req, 'offset', DEFAULT_OFFSET, 0), 38 | status: getString(this.req, 'status', 'all'), 39 | type: getString(this.req, 'type', 'all') 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webapp/src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'decentraland-commons' 2 | import { BaseAPI } from '@dapps/lib/api' 3 | import { PollsRequestFilters } from 'modules/poll/types' 4 | 5 | const URL = env.get('REACT_APP_API_URL', '') 6 | 7 | export class API extends BaseAPI { 8 | fetchTokens() { 9 | return this.request('get', '/tokens', {}) 10 | } 11 | 12 | fetchPolls(filters: PollsRequestFilters) { 13 | return this.request('get', '/polls', filters) 14 | } 15 | 16 | fetchPoll(id: string) { 17 | return this.request('get', `/polls/${id}`, {}) 18 | } 19 | 20 | fetchPollOptions(id: string) { 21 | return this.request('get', `/polls/${id}/options`, {}) 22 | } 23 | 24 | fetchPollVotes(id: string) { 25 | return this.request('get', `/polls/${id}/votes`, {}) 26 | } 27 | 28 | fetchTranslations(locale: string) { 29 | return this.request('get', `/translations/${locale}`, {}) 30 | } 31 | 32 | createVote(message: string, signature: string, id?: string) { 33 | return this.request('post', '/votes', { message, signature, id }) 34 | } 35 | 36 | fetchAccountBalances(address: string) { 37 | return this.request('get', `/accountBalances/${address}`, {}) 38 | } 39 | } 40 | 41 | export const api = new API(URL) 42 | -------------------------------------------------------------------------------- /src/Receipt/Receipt.router.ts: -------------------------------------------------------------------------------- 1 | import { server } from 'decentraland-server' 2 | import * as express from 'express' 3 | 4 | import { Router } from '../lib' 5 | import { Receipt } from './Receipt.model' 6 | import { ReceiptAttributes } from './Receipt.types' 7 | 8 | export class ReceiptRouter extends Router { 9 | mount() { 10 | /** 11 | * Returns all receipts 12 | */ 13 | this.app.get('/receipts', server.handleRequest(this.getReceipts)) 14 | 15 | /** 16 | * Returns a receipt by id 17 | */ 18 | this.app.get('/receipts/:id', server.handleRequest(this.getReceipt)) 19 | 20 | /** 21 | * Returns account receipts 22 | */ 23 | this.app.get( 24 | '/accounts/:address/receipts', 25 | server.handleRequest(this.getAccountReceipts) 26 | ) 27 | } 28 | 29 | async getReceipts() { 30 | return Receipt.find() 31 | } 32 | 33 | async getReceipt(req: express.Request) { 34 | const id = server.extractFromReq(req, 'id') 35 | return Receipt.findOne(id) 36 | } 37 | 38 | async getAccountReceipts(req: express.Request) { 39 | const address = server.extractFromReq(req, 'address') 40 | return Receipt.find({ account_address: address }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollDetailPage.types.ts: -------------------------------------------------------------------------------- 1 | import { match } from 'react-router' 2 | import { PollWithAssociations } from 'modules/poll/types' 3 | import { Wallet } from 'modules/wallet/types' 4 | import { Vote } from 'modules/vote/types' 5 | import { Option } from 'modules/option/types' 6 | import { Token } from 'modules/token/types' 7 | 8 | export type URLParams = { 9 | id: string 10 | } 11 | 12 | export type Tally = { 13 | [optionId: string]: Result 14 | } 15 | 16 | export type Result = { 17 | votes: number 18 | option: Option 19 | winner: boolean 20 | percentage: number 21 | token?: Token 22 | } 23 | 24 | export type Props = { 25 | match: match 26 | pollId: string 27 | poll: PollWithAssociations | null 28 | wallet: Wallet 29 | currentVote: Vote | null 30 | isLoading: boolean 31 | hasError: boolean 32 | isConnected: boolean 33 | onFetchPoll: Function 34 | onNavigate: Function 35 | } 36 | 37 | export type State = { 38 | activePage: number 39 | } 40 | 41 | export type MapStateProps = Pick< 42 | Props, 43 | | 'pollId' 44 | | 'poll' 45 | | 'wallet' 46 | | 'currentVote' 47 | | 'isLoading' 48 | | 'hasError' 49 | | 'isConnected' 50 | > 51 | 52 | export type MapDispatchProps = Pick 53 | -------------------------------------------------------------------------------- /migrations/1527706985005_polls-create.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate' 2 | import { Poll } from '../src/Poll' 3 | import { Token } from '../src/Token' 4 | 5 | const tableName = Poll.tableName 6 | 7 | export const up = (pgm: MigrationBuilder) => { 8 | pgm.createTable( 9 | tableName, 10 | { 11 | id: { 12 | type: 'UUID', 13 | default: pgm.func('uuid_generate_v4()'), 14 | primaryKey: true, 15 | notNull: true, 16 | comment: null 17 | }, 18 | title: { type: 'TEXT', notNull: true, comment: null }, 19 | description: 'TEXT', 20 | token_address: { 21 | type: 'TEXT', 22 | references: Token.tableName, 23 | notNull: true, 24 | comment: null 25 | }, 26 | balance: { type: 'DECIMAL', notNull: true, default: 0, comment: null }, 27 | submitter: { type: 'TEXT', notNull: true, comment: null }, 28 | closes_at: { type: 'BIGINT', notNull: true, comment: null }, 29 | created_at: { type: 'TIMESTAMP', notNull: true, comment: null }, 30 | updated_at: { type: 'TIMESTAMP', comment: null } 31 | }, 32 | { ifNotExists: true, comment: null } 33 | ) 34 | } 35 | 36 | export const down = (pgm: MigrationBuilder) => { 37 | pgm.dropTable(tableName, {}) 38 | } 39 | -------------------------------------------------------------------------------- /src/Token/DistrictToken/DistrictToken.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../Token.model' 2 | import { TokenAttributes } from '../Token.types' 3 | 4 | export const DISTRICT_TOKEN: TokenAttributes = Object.freeze({ 5 | address: 'district-token-address', 6 | name: 'DistrictToken', 7 | symbol: 'DT' 8 | }) 9 | 10 | export class DistrictToken extends Token { 11 | constructor(name: string) { 12 | const symbol = DistrictToken.toSymbol(name) 13 | 14 | super({ 15 | address: `${DISTRICT_TOKEN.address}-${symbol}`, 16 | name, 17 | symbol 18 | }) 19 | } 20 | 21 | static isValid(token: TokenAttributes): boolean { 22 | return this.isAddress(token.address) 23 | } 24 | 25 | static isAddress(address: string): boolean { 26 | return address.search(DISTRICT_TOKEN.address) !== -1 27 | } 28 | 29 | static toSymbol(name: string) { 30 | const symbolLen = 4 31 | 32 | const slug = name.replace(/[^\s\w]+/g, '') 33 | let parts = slug.split(' ') 34 | 35 | if (parts.length < symbolLen) { 36 | const filler = Array.from(slug).reverse() 37 | parts = parts.concat(filler) 38 | } 39 | 40 | parts = parts.filter(str => !!str) 41 | 42 | return parts 43 | .map(str => str[0].toUpperCase()) 44 | .slice(0, symbolLen) 45 | .join('') 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /webapp/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as router } from 'react-router-redux' 3 | 4 | import { translationReducer as translation } from '@dapps/modules/translation/reducer' 5 | import { locationReducer as location } from '@dapps/modules/location/reducer' 6 | import { 7 | storageReducer as storage, 8 | storageReducerWrapper 9 | } from '@dapps/modules/storage/reducer' 10 | 11 | import { RootState } from 'types' 12 | import { accountBalanceReducer as accountBalance } from 'modules/accountBalance/reducer' 13 | import { optionReducer as option } from 'modules/option/reducer' 14 | import { pollReducer as poll } from 'modules/poll/reducer' 15 | import { tokenReducer as token } from 'modules/token/reducer' 16 | import { voteReducer as vote } from 'modules/vote/reducer' 17 | import { walletReducer as wallet } from 'modules/wallet/reducer' 18 | import { uiReducer as ui } from 'modules/ui/reducer' 19 | 20 | // TODO: Consider spliting individual reducers into { data, loading, error } 21 | export const rootReducer = storageReducerWrapper( 22 | combineReducers({ 23 | accountBalance, 24 | option, 25 | poll, 26 | token, 27 | location, 28 | translation, 29 | router, 30 | storage, 31 | vote, 32 | wallet, 33 | ui 34 | }) 35 | ) 36 | -------------------------------------------------------------------------------- /webapp/src/types.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareAPI, AnyAction, Reducer, Store } from 'redux' 2 | import { RouterState } from 'react-router-redux' 3 | import { AccountBalanceState } from 'modules/accountBalance/reducer' 4 | import { StorageState } from '@dapps/modules/storage/reducer' 5 | import { TranslationState } from '@dapps/modules/translation/reducer' 6 | import { WalletState } from 'modules/wallet/reducer' 7 | import { OptionState } from 'modules/option/reducer' 8 | import { PollState } from 'modules/poll/reducer' 9 | import { TokenState } from 'modules/token/reducer' 10 | import { VoteState } from 'modules/vote/reducer' 11 | import { UIState } from 'modules/ui/reducer' 12 | 13 | export type RootState = { 14 | accountBalance: AccountBalanceState 15 | router: RouterState 16 | option: OptionState 17 | poll: PollState 18 | storage: StorageState 19 | token: TokenState 20 | translation: TranslationState 21 | vote: VoteState 22 | wallet: WalletState 23 | ui: UIState 24 | } 25 | 26 | export type RootStore = Store 27 | 28 | export interface RootDispatch { 29 | (action: A): A 30 | } 31 | 32 | export type RootMiddleware = ( 33 | store: MiddlewareAPI 34 | ) => (next: RootDispatch) => (action: AnyAction) => any 35 | 36 | export type RootReducer = Reducer 37 | -------------------------------------------------------------------------------- /src/Translation/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "poll": "投票", 4 | "polls": "投票", 5 | "votes": "投票", 6 | "weight": "权重", 7 | "view_more": "查看更多", 8 | "description": "说明", 9 | "contributions": "小区地块", 10 | "your_contributions": "您的小区地块数" 11 | }, 12 | "homepage": { 13 | "title": "帮助我们建立 Decentraland", 14 | "subtitle": "加入讨论", 15 | "cards": { 16 | "closed": "关闭", 17 | "time_left": "还剩 {time}" 18 | } 19 | }, 20 | "vote_page": { 21 | "vote": "投票", 22 | "cancel": "取消", 23 | "no_balance": "你没有 {symbol}", 24 | "voting_width": "投票", 25 | "no_contributions": "您没有小区地块" 26 | }, 27 | "polls_table": { 28 | "empty": "尚无内容", 29 | "votes": "投票数", 30 | "title": "标题", 31 | "all_polls": "所有投票", 32 | "active_polls": "正在进行的投票", 33 | "expired_polls": "已关闭的投票", 34 | "district_polls": "小区投票", 35 | "decentraland_polls": "Decentraland 投票" 36 | }, 37 | "poll_detail_page": { 38 | "vote": "投票", 39 | "when": "时间", 40 | "votes": "投票", 41 | "amount": "量", 42 | "address": "地址", 43 | "you_voted": "您投了 {option}", 44 | "cast_vote": "开始投票", 45 | "poll_closed": "此民意调查已结束", 46 | "stats": { 47 | "token": "通证", 48 | "finished": "已结束", 49 | "time_left": "剩余时间" 50 | }, 51 | "sign_in_link": "登录", 52 | "sign_in_message": "您需要 {sign_in_link} 后才能投票" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/extractFromReq.ts: -------------------------------------------------------------------------------- 1 | import { server } from 'decentraland-server' 2 | import { Request } from 'express' 3 | 4 | function getString(req: Request, param: string, defaultValue: any = null) { 5 | try { 6 | return server.extractFromReq(req, param) 7 | } catch (error) { 8 | return defaultValue 9 | } 10 | } 11 | 12 | function getBoolean( 13 | req: Request, 14 | param: string, 15 | defaultValue: boolean = false 16 | ) { 17 | let result = getString(req, param, defaultValue.toString()) 18 | if (result === 'true') { 19 | return true 20 | } 21 | if (result === 'false') { 22 | return false 23 | } 24 | 25 | throw new Error( 26 | `Invalid param "${param}", expected a boolean value but got "${result}"` 27 | ) 28 | } 29 | 30 | function getNumber( 31 | req: Request, 32 | param: string, 33 | defaultValue: number = 0, 34 | min?: number, 35 | max?: number 36 | ) { 37 | let result = getString(req, param, defaultValue.toString()) 38 | if (isNaN(Number(result))) { 39 | throw new Error( 40 | `Invalid param "${param}", expected a numeric value but got "${result}"` 41 | ) 42 | } 43 | result = Number(result) 44 | if (typeof min !== 'undefined') { 45 | result = Math.max(result, min) 46 | } 47 | if (typeof max !== 'undefined') { 48 | result = Math.min(result, max) 49 | } 50 | return result 51 | } 52 | 53 | export { getString, getBoolean, getNumber } 54 | -------------------------------------------------------------------------------- /src/Vote/Vote.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, SQL, raw } from 'decentraland-server' 2 | import { VoteAttributes, CastVote } from './Vote.types' 3 | import { Poll, PollQueries } from '../Poll' 4 | import { Option } from '../Option' 5 | import { Token } from '../Token' 6 | import { AccountBalanceAttributes } from '../AccountBalance' 7 | 8 | export class Vote extends Model { 9 | static tableName = 'votes' 10 | 11 | static async findCastVoteById(id: string) { 12 | return this.query(SQL` 13 | SELECT v.*, 14 | row_to_json(p.*) as poll, 15 | row_to_json(o.*) as option, 16 | row_to_json(t.*) as token 17 | FROM ${raw(this.tableName)} v 18 | JOIN ${raw(Poll.tableName)} p ON p.id = v.poll_id 19 | JOIN ${raw(Option.tableName)} o ON o.id = v.option_id 20 | JOIN ${raw(Token.tableName)} t ON t.address = p.token_address 21 | WHERE v.id = ${id}`) 22 | } 23 | 24 | static async updateBalance( 25 | account: AccountBalanceAttributes, 26 | balance: number | string 27 | ) { 28 | return this.query(SQL` 29 | UPDATE ${raw(this.tableName)} v 30 | SET account_balance = ${balance} 31 | FROM ${raw(Poll.tableName)} p 32 | ${PollQueries.whereStatusAndType({ 33 | status: 'active' 34 | })} 35 | AND v.account_address = ${account.address} 36 | AND v.poll_id = p.id 37 | AND p.token_address = ${account.token_address}`) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Translation/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "poll": "公共投票", 4 | "polls": "投票", 5 | "votes": "票数", 6 | "weight": "ウエイト", 7 | "view_more": "もっと見る", 8 | "description": "説明", 9 | "contributions": "貢献区", 10 | "your_contributions": "あなたの貢献区" 11 | }, 12 | "homepage": { 13 | "title": "Decentralandの構築に参加しましょう", 14 | "subtitle": "投票を通じてディスカッションに参加できます。", 15 | "cards": { 16 | "closed": "締切済", 17 | "time_left": "残り時間{time}" 18 | } 19 | }, 20 | "vote_page": { 21 | "vote": "投票する", 22 | "cancel": "キャンセル", 23 | "no_balance": "あなたは{symbol}を持っていません", 24 | "voting_width": "投票する", 25 | "no_contributions": "あなたの貢献はありません" 26 | }, 27 | "polls_table": { 28 | "empty": "おっと...ここには何もありません。", 29 | "votes": "票数", 30 | "title": "タイトル", 31 | "all_polls": "すべての投票", 32 | "active_polls": "受付中の投票", 33 | "expired_polls": "締切済の投票", 34 | "district_polls": "ディストリクト投票", 35 | "decentraland_polls": "Decentraland公共投票" 36 | }, 37 | "poll_detail_page": { 38 | "vote": "投票", 39 | "when": "投票日", 40 | "votes": "投票", 41 | "amount": "量", 42 | "address": "アドレス", 43 | "you_voted": "あなたは{option}に投票しました", 44 | "cast_vote": "投票する", 45 | "poll_closed": "この投票は締め切りました", 46 | "stats": { 47 | "token": "トークン", 48 | "finished": "終了", 49 | "time_left": "残り時間" 50 | }, 51 | "sign_in_link": "サインイン済み", 52 | "sign_in_message": "投票するには{sign_in_link}する必要があります" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Translation/locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "poll": "설문조사", 4 | "polls": "설문조사들", 5 | "votes": "투표들", 6 | "weight": "가중치", 7 | "view_more": "더보기", 8 | "description": "상세 내용", 9 | "contributions": "기여", 10 | "your_contributions": "당신의 기여들" 11 | }, 12 | "homepage": { 13 | "title": "함께 만들어 가는 Decentraland", 14 | "subtitle": "투표에 참여해주세요!", 15 | "cards": { 16 | "closed": "종료된 투표", 17 | "time_left": "남은 시간 {time}" 18 | } 19 | }, 20 | "vote_page": { 21 | "vote": "투표하다", 22 | "cancel": "취소", 23 | "no_balance": "당신은 {symbol} 를 가지고 있지 않습니다.", 24 | "voting_width": "투표 가중치", 25 | "no_contributions": "당신은 기부한 내역이 없습니다." 26 | }, 27 | "polls_table": { 28 | "empty": "죄송합니다. 아무것도 없습니다.", 29 | "votes": "투표들", 30 | "title": "제목", 31 | "all_polls": "모든 설문조사", 32 | "active_polls": "진행중인 설문조사", 33 | "expired_polls": "종료된 설문조사", 34 | "district_polls": "지역구 설문조사", 35 | "decentraland_polls": "Decentraland 설문조사" 36 | }, 37 | "poll_detail_page": { 38 | "vote": "투표결과", 39 | "when": "투표날짜", 40 | "votes": "진행한 투표들", 41 | "amount": "양", 42 | "address": "주소", 43 | "you_voted": "당신은 이미 투표 하셨습니다 {option}.", 44 | "cast_vote": "투표하기", 45 | "poll_closed": "이 설문 조사는 마감되었습니다.", 46 | "stats": { 47 | "token": "토큰", 48 | "finished": "종료된 투표날짜.", 49 | "time_left": "남은 시간" 50 | }, 51 | "sign_in_link": "로그인", 52 | "sign_in_message": "투표 하기 위해서는 {sign_in_link} 상태여야 합니다." 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /webapp/src/components/PollsTable/PollsTable.css: -------------------------------------------------------------------------------- 1 | .PollsTable { 2 | margin-bottom: 96px; 3 | } 4 | 5 | .PollsTable .symbol { 6 | padding-right: 0.3em; 7 | } 8 | 9 | .PollsTable .pagination-wrapper { 10 | margin: 24px; 11 | display: flex; 12 | justify-content: center; 13 | } 14 | 15 | .PollsTable .mobile-header { 16 | display: none; 17 | font-weight: bold; 18 | } 19 | 20 | .PollsTable .empty { 21 | color: var(--secondary-text); 22 | margin: 20px; 23 | text-align: center; 24 | } 25 | 26 | .PollsTable .finished .title a { 27 | color: var(--disabled-text); 28 | } 29 | 30 | .PollsTable .finished td, 31 | .PollsTable .finished td div { 32 | color: var(--disabled-text) !important; 33 | } 34 | 35 | .PollsTable .ui.basic.table tbody tr:hover { 36 | cursor: pointer; 37 | background-color: var(--hover); 38 | } 39 | 40 | .PollsTable .ui.basic.table tbody tr td:first-child { 41 | transition: padding 0.2s ease-in-out; 42 | } 43 | 44 | .PollsTable .ui.basic.table tbody tr:hover td:first-child { 45 | padding-left: 16px; 46 | } 47 | 48 | .PollsTable .ongoing:hover .title a { 49 | color: var(--text); 50 | } 51 | 52 | .PollsTable .finished .title a { 53 | font-weight: normal; 54 | } 55 | 56 | @media (max-width: 768px) { 57 | .PollsTable .ui.basic.table thead tr { 58 | display: none !important; 59 | } 60 | 61 | .PollsTable .mobile-header { 62 | display: inline; 63 | } 64 | 65 | .PollsTable .ui.pagination.menu .item { 66 | margin-left: 8px; 67 | min-width: 15px; 68 | padding: 12px; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollProgress/PollOption/PollOption.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Props } from './PollOptions.types' 3 | import { isDistrictToken } from 'modules/token/district_token/utils' 4 | import { t } from '@dapps/modules/translation/utils' 5 | 6 | export default class PollOption extends React.PureComponent { 7 | render() { 8 | const { winner, percentage, option, votes, token, position } = this.props 9 | let classes = `PollOption color-${position % 5}` 10 | if (winner) { 11 | classes += ' winner' 12 | } 13 | 14 | const hasSymbol = token && !isDistrictToken(token) 15 | 16 | const symbolBallon = { 17 | 'data-balloon': `${votes.toLocaleString()} ${token ? token.symbol : ''}`, 18 | 'data-balloon-pos': 'up' 19 | } 20 | 21 | const nonSymbolBalloon = { 22 | 'data-balloon': `${votes.toLocaleString()} ${t( 23 | 'global.contributions' 24 | ).toLocaleLowerCase()}`, 25 | 'data-balloon-pos': 'up' 26 | } 27 | 28 | const balloon = hasSymbol ? symbolBallon : nonSymbolBalloon 29 | 30 | return ( 31 |
36 |
37 |
38 | 39 | {option.value} 40 |  ( 41 | {percentage} 42 | %) 43 | 44 |
45 |
46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollDetailPage.container.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { isConnected } from '@dapps/modules/wallet/selectors' 3 | import { navigateTo } from '@dapps/modules/location/actions' 4 | import { RootState, RootDispatch } from 'types' 5 | import { fetchPollRequest } from 'modules/poll/actions' 6 | import { 7 | getPolls, 8 | isLoading as isPollLoading, 9 | getError 10 | } from 'modules/poll/selectors' 11 | import { getWallet } from 'modules/wallet/selectors' 12 | import { Wallet } from 'modules/wallet/types' 13 | import { findWalletVote } from 'modules/vote/utils' 14 | import { Props, MapStateProps, MapDispatchProps } from './PollDetailPage.types' 15 | import PollDetailPage from './PollDetailPage' 16 | 17 | const mapState = (state: RootState, ownProps: Props): MapStateProps => { 18 | const pollId = ownProps.match.params.id 19 | const isLoading = isPollLoading(state) 20 | const wallet = getWallet(state) as Wallet 21 | const polls = getPolls(state) 22 | 23 | const poll = polls[pollId] 24 | const currentVote = poll ? findWalletVote(wallet, poll.votes) : null 25 | 26 | return { 27 | pollId, 28 | poll, 29 | wallet, 30 | currentVote, 31 | isLoading, 32 | hasError: !!getError(state), 33 | isConnected: isConnected(state) 34 | } 35 | } 36 | 37 | const mapDispatch = (dispatch: RootDispatch): MapDispatchProps => ({ 38 | onFetchPoll: (id: string) => dispatch(fetchPollRequest(id)), 39 | onNavigate: (url: string) => dispatch(navigateTo(url)) 40 | }) 41 | 42 | export default connect( 43 | mapState, 44 | mapDispatch 45 | )(PollDetailPage) 46 | -------------------------------------------------------------------------------- /migrations/1529057165916_receipt-create.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate' 2 | import { Receipt } from '../src/Receipt' 3 | 4 | const tableName = Receipt.tableName 5 | 6 | export const up = (pgm: MigrationBuilder) => { 7 | pgm.createTable( 8 | tableName, 9 | { 10 | id: { 11 | type: 'UUID', 12 | default: pgm.func('uuid_generate_v4()'), 13 | primaryKey: true, 14 | comment: null 15 | }, 16 | server_signature: { type: 'TEXT', notNull: true, comment: null }, 17 | server_message: { type: 'TEXT', notNull: true, comment: null }, 18 | account_message: { type: 'TEXT', notNull: true, comment: null }, 19 | account_signature: { type: 'TEXT', notNull: true, comment: null }, 20 | account_address: { type: 'TEXT', notNull: true, comment: null }, 21 | option_value: { type: 'TEXT', notNull: true, comment: null }, 22 | vote_id: { 23 | type: 'UUID', 24 | notNull: true, 25 | comment: null 26 | }, 27 | nonce: { type: 'SERIAL', comment: null }, 28 | created_at: { type: 'TIMESTAMP', notNull: true, comment: null }, 29 | updated_at: { type: 'TIMESTAMP', comment: null } 30 | }, 31 | { ifNotExists: true, comment: null } 32 | ) 33 | 34 | pgm.createIndex(tableName, ['server_signature', 'server_message'], { 35 | unique: true 36 | }) 37 | pgm.createIndex(tableName, ['account_signature', 'account_message'], { 38 | unique: true 39 | }) 40 | pgm.createIndex(tableName, 'account_address') 41 | } 42 | 43 | export const down = (pgm: MigrationBuilder) => { 44 | pgm.dropTable(tableName, {}) 45 | } 46 | -------------------------------------------------------------------------------- /src/Translation/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "poll": "Poll", 4 | "polls": "Polls", 5 | "contributions": "Contributions", 6 | "your_contributions": "Your Contributions", 7 | "view_more": "View more", 8 | "description": "Description", 9 | "weight": "Weight", 10 | "votes": "Votes" 11 | }, 12 | "vote_page": { 13 | "vote": "Vote now", 14 | "cancel": "Cancel", 15 | "no_contributions": "You don't have any contributions", 16 | "no_balance": "You don't have any {symbol}", 17 | "voting_width": "Voting with" 18 | }, 19 | "homepage": { 20 | "title": "Help build Decentraland", 21 | "subtitle": "Join the discussion", 22 | "cards": { 23 | "time_left": "{time} left", 24 | "closed": "closed" 25 | } 26 | }, 27 | "polls_table": { 28 | "all_polls": "All polls", 29 | "decentraland_polls": "Decentraland polls", 30 | "district_polls": "District polls", 31 | "expired_polls": "Closed polls", 32 | "active_polls": "Ongoing polls", 33 | "title": "Title", 34 | "votes": "Votes", 35 | "empty": "Oops... nothing found here." 36 | }, 37 | "poll_detail_page": { 38 | "stats": { 39 | "token": "Token", 40 | "time_left": "Time Left", 41 | "finished": "Finished" 42 | }, 43 | "votes": "Votes", 44 | "when": "When", 45 | "address": "Address", 46 | "amount": "Amount", 47 | "vote": "Vote", 48 | "cast_vote": "Cast your vote", 49 | "you_voted": "You voted {option}", 50 | "sign_in_message": "You need to be {sign_in_link} to vote", 51 | "sign_in_link": "signed in", 52 | "poll_closed": "This poll is closed" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /migrations/1527707259148_votes-create.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate' 2 | import { Vote } from '../src/Vote' 3 | import { Poll } from '../src/Poll' 4 | import { Option } from '../src/Option' 5 | 6 | const tableName = Vote.tableName 7 | 8 | export const up = (pgm: MigrationBuilder) => { 9 | pgm.createTable( 10 | tableName, 11 | { 12 | id: { 13 | type: 'UUID', 14 | default: pgm.func('uuid_generate_v4()'), 15 | primaryKey: true, 16 | notNull: true, 17 | comment: null 18 | }, 19 | account_address: { type: 'TEXT', notNull: true, comment: null }, 20 | account_balance: { 21 | type: 'DECIMAL', 22 | notNull: true, 23 | default: '0', 24 | comment: null 25 | }, 26 | poll_id: { 27 | type: 'UUID', 28 | references: Poll.tableName, 29 | notNull: true, 30 | comment: null 31 | }, 32 | option_id: { 33 | type: 'UUID', 34 | references: Option.tableName, 35 | notNull: true, 36 | comment: null 37 | }, 38 | message: { type: 'TEXT', notNull: true, comment: null }, 39 | signature: { type: 'TEXT', notNull: true, comment: null }, 40 | created_at: { type: 'TIMESTAMP', notNull: true, comment: null }, 41 | updated_at: { type: 'TIMESTAMP', comment: null } 42 | }, 43 | { ifNotExists: true, comment: null } 44 | ) 45 | 46 | pgm.createIndex(tableName, ['account_address', 'poll_id'], { 47 | unique: true 48 | }) 49 | 50 | pgm.createIndex(tableName, 'poll_id') 51 | } 52 | 53 | export const down = (pgm: MigrationBuilder) => { 54 | pgm.dropTable(tableName, {}) 55 | } 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Translation/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "poll": "Sondage", 4 | "polls": "Sondages", 5 | "votes": "Votes", 6 | "weight": "Poids", 7 | "view_more": "Voir plus", 8 | "description": "Description", 9 | "contributions": "Contributions", 10 | "your_contributions": "Vos contributions" 11 | }, 12 | "homepage": { 13 | "title": "Aidez à construire Decentraland", 14 | "subtitle": "Rejoignez la discussion", 15 | "cards": { 16 | "closed": "Terminé", 17 | "time_left": "{time} restant" 18 | } 19 | }, 20 | "vote_page": { 21 | "vote": "Votez maintenant", 22 | "cancel": "Annuler", 23 | "no_balance": "Vous n'avez pas de {symbol}", 24 | "voting_width": "Voter avec", 25 | "no_contributions": "Vous n'avez aucune contribution" 26 | }, 27 | "polls_table": { 28 | "votes": "Votes", 29 | "title": "Titre", 30 | "empty": "Oups ... rien n'a été trouvé ici.", 31 | "all_polls": "Tous les sondages", 32 | "active_polls": "Sondages en cours", 33 | "expired_polls": "Sondages fermés", 34 | "district_polls": "Sondages de district", 35 | "decentraland_polls": "Sondages de Decentraland" 36 | }, 37 | "poll_detail_page": { 38 | "vote": "Vote", 39 | "when": "Date", 40 | "votes": "Votes", 41 | "amount": "Montant", 42 | "address": "Adresse", 43 | "you_voted": "Vous avez voté {option}", 44 | "cast_vote": "Voter", 45 | "poll_closed": "Ce sondage est fermé", 46 | "stats": { 47 | "token": "Jeton", 48 | "finished": "Fini", 49 | "time_left": "Temps restant" 50 | }, 51 | "sign_in_link": "connecté", 52 | "sign_in_message": "Vous devez être {sign_in_link} pour voter" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Translation/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "poll": "Encuesta", 4 | "votes": "Votos", 5 | "polls": "Encuestas", 6 | "weight": "Peso", 7 | "view_more": "Ver más", 8 | "description": "Descripción", 9 | "contributions": "Contribuciones", 10 | "your_contributions": "Tus Contribuciones" 11 | }, 12 | "homepage": { 13 | "title": "Ayúda a construir Decentraland", 14 | "subtitle": "Únete a la discusión", 15 | "cards": { 16 | "closed": "Cerrado", 17 | "time_left": "Quedan {time}" 18 | } 19 | }, 20 | "vote_page": { 21 | "vote": "Votar", 22 | "cancel": "Cancelar", 23 | "no_balance": "No tienes suficiente {symbol}", 24 | "voting_width": "Votando con", 25 | "no_contributions": "No tienes contribuciones" 26 | }, 27 | "polls_table": { 28 | "title": "Título", 29 | "votes": "Votos", 30 | "empty": "Vaya... no hay nada aquí.", 31 | "all_polls": "Todas las encuestas", 32 | "active_polls": "Encuestas en curso", 33 | "expired_polls": "Encuestas cerradas", 34 | "district_polls": "Encuestas de distritos", 35 | "decentraland_polls": "Encuestas de Decentraland" 36 | }, 37 | "poll_detail_page": { 38 | "when": "Fecha", 39 | "vote": "Voto", 40 | "votes": "Votos", 41 | "amount": "Cantidad", 42 | "address": "Dirección", 43 | "cast_vote": "Emitir voto", 44 | "you_voted": "Has votado {option}", 45 | "poll_closed": "Esta encuesta esta cerrada", 46 | "stats": { 47 | "token": "Token", 48 | "finished": "Finalizada", 49 | "time_left": "Tiempo disponible" 50 | }, 51 | "sign_in_link": "conectado", 52 | "sign_in_message": "Tienes que estar {sign_in_link} para votar" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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 | 8 | import { createAnalyticsMiddleware } from '@dapps/modules/analytics/middleware' 9 | import { createStorageMiddleware } from '@dapps/modules/storage/middleware' 10 | 11 | import { rootReducer } from './reducer' 12 | import { rootSaga } from './sagas' 13 | 14 | const composeEnhancers = 15 | (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 16 | 17 | const history = createHistory() 18 | 19 | const historyMiddleware = routerMiddleware(history) 20 | const sagasMiddleware = createSagasMiddleware() 21 | const loggerMiddleware = createLogger({ 22 | collapsed: () => true, 23 | predicate: (_: any, action) => 24 | env.isDevelopment() || action.type.includes('Failure') 25 | }) 26 | const analyticsMiddleware = createAnalyticsMiddleware( 27 | env.get('REACT_APP_SEGMENT_API_KEY') 28 | ) 29 | const { storageMiddleware, loadStorageMiddleware } = createStorageMiddleware({ 30 | storageKey: env.get('REACT_APP_LOCAL_STORAGE_KEY', 'decentraland-agora') 31 | }) 32 | 33 | const middleware = applyMiddleware( 34 | historyMiddleware, 35 | sagasMiddleware, 36 | loggerMiddleware, 37 | analyticsMiddleware, 38 | storageMiddleware 39 | ) 40 | const enhancer = composeEnhancers(middleware) 41 | const store = createStore(rootReducer, enhancer) 42 | 43 | sagasMiddleware.run(rootSaga) 44 | loadStorageMiddleware(store) 45 | 46 | if (env.isDevelopment()) { 47 | const _window = window as any 48 | _window.getState = store.getState 49 | } 50 | 51 | export { history, store } 52 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser' 2 | import * as express from 'express' 3 | import { env } from 'decentraland-commons' 4 | import { AppRouter } from './App' 5 | import { AccountBalanceRouter } from './AccountBalance' 6 | import { OptionRouter } from './Option' 7 | import { PollRouter } from './Poll' 8 | import { ReceiptRouter } from './Receipt' 9 | import { VoteRouter } from './Vote' 10 | import { TokenRouter } from './Token' 11 | import { TranslationRouter } from './Translation' 12 | import { db } from './database' 13 | 14 | env.load() 15 | 16 | const SERVER_PORT = env.get('SERVER_PORT', 5000) 17 | 18 | const app = express() 19 | 20 | app.use(bodyParser.urlencoded({ extended: false, limit: '2mb' })) 21 | app.use(bodyParser.json()) 22 | 23 | if (env.isDevelopment()) { 24 | app.use(function(_, res, next) { 25 | res.setHeader('Access-Control-Allow-Origin', '*') 26 | res.setHeader('Access-Control-Request-Method', '*') 27 | res.setHeader( 28 | 'Access-Control-Allow-Methods', 29 | 'OPTIONS, GET, POST, PUT, DELETE' 30 | ) 31 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type') 32 | 33 | next() 34 | }) 35 | } 36 | 37 | new AppRouter(app).mount() 38 | new AccountBalanceRouter(app).mount() 39 | new PollRouter(app).mount() 40 | new OptionRouter(app).mount() 41 | new VoteRouter(app).mount() 42 | new ReceiptRouter(app).mount() 43 | new TokenRouter(app).mount() 44 | new TranslationRouter(app).mount() 45 | 46 | /* Start the server only if run directly */ 47 | if (require.main === module) { 48 | startServer().catch(console.error) 49 | } 50 | 51 | async function startServer() { 52 | console.log('Connecting database') 53 | await db.connect() 54 | 55 | return app.listen(SERVER_PORT, () => 56 | console.log('Server running on port', SERVER_PORT) 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollProgress/PollProgress.css: -------------------------------------------------------------------------------- 1 | .PollProgress { 2 | display: flex; 3 | width: 100%; 4 | height: 42px; 5 | background-color: #7d8499; 6 | border-radius: 8px; 7 | } 8 | 9 | .PollProgress .PollOption:first-child .bg { 10 | border-radius: 8px 0px 0px 8px; 11 | } 12 | 13 | .PollProgress .PollOption:last-child .bg { 14 | border-radius: 0px 8px 8px 0px; 15 | } 16 | 17 | .PollProgress .mask { 18 | display: inline-flex; 19 | align-items: center; 20 | justify-content: center; 21 | position: relative; 22 | color: var(--text-on-primary); 23 | width: 100%; 24 | height: 100%; 25 | overflow: hidden; 26 | padding: 8px; 27 | } 28 | 29 | .PollProgress .bg { 30 | position: absolute; 31 | width: 100%; 32 | height: 100%; 33 | border-left: 2px solid var(--background); 34 | } 35 | 36 | .PollProgress .PollOption:first-child .bg { 37 | border-left: none; 38 | } 39 | 40 | .PollProgress .PollOption { 41 | min-width: calc(8px + 8px); 42 | cursor: default !important; 43 | } 44 | 45 | .PollProgress .PollOption.color-0 .bg { 46 | background: var(--night-time); 47 | } 48 | 49 | .PollProgress .PollOption.color-1 .bg { 50 | background: var(--luisxvi-violet); 51 | } 52 | 53 | .PollProgress .PollOption.color-2 .bg { 54 | background: var(--candy-purple); 55 | } 56 | 57 | .PollProgress .PollOption.color-3 .bg { 58 | background: var(--summer-red); 59 | } 60 | 61 | .PollProgress .PollOption.color-4 .bg { 62 | background: var(--oj-not-simpson); 63 | } 64 | 65 | .PollProgress .PollOption.winner { 66 | flex: 1 1 auto; 67 | } 68 | 69 | .PollProgress .text { 70 | z-index: 100; 71 | width: 100%; 72 | white-space: nowrap; 73 | overflow: hidden; 74 | text-overflow: ellipsis; 75 | text-align: center; 76 | color: var(--background); 77 | text-transform: uppercase; 78 | } 79 | -------------------------------------------------------------------------------- /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.flatten(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 | -------------------------------------------------------------------------------- /webapp/src/modules/vote/actions.ts: -------------------------------------------------------------------------------- 1 | import { action } from 'typesafe-actions' 2 | import { Vote, NewVote } from 'modules/vote/types' 3 | import { Wallet } from 'modules/wallet/types' 4 | 5 | // Fetch Poll Votes 6 | 7 | export const FETCH_POLL_VOTES_REQUEST = '[Request] Fetch Poll Votes' 8 | export const FETCH_POLL_VOTES_SUCCESS = '[Success] Fetch Poll Votes' 9 | export const FETCH_POLL_VOTES_FAILURE = '[Failure] Fetch Poll Votes' 10 | 11 | export const fetchVotesByPollIdRequest = (pollId: string) => 12 | action(FETCH_POLL_VOTES_REQUEST, { pollId }) 13 | export const fetchVotesByPollIdSuccess = (votes: Vote[], pollId: string) => 14 | action(FETCH_POLL_VOTES_SUCCESS, { votes, pollId }) 15 | export const fetchVotesByPollIdFailure = (error: string) => 16 | action(FETCH_POLL_VOTES_FAILURE, { error }) 17 | 18 | export type FetchPollVotesRequestAction = ReturnType< 19 | typeof fetchVotesByPollIdRequest 20 | > 21 | export type FetchPollVotesSuccessAction = ReturnType< 22 | typeof fetchVotesByPollIdSuccess 23 | > 24 | export type FetchPollVotesFailureAction = ReturnType< 25 | typeof fetchVotesByPollIdFailure 26 | > 27 | 28 | // Create Vote 29 | 30 | export const CREATE_VOTE_REQUEST = '[Request] Vote' 31 | export const CREATE_VOTE_SUCCESS = '[Success] Vote' 32 | export const CREATE_VOTE_FAILURE = '[Failure] Vote' 33 | 34 | export const createVoteRequest = (newVote: NewVote) => 35 | action(CREATE_VOTE_REQUEST, { newVote }) 36 | export const createVoteSuccess = (vote: Vote, wallet: Wallet) => 37 | action(CREATE_VOTE_SUCCESS, { vote, wallet }) 38 | export const createVoteFailure = (error: string) => 39 | action(CREATE_VOTE_FAILURE, { error }) 40 | 41 | export type CreateVoteRequestAction = ReturnType 42 | export type CreateVoteSuccessAction = ReturnType 43 | export type CreateVoteFailureAction = ReturnType 44 | -------------------------------------------------------------------------------- /webapp/src/components/VotePage/VotePage.container.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { RootDispatch, RootState } from 'types' 3 | import { isConnected } from '@dapps/modules/wallet/selectors' 4 | import { navigateTo, NavigateToAction } from '@dapps/modules/location/actions' 5 | import { getPolls, isLoading as isPollLoading } from 'modules/poll/selectors' 6 | import { getWallet } from 'modules/wallet/selectors' 7 | import { fetchPollRequest, FetchPollRequestAction } from 'modules/poll/actions' 8 | import { 9 | createVoteRequest, 10 | CreateVoteRequestAction 11 | } from 'modules/vote/actions' 12 | import { Wallet } from 'modules/wallet/types' 13 | import { NewVote } from 'modules/vote/types' 14 | import { findWalletVote } from 'modules/vote/utils' 15 | import { Props, MapStateProps, MapDispatchProps } from './VotePage.types' 16 | 17 | import VotePage from './VotePage' 18 | 19 | const mapState = (state: RootState, ownProps: Props): MapStateProps => { 20 | const pollId = ownProps.match.params.id 21 | const isLoading = isPollLoading(state) 22 | const wallet = getWallet(state) as Wallet 23 | const polls = getPolls(state) 24 | 25 | const poll = polls[pollId] || null 26 | const currentVote = poll ? findWalletVote(wallet, poll.votes) : null 27 | 28 | return { 29 | pollId, 30 | poll, 31 | wallet, 32 | currentVote, 33 | isLoading, 34 | isConnected: isConnected(state) 35 | } 36 | } 37 | 38 | const mapDispatch = ( 39 | dispatch: RootDispatch< 40 | FetchPollRequestAction | CreateVoteRequestAction | NavigateToAction 41 | > 42 | ): MapDispatchProps => ({ 43 | onFetchPoll: (id: string) => dispatch(fetchPollRequest(id)), 44 | onCreateVote: (newVote: NewVote) => dispatch(createVoteRequest(newVote)), 45 | onNavigate: (url: string) => dispatch(navigateTo(url)) 46 | }) 47 | 48 | export default connect( 49 | mapState, 50 | mapDispatch 51 | )(VotePage) 52 | -------------------------------------------------------------------------------- /webapp/src/modules/poll/sagas.ts: -------------------------------------------------------------------------------- 1 | import { call, put, takeEvery } from 'redux-saga/effects' 2 | import { 3 | fetchPollsSuccess, 4 | fetchPollsFailure, 5 | fetchPollSuccess, 6 | fetchPollFailure, 7 | FETCH_POLLS_REQUEST, 8 | FETCH_POLL_REQUEST, 9 | FetchPollRequestAction, 10 | FetchPollsRequestAction 11 | } from 'modules/poll/actions' 12 | import { 13 | Poll, 14 | PollResponse, 15 | PollsResponse, 16 | PollWithAssociations 17 | } from 'modules/poll/types' 18 | import { api } from 'lib/api' 19 | 20 | export function* pollSaga() { 21 | yield takeEvery(FETCH_POLLS_REQUEST, handlePollsRequest) 22 | yield takeEvery(FETCH_POLL_REQUEST, handlePollRequest) 23 | } 24 | 25 | function* handlePollsRequest(action: FetchPollsRequestAction) { 26 | try { 27 | const filters = action.payload 28 | const response: PollsResponse = yield call(() => api.fetchPolls(filters)) 29 | const polls: PollWithAssociations[] = response.polls.map( 30 | (poll: PollResponse) => ({ 31 | ...poll, 32 | balance: Number(poll.balance), 33 | closes_at: Number(poll.closes_at) 34 | }) 35 | ) 36 | yield put(fetchPollsSuccess(polls, response.total, filters)) 37 | } catch (error) { 38 | yield put(fetchPollsFailure(error.message)) 39 | } 40 | } 41 | 42 | function* handlePollRequest(action: FetchPollRequestAction) { 43 | const id = action.payload.id 44 | try { 45 | const pollResponse: PollResponse = yield call(() => api.fetchPoll(id)) 46 | if (!pollResponse) throw new Error(`Couldn't find poll ${id}`) 47 | const { token, votes, options, ...pollAttributes } = pollResponse 48 | 49 | const poll: Poll = { 50 | ...pollAttributes, 51 | closes_at: Number(pollAttributes.closes_at), 52 | balance: Number(pollAttributes.balance) 53 | } 54 | 55 | yield put(fetchPollSuccess(poll, token, votes, options)) 56 | } catch (error) { 57 | yield put(fetchPollFailure(error.message)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /webapp/src/components/PollsTable/PollsTable.container.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { withRouter } from 'react-router' 3 | import * as queryString from 'query-string' 4 | import PollsTable from './PollsTable' 5 | import { RootState } from 'types' 6 | import { 7 | Props, 8 | MapStateProps, 9 | MapDispatchProps, 10 | QueryParams 11 | } from './PollsTable.types' 12 | import { getPolls, getTotal } from 'modules/ui/polls/selectors' 13 | import { navigateTo } from 'decentraland-dapps/dist/modules/location/actions' 14 | import { locations } from 'locations' 15 | import { PollsRequestFilters, FilterStatus } from 'modules/poll/types' 16 | import { fetchPollsRequest } from 'modules/poll/actions' 17 | 18 | const mapState = (state: RootState, ownProps: Props): MapStateProps => { 19 | const queryParams: QueryParams = queryString.parse(ownProps.location.search) 20 | return { 21 | type: ownProps.type || queryParams.type || 'all', 22 | status: ownProps.status || queryParams.status || 'all', 23 | page: +queryParams.page || 1, 24 | polls: getPolls(state), 25 | totalRows: getTotal(state) 26 | } 27 | } 28 | 29 | const mapDispatch = (dispatch: any, ownProps: any): MapDispatchProps => { 30 | const queryParams: QueryParams = queryString.parse(ownProps.location.search) 31 | const type = ownProps.type || queryParams.type || 'all' 32 | const status = ownProps.status || queryParams.status || 'all' 33 | return { 34 | onPageChange: (page: number) => 35 | dispatch(navigateTo(locations.pollsTable(page, type, status))), 36 | onStatusChange: (status: FilterStatus) => 37 | dispatch(navigateTo(locations.pollsTable(1, type, status))), 38 | onFetchPolls: (pagination: PollsRequestFilters) => 39 | dispatch(fetchPollsRequest(pagination)), 40 | onNavigate: (location: string) => dispatch(navigateTo(location)) 41 | } 42 | } 43 | 44 | export default withRouter( 45 | connect( 46 | mapState, 47 | mapDispatch 48 | )(PollsTable) 49 | ) 50 | -------------------------------------------------------------------------------- /webapp/src/components/PollDetailPage/PollDetailPage.css: -------------------------------------------------------------------------------- 1 | .PollDetailPage { 2 | margin-bottom: 96px; 3 | } 4 | 5 | .PollDetailPage .description { 6 | font-size: 17px; 7 | line-height: 26px; 8 | letter-spacing: -0.2px; 9 | margin-top: 8px; 10 | } 11 | 12 | .PollDetailPage .description-wrapper { 13 | margin-bottom: 48px; 14 | overflow: hidden; 15 | text-overflow: ellipsis; 16 | } 17 | 18 | .PollDetailPage .description-wrapper a { 19 | color: var(--primary); 20 | } 21 | 22 | .PollDetailPage .description-wrapper a:hover { 23 | color: var(--primary-dark); 24 | } 25 | 26 | .PollDetailPage > .stats { 27 | margin-bottom: 48px; 28 | } 29 | 30 | .PollDetailPage .row { 31 | display: flex; 32 | align-items: flex-start; 33 | } 34 | 35 | .PollDetailPage .progress { 36 | flex: 1 0 auto; 37 | max-width: calc(100% - 262px); 38 | } 39 | 40 | .PollDetailPage .main.row { 41 | margin-bottom: 18px; 42 | } 43 | 44 | .PollDetailPage .sub.row { 45 | justify-content: space-between; 46 | } 47 | 48 | .PollDetailPage .options { 49 | display: flex; 50 | flex-wrap: wrap; 51 | } 52 | 53 | .PollDetailPage .votes { 54 | margin-top: 80px; 55 | } 56 | 57 | .PollDetailPage .votes-pagination { 58 | text-align: center; 59 | margin-top: 40px; 60 | } 61 | 62 | .PollDetailPage .mobile-header { 63 | display: none; 64 | font-weight: bold; 65 | } 66 | 67 | .PollDetailPage .sign-in-message { 68 | color: var(--secondary-text); 69 | } 70 | 71 | @media (max-width: 768px) { 72 | .PollDetailPage .votes { 73 | margin-top: 0px; 74 | } 75 | 76 | .PollDetailPage .ui.basic.table thead tr { 77 | display: none !important; 78 | } 79 | 80 | .PollDetailPage .ui.pagination.menu .item { 81 | margin-left: 8px; 82 | min-width: 15px; 83 | padding: 12px; 84 | } 85 | 86 | .PollDetailPage .mobile-header { 87 | display: inline; 88 | } 89 | 90 | .ui.table:not(.unstackable) td:first-child, 91 | .ui.table:not(.unstackable) th:first-child { 92 | font-weight: normal; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - install 3 | - build 4 | - deploy 5 | 6 | services: 7 | - docker:dind 8 | 9 | # Cache modules in between jobs 10 | cache: 11 | key: ${CI_COMMIT_REF_SLUG} 12 | key: ${CI_COMMIT_REF_NAME} 13 | paths: 14 | - .npm/ 15 | 16 | build:code: 17 | stage: install 18 | image: decentraland/ci-node:latest 19 | only: 20 | - master 21 | - staging 22 | - release 23 | artifacts: 24 | paths: 25 | - lib 26 | - public 27 | - static 28 | - src 29 | - package.json 30 | - webapp/build 31 | when: on_success 32 | expire_in: 30 days 33 | script: 34 | - source dcl-env 35 | # Build Front 36 | - cd webapp 37 | - cp ../.ci/.env.${ENVIRONMENT} .env 38 | - npm install --cache .npm --prefer-offline 39 | - npm run build 40 | # Build Backend 41 | - cd .. 42 | - npm install --cache .npm --prefer-offline 43 | - npm run build:tsc 44 | 45 | build:docker: 46 | stage: build 47 | image: docker:latest 48 | dependencies: 49 | - build:code 50 | only: 51 | - master 52 | - staging 53 | - release 54 | services: 55 | - docker:dind 56 | before_script: 57 | - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 58 | script: 59 | - DOCKER_BUILDKIT=1 docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" . 60 | - DOCKER_BUILDKIT=1 docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" . 61 | - DOCKER_BUILDKIT=1 docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" 62 | - DOCKER_BUILDKIT=1 docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" 63 | 64 | deploy: 65 | stage: deploy 66 | image: decentraland/ci-node:latest 67 | dependencies: 68 | - build:code 69 | - build:docker 70 | only: 71 | - master 72 | - staging 73 | - release 74 | script: 75 | - source dcl-env 76 | - cd .ci && npm install --cache ../.npm --prefer-offline 77 | - dcl-lock-sync 78 | - dcl-up website-agora 79 | - cd .. 80 | - dcl-upload webapp/build 81 | - dcl-sync-release 82 | - dcl-announce-docker-build 83 | 84 | -------------------------------------------------------------------------------- /webapp/src/modules/poll/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from 'types' 3 | import { utils } from 'decentraland-commons' 4 | import { PollWithAssociations, Poll } from 'modules/poll/types' 5 | import { getData as getOptions } from 'modules/option/selectors' 6 | import { getData as getTokens } from 'modules/token/selectors' 7 | import { getData as getVotes } from 'modules/vote/selectors' 8 | import { ModelById } from '@dapps/lib/types' 9 | import { PollState } from 'modules/poll/reducer' 10 | import { OptionState } from 'modules/option/reducer' 11 | import { VoteState } from 'modules/vote/reducer' 12 | import { TokenState } from 'modules/token/reducer' 13 | 14 | export const getState: (state: RootState) => PollState = state => state.poll 15 | 16 | export const getData: (state: RootState) => PollState['data'] = state => 17 | getState(state).data 18 | 19 | export const isLoading: (state: RootState) => boolean = state => 20 | getState(state).loading.length > 0 21 | 22 | export const getError: (state: RootState) => PollState['error'] = state => 23 | getState(state).error 24 | 25 | export const getPolls = createSelector< 26 | RootState, 27 | PollState['data'], 28 | OptionState['data'], 29 | TokenState['data'], 30 | VoteState['data'], 31 | ModelById 32 | >(getData, getOptions, getTokens, getVotes, (polls, options, tokens, votes) => 33 | Object.keys(polls).reduce>( 34 | (result, pollId) => { 35 | const poll = polls[pollId] 36 | 37 | const fullPoll: PollWithAssociations = { 38 | ...utils.omit(poll, ['option_ids', 'vote_ids']), 39 | token: tokens[poll.token_address], 40 | 41 | votes: poll.vote_ids 42 | .map(voteId => votes[voteId]) 43 | .filter(vote => !!vote), 44 | 45 | options: poll.option_ids 46 | .map(optionIds => options[optionIds]) 47 | .filter(option => !!option) 48 | } 49 | 50 | return { 51 | ...result, 52 | [pollId]: fullPoll 53 | } 54 | }, 55 | {} 56 | ) 57 | ) 58 | -------------------------------------------------------------------------------- /.ci/index.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from '@pulumi/pulumi' 2 | import { createFargateTask } from 'dcl-ops-lib/createFargateTask' 3 | import { env, envTLD } from 'dcl-ops-lib/domain' 4 | import { acceptDbSecurityGroup } from 'dcl-ops-lib/acceptDb' 5 | import { buildStatic } from 'dcl-ops-lib/buildStatic' 6 | 7 | export = async function main() { 8 | const config = new pulumi.Config() 9 | 10 | const revision = process.env['CI_COMMIT_SHA'] 11 | const image = `decentraland/agora:${revision}` 12 | 13 | 14 | const hostname = 'agora-api.decentraland.' + envTLD 15 | 16 | const agoraApi = await createFargateTask( 17 | `agora-api`, 18 | image, 19 | 5000, 20 | [ 21 | { name: 'hostname', value: `agora-api-${env}` }, 22 | { name: 'name', value: `agora-api-${env}` }, 23 | { name: 'NODE_ENV', value: 'development' }, 24 | { name: 'MONITOR_BALANCES_DELAY', value: '100000' }, 25 | { name: 'SERVER_PORT', value: '5000' }, 26 | { name: 'MANA_TOKEN_CONTRACT_ADDRESS', value: '0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb' }, 27 | { name: 'CONNECTION_STRING', value: config.requireSecret('CONNECTION_STRING') }, 28 | { 29 | name: 'SERVER_SIGNING_KEY', 30 | value: config.requireSecret('SERVER_SIGNING_KEY'), 31 | }, 32 | { 33 | name: 'RPC_URL', 34 | value: config.requireSecret('RPC_URL'), 35 | } 36 | ], 37 | hostname, 38 | { 39 | // @ts-ignore 40 | healthCheck: { 41 | path: '/status', 42 | interval: 60, 43 | timeout: 10, 44 | unhealthyThreshold: 10, 45 | healthyThreshold: 3 46 | }, 47 | version: '1', 48 | memoryReservation: 1024, 49 | securityGroups: [(await acceptDbSecurityGroup()).id], 50 | } 51 | ) 52 | 53 | const agoraFront = buildStatic({ 54 | domain: `agora.decentraland.${env === 'prd' ? 'org' : envTLD}`, 55 | defaultPath: 'index.html', 56 | }) 57 | 58 | return { 59 | publicUrl: agoraApi.endpoint, 60 | cloudfrontDistribution: agoraFront.cloudfrontDistribution, 61 | bucketName: agoraFront.contentBucket, 62 | } 63 | } -------------------------------------------------------------------------------- /webapp/src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Switch, Route, Redirect } from 'react-router-dom' 3 | 4 | import App from '@dapps/containers/App' 5 | import SignInPage from '@dapps/containers/SignInPage' 6 | 7 | import { locations } from 'locations' 8 | import Hero from 'components/Hero' 9 | import HomePage from 'components/HomePage' 10 | import PollDetailPage from 'components/PollDetailPage' 11 | import VotePage from 'components/VotePage' 12 | import PollsTable from 'components/PollsTable' 13 | import { Container } from 'decentraland-ui' 14 | 15 | export default class Routes extends React.Component { 16 | withApp = (Component: React.ComponentType) => (props: any) => ( 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | 24 | withHero = (Component: React.ComponentType) => (props: any) => ( 25 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | 38 | render() { 39 | return ( 40 | 41 | 46 | 51 | 56 | 61 | 66 | 67 | 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Poll/Poll.queries.ts: -------------------------------------------------------------------------------- 1 | import { SQL, SQLStatement, raw } from 'decentraland-server' 2 | import { Token, DISTRICT_TOKEN } from '../Token' 3 | import { Option } from '../Option' 4 | import { Vote } from '../Vote' 5 | import { ModelQueries } from '../lib' 6 | import { 7 | FilterStatus, 8 | FilterType, 9 | DEFAULT_STATUS, 10 | DEFAULT_TYPE 11 | } from './PollRequestFilters' 12 | 13 | export const PollQueries = Object.freeze({ 14 | whereStatusAndType( 15 | { status, type }: { status?: FilterStatus; type?: FilterType } = { 16 | status: DEFAULT_STATUS, 17 | type: DEFAULT_TYPE 18 | } 19 | ) { 20 | let statusQuery 21 | let typeQuery 22 | 23 | if (status === 'active') { 24 | statusQuery = SQL`closes_at > extract(epoch from now()) * 1000` 25 | } else if (status === 'expired') { 26 | statusQuery = SQL`closes_at <= extract(epoch from now()) * 1000` 27 | } 28 | 29 | if (type === 'district') { 30 | typeQuery = SQL`token_address LIKE '${SQL.raw(DISTRICT_TOKEN.address)}-%'` 31 | } else if (type === 'decentraland') { 32 | typeQuery = SQL`token_address LIKE '0x%'` 33 | } 34 | 35 | if (statusQuery && typeQuery) { 36 | return SQL`WHERE ${statusQuery} AND ${typeQuery}` 37 | } else if (statusQuery) { 38 | return SQL`WHERE ${statusQuery}` 39 | } else if (typeQuery) { 40 | return SQL`WHERE ${typeQuery}` 41 | } else { 42 | return SQL`` 43 | } 44 | }, 45 | findWithAssociations: (whereStatement: SQLStatement = SQL``): SQLStatement => 46 | SQL` 47 | SELECT p.*, 48 | row_to_json(t.*) as token, 49 | (SELECT ${ModelQueries.jsonAgg('v', { 50 | orderColumn: 'timestamp DESC' 51 | })} AS votes FROM ${raw(Vote.tableName)} v WHERE v.poll_id = p.id), 52 | (SELECT ${ModelQueries.jsonAgg('o', { 53 | orderColumn: 'value ASC' 54 | })} AS options FROM ${raw(Option.tableName)} o WHERE o.poll_id = p.id) 55 | FROM polls p 56 | JOIN ${raw(Token.tableName)} t ON t.address = p.token_address 57 | ${whereStatement} 58 | GROUP BY p.id, t.address` 59 | }) 60 | -------------------------------------------------------------------------------- /webapp/src/modules/poll/actions.ts: -------------------------------------------------------------------------------- 1 | import { action } from 'typesafe-actions' 2 | import { 3 | Poll, 4 | PollWithAssociations, 5 | PollsRequestFilters 6 | } from 'modules/poll/types' 7 | import { Token } from 'modules/token/types' 8 | import { Option } from 'modules/option/types' 9 | import { Vote } from 'modules/vote/types' 10 | 11 | // Fetch Polls 12 | 13 | export const FETCH_POLLS_REQUEST = '[Request] Fetch Polls' 14 | export const FETCH_POLLS_SUCCESS = '[Success] Fetch Polls' 15 | export const FETCH_POLLS_FAILURE = '[Failure] Fetch Polls' 16 | 17 | export const fetchPollsRequest = ({ 18 | limit, 19 | offset, 20 | status, 21 | type 22 | }: PollsRequestFilters = {}) => 23 | action(FETCH_POLLS_REQUEST, { 24 | limit, 25 | offset, 26 | status, 27 | type 28 | }) 29 | export const fetchPollsSuccess = ( 30 | polls: PollWithAssociations[], 31 | total: number, 32 | filters: PollsRequestFilters 33 | ) => action(FETCH_POLLS_SUCCESS, { polls, total, filters }) 34 | export const fetchPollsFailure = (error: string) => 35 | action(FETCH_POLLS_FAILURE, { error }) 36 | 37 | export type FetchPollsRequestAction = ReturnType 38 | export type FetchPollsSuccessAction = ReturnType 39 | export type FetchPollsFailureAction = ReturnType 40 | 41 | // Fetch Poll 42 | 43 | export const FETCH_POLL_REQUEST = '[Request] Fetch Poll' 44 | export const FETCH_POLL_SUCCESS = '[Success] Fetch Poll' 45 | export const FETCH_POLL_FAILURE = '[Failure] Fetch Poll' 46 | 47 | export const fetchPollRequest = (id: string) => 48 | action(FETCH_POLL_REQUEST, { id }) 49 | export const fetchPollSuccess = ( 50 | poll: Poll, 51 | token: Token, 52 | votes: Vote[], 53 | options: Option[] 54 | ) => action(FETCH_POLL_SUCCESS, { poll, token, votes, options }) 55 | export const fetchPollFailure = (error: string) => 56 | action(FETCH_POLL_FAILURE, { error }) 57 | 58 | export type FetchPollRequestAction = ReturnType 59 | export type FetchPollSuccessAction = ReturnType 60 | export type FetchPollFailureAction = ReturnType 61 | -------------------------------------------------------------------------------- /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 50 | 51 | 52 | -------------------------------------------------------------------------------- /webapp/src/modules/option/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux' 2 | import { loadingReducer, LoadingState } from '@dapps/modules/loading/reducer' 3 | import { Option } from 'modules/option/types' 4 | import { toObjectById } from '@dapps/lib/utils' 5 | import { ModelById } from '@dapps/lib/types' 6 | import { 7 | FETCH_POLL_OPTIONS_REQUEST, 8 | FETCH_POLL_OPTIONS_SUCCESS, 9 | FETCH_POLL_OPTIONS_FAILURE, 10 | FetchPollOptionsRequestAction, 11 | FetchPollOptionsSuccessAction, 12 | FetchPollOptionsFailureAction 13 | } from 'modules/option/actions' 14 | import { 15 | FETCH_POLLS_REQUEST, 16 | FETCH_POLL_REQUEST, 17 | FetchPollsRequestAction, 18 | FetchPollsSuccessAction, 19 | FetchPollsFailureAction, 20 | FetchPollRequestAction, 21 | FetchPollSuccessAction, 22 | FetchPollFailureAction, 23 | FETCH_POLLS_SUCCESS, 24 | FETCH_POLL_SUCCESS, 25 | FETCH_POLLS_FAILURE, 26 | FETCH_POLL_FAILURE 27 | } from 'modules/poll/actions' 28 | 29 | export type OptionState = { 30 | data: ModelById