├── .babelrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── client ├── actions │ ├── judgeActions.js │ ├── modalsActions.js │ ├── sponsorActions.js │ ├── teamActions.js │ ├── types.js │ └── userActions.js ├── components │ ├── Admin │ │ ├── AdminJudges │ │ │ ├── AdminJudgesForm.jsx │ │ │ ├── _index.scss │ │ │ └── index.jsx │ │ ├── AdminOptions │ │ │ ├── AdminOptions.js │ │ │ ├── Payout │ │ │ │ ├── Payout.jsx │ │ │ │ └── payout.scss │ │ │ └── adminOptions.scss │ │ ├── AdminSponsors │ │ │ ├── AdminSponsorsForm.jsx │ │ │ ├── _index.scss │ │ │ └── index.jsx │ │ ├── AdminTeams │ │ │ ├── AdminTeamsForm.jsx │ │ │ ├── _index.scss │ │ │ └── index.jsx │ │ ├── _index.scss │ │ └── index.jsx │ ├── App │ │ ├── App.jsx │ │ └── app.scss │ ├── Decorative │ │ ├── AddCircleIcon │ │ │ └── AddCircleIcon.js │ │ ├── CloseIcon │ │ │ └── CloseIcon.jsx │ │ ├── CubeLoader │ │ │ ├── CubeLoader.jsx │ │ │ └── cubeLoader.scss │ │ ├── DollarIcon │ │ │ └── index.jsx │ │ ├── EthereumIcon │ │ │ └── EthereumIcon.jsx │ │ ├── Loader │ │ │ ├── _index.scss │ │ │ └── index.jsx │ │ ├── MedalIcon │ │ │ └── MedalIcon.jsx │ │ └── StarIcon │ │ │ └── index.jsx │ ├── Footer │ │ ├── Footer.js │ │ └── footer.scss │ ├── FormComonent │ │ ├── FormComponent.jsx │ │ └── forms.scss │ ├── Header │ │ ├── HeaderStatus │ │ │ ├── _index.scss │ │ │ └── index.jsx │ │ ├── _index.scss │ │ └── index.jsx │ ├── Jury │ │ ├── _index.scss │ │ ├── assets │ │ │ ├── 001-arrows.png │ │ │ └── 002-arrows-1.png │ │ └── index.jsx │ ├── Landing │ │ ├── _index.scss │ │ └── index.jsx │ ├── Modals │ │ ├── JudgeModal.jsx │ │ ├── ModalWrapper │ │ │ ├── ModalWrapper.jsx │ │ │ └── modalWrapper.scss │ │ ├── SponsorModal.jsx │ │ └── TeamModal.jsx │ ├── NoMetamask │ │ └── NoMetamask.jsx │ ├── OpenModalButton │ │ └── OpenModalButton.jsx │ ├── Scoreboard │ │ ├── Scoreboard.jsx │ │ └── scoreboard.scss │ └── Sponsor │ │ ├── _index.scss │ │ └── index.jsx ├── config │ ├── Routes.jsx │ ├── initWeb3.js │ └── storeGenerator.js ├── external │ └── particlesConfig.json ├── index.html ├── index.js ├── modules │ ├── config.json │ └── ethereumService.js └── reducers │ ├── index.js │ ├── judgesReducer.js │ ├── modalsReducer.js │ ├── sponsorsReducer.js │ ├── teamsReducer.js │ └── userReducer.js ├── dist ├── 521fb616232adb3eb64828d5a3d820a1.png ├── eecc5a0815a9233ebd31eb7d77eda4b1.png ├── index.html ├── main.css └── main.js ├── package.json ├── solidity └── DecenterHackathon.sol └── webpack ├── webpack-dev.config.js └── webpack-prod.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": ["transform-object-rest-spread", "transform-exponentiation-operator"] 4 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': 'eslint-config-airbnb', 3 | 'env': { 4 | 'browser': true 5 | }, 6 | 'rules': { 7 | 'jsx-a11y/href-no-hash': 'off', 8 | 'jsx-a11y/anchor-is-valid': ['warn', { 'aspects': ['invalidHref'] }], 9 | 'jsx-a11y/label-has-for': 0, 10 | 'jsx-a11y/no-autofocus': 0, 11 | 'comma-dangle': 0, 12 | 'react/forbid-prop-types': 0, 13 | 'react/jsx-filename-extension': 0, 14 | 'arrow-parens': 0, 15 | 'import/extensions': 0, 16 | 'no-unused-vars': 1, 17 | 'no-new': 0, 18 | 'no-underscore-dangle': 0, 19 | 'prefer-template': 0, 20 | 'keyword-spacing': 0, 21 | 'no-plusplus': 0, 22 | 'prefer-const': 0 23 | }, 24 | 'plugins': [ 25 | 'react', 'import' 26 | ], 27 | 'globals': { 28 | 'web3': true 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | .DS_Store 3 | .idea/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smart Hackathon DApp 2 | 3 | This is a fully functional DApp that can be used to automate every aspect of a typical hackathon. 4 | 5 | Administrator (contract owner) can add jury members, jury members can vote, sponsors can donate ETH to the prize pool and contestents can register teams and receive prizes from the pool. 6 | 7 | The applications consistes of React-based front-end that interacts with Ethereum smart contracts (written in Solidity) via web3. 8 | 9 | ## Development 10 | to install all modules run ```yarn``` 11 | 12 | to start dev server on localhost:8080 run ```yarn dev``` 13 | 14 | to build production run ```yarn production```. Production code will be generated in the dist directory 15 | -------------------------------------------------------------------------------- /client/actions/judgeActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | NEW_JUDGE, 3 | JUDGES_FETCH, 4 | JUDGES_SUCCESS, 5 | JUDGES_ERROR, 6 | ADD_JUDGE, 7 | ADD_JUDGE_SUCCESS, 8 | ADD_JUDGE_ERROR 9 | } from './types'; 10 | import toggleModal from './modalsActions'; 11 | 12 | import * as eth from '../modules/ethereumService'; 13 | 14 | const judgesFormValidator = (values) => { 15 | const errors = {}; 16 | 17 | if (!values.name) errors.name = 'Required'; 18 | if (!values.address) errors.address = 'Required'; 19 | 20 | if (values.address && !web3.isAddress(values.address)) errors.address = 'Ethereum address is not valid'; 21 | 22 | return errors; 23 | }; 24 | 25 | const submitAddJudgesForm = (judge) => (dispatch) => { 26 | dispatch({ type: ADD_JUDGE }); 27 | 28 | eth._registerJuryMember(judge.name, judge.address) 29 | .then((res) => { 30 | dispatch({ type: ADD_JUDGE_SUCCESS, payload: { judge: res } }); 31 | dispatch(toggleModal(location.hash, false)); 32 | }) 33 | .catch((error) => { 34 | dispatch({ type: ADD_JUDGE_ERROR, payload: { addJudgeError: error.message.toString() } }); 35 | }); 36 | }; 37 | 38 | const fetchJudges = () => (dispatch) => { 39 | dispatch({ type: JUDGES_FETCH }); 40 | eth.getJuries() 41 | .then((res) => { 42 | dispatch({ 43 | type: JUDGES_SUCCESS, 44 | judges: res 45 | }); 46 | }) 47 | .catch((error) => { 48 | console.log(error); 49 | const errorMessage = error.message ? error.message.toString() : error; 50 | dispatch({ 51 | type: JUDGES_ERROR, 52 | error: errorMessage 53 | }); 54 | }); 55 | }; 56 | 57 | const judgeEventListener = () => (dispatch) => { 58 | eth.JuryMemberAddedEvent((error, data) => { 59 | if (!error) { 60 | console.log(data); 61 | dispatch({ 62 | type: NEW_JUDGE, 63 | event: data, 64 | }); 65 | } 66 | }); 67 | }; 68 | 69 | module.exports = { 70 | fetchJudges, 71 | judgesFormValidator, 72 | submitAddJudgesForm, 73 | judgeEventListener, 74 | }; 75 | -------------------------------------------------------------------------------- /client/actions/modalsActions.js: -------------------------------------------------------------------------------- 1 | import { reset } from 'redux-form'; 2 | import { TOGGLE_MODAL } from './types'; 3 | import JudgeModal from '../components/Modals/JudgeModal'; 4 | import teamModal from '../components/Modals/TeamModal'; 5 | import sponsorModal from '../components/Modals/SponsorModal'; 6 | 7 | export default (routePath, state) => ( 8 | (dispatch) => { 9 | switch (routePath) { 10 | case '#/admin/teams': 11 | if (state === false) dispatch(reset('teamsForm')); 12 | dispatch({ type: TOGGLE_MODAL, payload: { state, modalComponent: teamModal } }); 13 | return true; 14 | case '#/admin/sponsors': 15 | if (state === false) dispatch(reset('sponsorsForm')); 16 | dispatch({ type: TOGGLE_MODAL, payload: { state, modalComponent: sponsorModal } }); 17 | return true; 18 | case '#/admin/judges': 19 | if (state === false) dispatch(reset('judgesForm')); 20 | dispatch({ type: TOGGLE_MODAL, payload: { state, modalComponent: JudgeModal } }); 21 | return true; 22 | default: 23 | return false; 24 | } 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /client/actions/sponsorActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | NEW_SPONSOR, SPONSORS_FETCH, SPONSORS_SUCCESS, SPONSORS_ERROR, ADD_SPONSOR, ADD_SPONSOR_SUCCESS, 3 | ADD_SPONSOR_ERROR, SPONSORS_PRIZE_ETH, SPONSORS_PRIZE_EUR 4 | } from './types'; 5 | import toggleModal from './modalsActions'; 6 | 7 | import * as eth from '../modules/ethereumService'; 8 | 9 | const isUriImage = (uri) => { 10 | if (!uri) return false; 11 | const uriNoParam = uri.split('?')[0]; 12 | const parts = uriNoParam.split('.'); 13 | const extension = parts[parts.length - 1]; 14 | const imageTypes = ['jpg', 'jpeg', 'tiff', 'png', 'gif', 'bmp']; 15 | return imageTypes.indexOf(extension) !== -1; 16 | }; 17 | 18 | const isURL = (str) => { 19 | const regexp = /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/gm; // eslint-disable-line 20 | return regexp.test(str); 21 | }; 22 | 23 | const sponsorsFormValidator = (values) => { 24 | const errors = {}; 25 | 26 | if (!values.name) errors.name = 'Required'; 27 | if (!values.amount) errors.amount = 'Required'; 28 | if (!values.logoUrl) errors.logoUrl = 'Required'; 29 | if (!values.websiteUrl) errors.websiteUrl = 'Required'; 30 | 31 | if (values.logoUrl && !isUriImage(values.logoUrl)) errors.logoUrl = 'Url does not contain valid image extension'; 32 | if (values.websiteUrl && !isURL(values.websiteUrl)) errors.websiteUrl = 'Url is not valid'; 33 | 34 | if (values.amount) { 35 | const commaError = values.amount && values.amount.indexOf(',') > 0; 36 | const nanError = isNaN(parseFloat(values.amount)); 37 | const amountError = parseFloat(values.amount) < 0.1; 38 | 39 | if (commaError) errors.amount = 'Use a full stop as a delimiter instead of a comma'; 40 | if (nanError) errors.amount = 'The provided input is not a number'; 41 | if (amountError) errors.amount = 'The donation can\'t be lower than 0.1 ETH'; 42 | } 43 | 44 | return errors; 45 | }; 46 | 47 | const submitAddSponsorsForm = (sponsor) => (dispatch) => { 48 | dispatch({ type: ADD_SPONSOR }); 49 | 50 | eth._contributeToPrizePool( 51 | sponsor.name, 52 | parseFloat(sponsor.amount), 53 | sponsor.websiteUrl, 54 | sponsor.logoUrl 55 | ) 56 | .then((res) => { 57 | dispatch({ type: ADD_SPONSOR_SUCCESS, payload: { sponsor: res } }); 58 | dispatch(toggleModal(location.hash, false)); 59 | }) 60 | .catch((error) => { 61 | dispatch({ type: ADD_SPONSOR_ERROR, payload: { addSponsorError: error.message.toString() } }); 62 | }); 63 | }; 64 | 65 | const fetchSponsors = () => (dispatch) => { 66 | dispatch({ type: SPONSORS_FETCH }); 67 | eth.getSponsors() 68 | .then((res) => { 69 | dispatch({ 70 | type: SPONSORS_SUCCESS, 71 | sponsors: res 72 | }); 73 | }) 74 | .catch((error) => { 75 | console.log(error); 76 | const errorMessage = error.message ? error.message.toString() : error; 77 | dispatch({ 78 | type: SPONSORS_ERROR, 79 | error: errorMessage 80 | }); 81 | }); 82 | }; 83 | 84 | const fetchPrizePoolSize = () => (dispatch) => { 85 | eth.getPrizePoolSize() 86 | .then((res) => { 87 | const prize = web3.fromWei(res).toString(); 88 | console.log(prize); 89 | dispatch({ 90 | type: SPONSORS_PRIZE_ETH, 91 | prize 92 | }); 93 | fetch('https://api.kraken.com/0/public/Ticker?pair=xethzeur', { 94 | method: 'get' 95 | }) 96 | .then(response => response.json()) 97 | .then((data) => { 98 | let eurPrize = data.result.XETHZEUR.c[0] * prize; 99 | eurPrize = eurPrize.toFixed(2); 100 | dispatch({ 101 | type: SPONSORS_PRIZE_EUR, 102 | prize: eurPrize.toString(), 103 | }); 104 | }) 105 | .catch((error) => { 106 | console.log(error); 107 | }); 108 | }) 109 | .catch((error) => { 110 | console.log(error); 111 | }); 112 | }; 113 | 114 | const sponsorEventListener = () => (dispatch) => { 115 | eth.SponsorshipReceivedEvent((error, data) => { 116 | if (!error) { 117 | dispatch({ 118 | type: NEW_SPONSOR, 119 | event: data, 120 | }); 121 | dispatch(fetchPrizePoolSize()); 122 | } 123 | }); 124 | }; 125 | 126 | module.exports = { 127 | fetchSponsors, 128 | sponsorsFormValidator, 129 | submitAddSponsorsForm, 130 | sponsorEventListener, 131 | fetchPrizePoolSize, 132 | }; 133 | -------------------------------------------------------------------------------- /client/actions/teamActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | NEW_TEAM, 3 | TEAM_UP, 4 | TEAM_DOWN, 5 | TEAMS_FETCH, 6 | TEAMS_SUCCESS, 7 | TEAMS_ERROR, 8 | ADD_TEAM, 9 | ADD_TEAM_ERROR, 10 | ADD_TEAM_SUCCESS, 11 | VOTE, 12 | VOTE_SUCCESS, 13 | VOTE_ERROR 14 | } from './types'; 15 | import toggleModal from './modalsActions'; 16 | 17 | import * as eth from '../modules/ethereumService'; 18 | 19 | const teamsFormValidator = (values) => { 20 | const errors = {}; 21 | 22 | if (!values.name) errors.name = 'Required'; 23 | if (!values.address) errors.address = 'Required'; 24 | 25 | if (values.address && !web3.isAddress(values.address)) errors.address = 'Ethereum address is not valid'; 26 | 27 | if (!values.teamMembers) errors.teamMembers = 'Required'; 28 | 29 | if (values.teamMembers) { 30 | const teamMembers = values.teamMembers.split(', '); 31 | teamMembers.reduce((sum, val) => { 32 | if (val.indexOf(',') > 0) errors.teamMembers = 'Team members are not correctly separated'; 33 | return sum.concat(`, ${val}`); 34 | }, ''); 35 | } 36 | 37 | return errors; 38 | }; 39 | 40 | const submitAddTeamsForm = (team) => (dispatch) => { 41 | dispatch({ type: ADD_TEAM }); 42 | 43 | eth._registerTeam(team.name, team.address, team.teamMembers, !team.excludeFromPrize) 44 | .then((res) => { 45 | dispatch({ type: ADD_TEAM_SUCCESS, payload: { team: res } }); 46 | dispatch(toggleModal(location.hash, false)); 47 | }) 48 | .catch((error) => { 49 | dispatch({ type: ADD_TEAM_ERROR, payload: { addTeamError: error.message.toString() } }); 50 | }); 51 | }; 52 | 53 | const fetchTeams = () => (dispatch) => { 54 | dispatch({ type: TEAMS_FETCH }); 55 | 56 | eth.getTeams() 57 | .then((res) => { 58 | dispatch({ type: TEAMS_SUCCESS, teams: res }); 59 | }) 60 | .catch((error) => { 61 | console.log(error); 62 | const errorMessage = error.message ? error.message.toString() : error; 63 | dispatch({ 64 | type: TEAMS_ERROR, 65 | error: errorMessage 66 | }); 67 | }); 68 | }; 69 | 70 | const fetchTeamScores = () => (dispatch) => { 71 | dispatch({ type: TEAMS_FETCH }); 72 | eth.getTeams() 73 | .then((teams) => { 74 | eth.getTeamScores() 75 | .then((events) => { 76 | let result = {}; 77 | for (let i = 0; i < events.length; i++) { 78 | let teamAddress = events[i].args.teamAddress; 79 | let juryMemberName = events[i].args.juryMemberName; 80 | let points = parseInt(events[i].args.points.toString(10), 10); 81 | 82 | if (result[teamAddress] !== undefined) { 83 | result[teamAddress].totalScore += points; 84 | result[teamAddress].scoreBreakdown.push({ juryMemberName, points }); 85 | } else { 86 | result[teamAddress] = { 87 | totalScore: points, 88 | scoreBreakdown: [{ 89 | juryMemberName, 90 | points 91 | }] 92 | }; 93 | } 94 | } 95 | 96 | let teamsWithPoints = teams.map((item) => { 97 | let score = result[item.args.teamAddress]; 98 | let scoredItem = item; 99 | scoredItem.args.totalScore = score.totalScore; 100 | scoredItem.args.scoreBreakdown = score.scoreBreakdown; 101 | 102 | return scoredItem; 103 | }).sort((a, b) => b.args.totalScore - a.args.totalScore); 104 | 105 | dispatch({ type: TEAMS_SUCCESS, teams: teamsWithPoints }); 106 | }) 107 | .catch((error) => { 108 | console.log(error); 109 | const errorMessage = error.message ? error.message.toString() : error; 110 | console.log(errorMessage); 111 | }); 112 | }) 113 | .catch(error => console.log(error)); 114 | }; 115 | 116 | const teamsEventListener = () => (dispatch) => { 117 | eth.TeamRegisteredEvent((error, data) => { 118 | if (!error) { 119 | console.log(data); 120 | dispatch({ 121 | type: NEW_TEAM, 122 | event: data, 123 | }); 124 | } 125 | }); 126 | }; 127 | 128 | const moveTeamUp = (index) => { 129 | if (index === 0) { 130 | return { 131 | type: '', 132 | }; 133 | } 134 | 135 | return { 136 | type: TEAM_UP, 137 | index 138 | }; 139 | }; 140 | 141 | const moveTeamDown = (index) => (dispatch, getState) => { 142 | if (index === getState().teams.teams.length - 1) { 143 | return dispatch({ 144 | type: '' 145 | }); 146 | } 147 | 148 | return dispatch({ 149 | type: TEAM_DOWN, 150 | index 151 | }); 152 | }; 153 | 154 | const vote = (votes) => dispatch => { 155 | dispatch({ type: VOTE }); 156 | 157 | eth._vote(votes) 158 | .then(data => { 159 | dispatch({ type: VOTE_SUCCESS }); 160 | }) 161 | .catch(error => { 162 | dispatch({ type: VOTE_ERROR }); 163 | }); 164 | }; 165 | 166 | module.exports = { 167 | fetchTeamScores, 168 | teamsEventListener, 169 | fetchTeams, 170 | teamsFormValidator, 171 | submitAddTeamsForm, 172 | moveTeamUp, 173 | moveTeamDown, 174 | vote, 175 | }; 176 | -------------------------------------------------------------------------------- /client/actions/types.js: -------------------------------------------------------------------------------- 1 | export const TOGGLE_MODAL = 'close_modal'; 2 | 3 | export const ADD_TEAM = 'add_team'; 4 | export const ADD_TEAM_SUCCESS = 'add_team_success'; 5 | export const ADD_TEAM_ERROR = 'add_team_error'; 6 | 7 | export const USER_CHECKING = 'user_checking'; 8 | export const USER_FOUND = 'user_found'; 9 | 10 | export const TEAMS_FETCH = 'teams_fetch'; 11 | export const TEAMS_SUCCESS = 'teams_success'; 12 | export const TEAMS_ERROR = 'teams_error'; 13 | export const TEAM_UP = 'team_up'; 14 | export const TEAM_DOWN = 'team_down'; 15 | export const NEW_TEAM = 'new_team'; 16 | 17 | export const ADD_SPONSOR = 'add_sponsor'; 18 | export const ADD_SPONSOR_SUCCESS = 'add_sponsor_success'; 19 | export const ADD_SPONSOR_ERROR = 'add_sponsor_error'; 20 | 21 | export const SPONSORS_FETCH = 'sponsors_fetch'; 22 | export const SPONSORS_SUCCESS = 'sponsors_success'; 23 | export const SPONSORS_ERROR = 'sponsors_error'; 24 | export const NEW_SPONSOR = 'new_sponsors'; 25 | export const SPONSORS_PRIZE_ETH = 'sponsors_prize_eth'; 26 | export const SPONSORS_PRIZE_EUR = 'sponsors_prize_eur'; 27 | 28 | export const ADD_JUDGE = 'add_judge'; 29 | export const ADD_JUDGE_SUCCESS = 'add_judge_success'; 30 | export const ADD_JUDGE_ERROR = 'add_judge_error'; 31 | 32 | export const JUDGES_FETCH = 'judges_fetch'; 33 | export const JUDGES_SUCCESS = 'judges_success'; 34 | export const JUDGES_ERROR = 'judges_error'; 35 | export const NEW_JUDGE = 'new_judge'; 36 | 37 | export const PHASE_FETCH = 'phase_fetch'; 38 | export const PHASE_FETCH_SUCCESS = 'phase_fetch_success'; 39 | export const PHASE_FETCH_ERROR = 'phase_fetch_error'; 40 | export const PHASE_CHANGE = 'phase_change'; 41 | export const UPDATE_PHASE = 'update_phase'; 42 | 43 | export const CHANGE_PHASE = 'change_phase'; 44 | export const CHANGE_PHASE_SUCCESS = 'change_phase_success'; 45 | export const CHANGE_PHASE_ERROR = 'change_phase_error'; 46 | 47 | export const SUBMIT_PAYOUT = 'submit_payment'; 48 | export const SUBMIT_PAYOUT_SUCCESS = 'submit_payment_success'; 49 | export const SUBMIT_PAYOUT_ERROR = 'submit_payment_error'; 50 | 51 | export const VOTE = 'jury_vote'; 52 | export const VOTE_SUCCESS = 'jury_vote_success'; 53 | export const VOTE_ERROR = 'jury_vote_error'; 54 | 55 | export const ALREADY_VOTED = 'already_voted'; 56 | -------------------------------------------------------------------------------- /client/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | PHASE_FETCH, PHASE_FETCH_SUCCESS, PHASE_FETCH_ERROR, CHANGE_PHASE, 3 | CHANGE_PHASE_SUCCESS, CHANGE_PHASE_ERROR, USER_CHECKING, USER_FOUND, 4 | SUBMIT_PAYOUT, SUBMIT_PAYOUT_ERROR, SUBMIT_PAYOUT_SUCCESS, UPDATE_PHASE, 5 | ALREADY_VOTED 6 | } from './types'; 7 | 8 | import * as eth from '../modules/ethereumService'; 9 | 10 | const payoutTeams = (sortedTeams) => (dispatch) => { 11 | dispatch({ type: SUBMIT_PAYOUT }); 12 | 13 | for (let i = 0; i < sortedTeams.length - 1; i += 1) { 14 | if (sortedTeams[i].points < sortedTeams[i + 1].points) { 15 | return dispatch({ 16 | type: SUBMIT_PAYOUT_ERROR, 17 | message: 'Teams are not sorted' 18 | }); 19 | } 20 | } 21 | 22 | const teamAddresses = sortedTeams.map((elem) => (elem.address)); 23 | 24 | return eth._payoutPrizes(teamAddresses) 25 | .then(() => { 26 | dispatch({ type: SUBMIT_PAYOUT_SUCCESS }); 27 | }) 28 | .catch((error) => { 29 | console.log(error); 30 | dispatch({ 31 | type: SUBMIT_PAYOUT_ERROR, 32 | message: 'Payout prizes contract error.' 33 | }); 34 | }); 35 | }; 36 | 37 | const fetchPhase = () => (dispatch) => { 38 | dispatch({ type: PHASE_FETCH }); 39 | 40 | eth.getPhase() 41 | .then((res) => { 42 | const phase = parseFloat(res); 43 | dispatch({ type: PHASE_FETCH_SUCCESS, payload: { phase } }); 44 | }) 45 | .catch((error) => { 46 | dispatch({ type: PHASE_FETCH_ERROR, payload: { error } }); 47 | }); 48 | }; 49 | 50 | const changePhase = () => (dispatch) => { 51 | dispatch({ type: CHANGE_PHASE }); 52 | 53 | eth._switchToNextPeriod() 54 | .then((res) => { 55 | dispatch({ type: CHANGE_PHASE_SUCCESS, }); 56 | }) 57 | .catch((error) => { 58 | console.error(error); 59 | dispatch({ type: CHANGE_PHASE_ERROR, payload: { error: error.message.toString() } }); 60 | }); 61 | }; 62 | 63 | const checkUser = () => (dispatch) => { 64 | dispatch({ type: USER_CHECKING }); 65 | eth.getUserTypeWithTimeout() 66 | .then((res) => { 67 | console.log(res); 68 | dispatch({ 69 | type: USER_FOUND, 70 | userType: res 71 | }); 72 | }) 73 | .catch((error) => { 74 | console.log(error); 75 | }); 76 | }; 77 | 78 | const periodChangedListener = () => (dispatch) => { 79 | eth.PeriodChangedEvent((error, event) => { 80 | if (!error) { 81 | console.log(event); 82 | const phase = parseFloat(event.args.newPeriod.toString()); 83 | console.log(phase); 84 | dispatch({ 85 | type: UPDATE_PHASE, 86 | payload: { 87 | phase, 88 | } 89 | }); 90 | } 91 | }); 92 | }; 93 | 94 | const checkIfJuryVoted = () => (dispatch) => { 95 | eth.checkJuryVoted((error) => { 96 | if (!error) { 97 | dispatch({ type: ALREADY_VOTED }); 98 | } 99 | }); 100 | }; 101 | 102 | module.exports = { 103 | periodChangedListener, 104 | checkUser, 105 | fetchPhase, 106 | changePhase, 107 | payoutTeams, 108 | checkIfJuryVoted 109 | }; 110 | -------------------------------------------------------------------------------- /client/components/Admin/AdminJudges/AdminJudgesForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Field, reduxForm } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import formGroup from '../../FormComonent/FormComponent'; 5 | import { judgesFormValidator, submitAddJudgesForm } from '../../../actions/judgeActions'; 6 | import CubeLoader from '../../Decorative/CubeLoader/CubeLoader'; 7 | 8 | require('../../FormComonent/forms.scss'); 9 | 10 | let JudgesTeamsForm = ({ handleSubmit, pristine, submittingForm, invalid, addJudgeError }) => ( // eslint-disable-line 11 |
12 | 22 | 23 | 33 | 34 | { addJudgeError &&
Error: {addJudgeError}
} 35 | 36 | 44 | 45 | ); 46 | 47 | const mapStateToProps = (state) => ({ 48 | submittingForm: state.judges.submitting, 49 | addJudgeError: state.judges.addJudgeError 50 | }); 51 | 52 | const validate = judgesFormValidator; 53 | 54 | JudgesTeamsForm = reduxForm({ form: 'judgesForm', validate })(JudgesTeamsForm); 55 | 56 | export default connect(mapStateToProps, { onSubmit: submitAddJudgesForm })(JudgesTeamsForm); 57 | -------------------------------------------------------------------------------- /client/components/Admin/AdminJudges/_index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DecenterApps/Smart-Hackathon-DApp/31e6419a47c1a6c9885e35b359466c7fa26eb032/client/components/Admin/AdminJudges/_index.scss -------------------------------------------------------------------------------- /client/components/Admin/AdminJudges/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import Loader from '../../Decorative/Loader/index.jsx'; 6 | import OpenModalButton from '../../OpenModalButton/OpenModalButton'; 7 | import judgeActions from '../../../actions/judgeActions'; 8 | 9 | class AdminJudges extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = {}; 14 | } 15 | componentWillMount() { 16 | this.props.fetchJudges(); 17 | } 18 | render() { 19 | return ( 20 |
21 | { 22 | this.props.judges.isFetching && 23 |
24 |

Loading

25 |
26 | } 27 | { 28 | !this.props.judges.isFetching && 29 | this.props.judges.error && 30 |
31 |

{this.props.judges.error.toString()}

32 |
33 | } 34 | { 35 | !this.props.judges.isFetching && 36 | !this.props.judges.error && 37 | !this.props.judges.judges.length > 0 && 38 |
39 |
40 |

No judges

41 | 42 |
43 |
44 | } 45 | { 46 | !this.props.judges.isFetching && 47 | !this.props.judges.error && 48 | this.props.judges.judges.length > 0 && 49 | 50 | 51 | { 52 | this.props.judges.judges.map((judge) => ( 53 | 54 | 55 | 60 | 61 | )) 62 | } 63 | 64 |
{judge.args.juryMemberName} 56 | 57 | {judge.args.juryMemberAddress} 58 | 59 |
65 | } 66 |
67 | ); 68 | } 69 | } 70 | 71 | AdminJudges.propTypes = { 72 | judges: PropTypes.shape({ 73 | judges: PropTypes.array, 74 | isFetching: PropTypes.bool, 75 | error: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), 76 | }), 77 | fetchJudges: PropTypes.func.isRequired, 78 | }; 79 | AdminJudges.defaultProps = { 80 | judges: { 81 | judges: [] 82 | }, 83 | isFetching: true, 84 | }; 85 | 86 | const mapStateToProps = state => state; 87 | const mapDispatchToProps = dispatch => bindActionCreators({ 88 | ...judgeActions 89 | }, dispatch); 90 | 91 | export default connect( 92 | mapStateToProps, 93 | mapDispatchToProps, 94 | )(AdminJudges); 95 | 96 | -------------------------------------------------------------------------------- /client/components/Admin/AdminOptions/AdminOptions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | import { changePhase } from '../../../actions/userActions'; 5 | import CubeLoader from '../../Decorative/CubeLoader/CubeLoader'; 6 | import Payout from './Payout/Payout'; 7 | 8 | require('./adminOptions.scss'); 9 | 10 | const AdminOptions = ({ 11 | $changePhase, changingPhase, changingError, phase, lastPhaseIndex 12 | }) => ( 13 |
14 | { 15 | phase < lastPhaseIndex - 1 && 16 |
17 | Switch to the next period: 18 | 22 | 23 | { 24 | changingError &&
{changingError}
25 | } 26 | 27 |
28 | } 29 | { 30 | phase === lastPhaseIndex && 31 |
32 | The last period is active. It can not be changed 33 |
34 | } 35 | { 36 | phase === 3 && 37 | } 38 |
39 | ); 40 | 41 | AdminOptions.propTypes = { 42 | $changePhase: PropTypes.func.isRequired, 43 | changingPhase: PropTypes.bool.isRequired, 44 | changingError: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired, 45 | phase: PropTypes.number.isRequired, 46 | lastPhaseIndex: PropTypes.number.isRequired 47 | }; 48 | 49 | const mapStateToProps = (state) => ({ 50 | changingPhase: state.user.changingPhase, 51 | changingError: state.user.changingError, 52 | phase: state.user.phase, 53 | lastPhaseIndex: state.user.phases.length - 1 54 | }); 55 | 56 | export default connect(mapStateToProps, { $changePhase: changePhase })(AdminOptions); 57 | -------------------------------------------------------------------------------- /client/components/Admin/AdminOptions/Payout/Payout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | import { bindActionCreators } from 'redux'; 5 | 6 | import * as teamActions from '../../../../actions/teamActions'; 7 | import userActions from '../../../../actions/userActions'; 8 | 9 | import CubeLoader from '../../../Decorative/CubeLoader/CubeLoader'; 10 | import Loader from '../../../Decorative/Loader/index'; 11 | import arrowup from '../../../Jury/assets/001-arrows.png'; 12 | import arrowdown from '../../../Jury/assets/002-arrows-1.png'; 13 | 14 | require('./payout.scss'); 15 | 16 | class Payout extends Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = {}; 21 | 22 | this.payoutTeams = this.payoutTeams.bind(this); 23 | } 24 | 25 | componentDidMount() { 26 | this.props.fetchTeamScores(); 27 | } 28 | 29 | payoutTeams() { 30 | // fix after points implementation 31 | const votes = this.props.teams.teams.map((elem, i) => ({ 32 | address: elem.args.teamAddress, 33 | points: elem.args.totalScore, 34 | })); 35 | 36 | this.props.payoutTeams(votes); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 | { 43 | this.props.teams.isFetching && 44 |
45 |

Loading

46 |
47 | } 48 | { 49 | !this.props.teams.isFetching && 50 | this.props.teams.error && 51 |
52 |

{this.props.teams.error.toString()}

53 |
54 | } 55 | { 56 | !this.props.teams.isFetching && 57 | !this.props.teams.error && 58 | !this.props.teams.teams.length > 0 && 59 |
60 |
61 |

No teams

62 |
63 |
64 | } 65 | 66 | { 67 | !this.props.teams.isFetching && 68 | !this.props.teams.error && 69 | this.props.teams.teams.length > 0 && 70 |
71 | Payout reward to teams: 72 | 76 |
77 | } 78 | 79 | { 80 | this.props.teams && 81 | this.props.user.submittingPayoutError && 82 | !this.props.user.submittingPayout && 83 |
{this.props.user.submittingPayoutError}
84 | } 85 | 86 | { 87 | !this.props.teams.isFetching && 88 | !this.props.teams.error && 89 | this.props.teams.teams.length > 0 && 90 |
91 |
92 | Scoreboard 93 | 94 | * You can switch the teams with the same number of points 95 | 96 |
97 |
98 | { 99 | this.props.teams.teams.map((team, i) => ( 100 |
101 |
{i + 1}.
102 |
{team.args.teamName}
103 |
{team.args.teamAddress}
104 |
{team.args.totalScore} points
105 |
this.props.moveTeamDown(i)} 108 | role="button" 109 | tabIndex="-1" 110 | > 111 | Move a team down 112 |
113 |
this.props.moveTeamUp(i)} 116 | role="button" 117 | tabIndex="-1" 118 | > 119 | Move a team up 120 |
121 |
122 | )) 123 | } 124 |
125 |
126 | } 127 |
128 | ); 129 | } 130 | } 131 | 132 | Payout.propTypes = { 133 | teams: PropTypes.shape({ 134 | isFetching: PropTypes.bool.isRequired, 135 | teams: PropTypes.array, 136 | error: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), 137 | }), 138 | user: PropTypes.shape({ 139 | submittingPayout: PropTypes.bool.isRequired, 140 | submittingPayoutError: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired 141 | }).isRequired, 142 | moveTeamUp: PropTypes.func.isRequired, 143 | moveTeamDown: PropTypes.func.isRequired, 144 | payoutTeams: PropTypes.func.isRequired, 145 | fetchTeamScores: PropTypes.func.isRequired, 146 | }; 147 | 148 | Payout.defaultProps = { 149 | teams: { 150 | isFetching: true, 151 | teams: [1] 152 | } 153 | }; 154 | 155 | const mapStateToProps = state => state; 156 | const mapDispatchToProps = dispatch => bindActionCreators({ 157 | ...teamActions, 158 | ...userActions, 159 | }, dispatch); 160 | 161 | export default connect( 162 | mapStateToProps, 163 | mapDispatchToProps, 164 | )(Payout); 165 | -------------------------------------------------------------------------------- /client/components/Admin/AdminOptions/Payout/payout.scss: -------------------------------------------------------------------------------- 1 | .payout-wrapper { 2 | border-top: 1px solid #d0d0d0; 3 | 4 | .payout-error { 5 | color: #F57170; 6 | font-size: 16px; 7 | margin-left: 20px; 8 | } 9 | 10 | button { 11 | display: inline-flex; 12 | align-items: center; 13 | justify-content: center; 14 | color: white; 15 | background: #673AB7; 16 | padding: 10px 20px; 17 | font-size: 16px; 18 | border-radius: 3px; 19 | margin-left: 10px; 20 | 21 | &:hover { 22 | background: lighten(#673AB7, 15); 23 | } 24 | 25 | &:disabled { 26 | user-select: none; 27 | cursor: not-allowed; 28 | background: lighten(#673AB7, 30); 29 | } 30 | } 31 | 32 | .pay-button-wrapper { 33 | font-size: 18px; 34 | font-weight: 700; 35 | margin: 20px; 36 | } 37 | 38 | .table-header { 39 | display: flex; 40 | justify-content: space-between; 41 | align-items: center; 42 | border-top: 1px solid #d0d0d0; 43 | font-size: 20px; 44 | padding: 30px; 45 | 46 | .tip { 47 | font-size: 14px; 48 | } 49 | } 50 | 51 | .table { 52 | border-bottom: 0; 53 | margin-bottom: 0; 54 | 55 | .team-name { 56 | flex-grow: 0.5; 57 | } 58 | 59 | .arrows, .team-rank, .team-points { 60 | flex-grow: 0.2; 61 | justify-content: center; 62 | } 63 | 64 | .team-points { 65 | flex-grow: 0.6; 66 | } 67 | 68 | .arrows { 69 | cursor: pointer; 70 | flex-grow: 0.2; 71 | } 72 | 73 | .team-address { 74 | justify-content: flex-start; 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /client/components/Admin/AdminOptions/adminOptions.scss: -------------------------------------------------------------------------------- 1 | .admin-change-phase { 2 | padding: 50px 10px; 3 | text-align: center; 4 | font-size: 20px; 5 | 6 | span { 7 | margin-right: 10px; 8 | } 9 | 10 | button { 11 | display: inline-flex; 12 | align-items: center; 13 | justify-content: center; 14 | color: white; 15 | background: #673AB7; 16 | padding: 10px 20px; 17 | font-size: 16px; 18 | border-radius: 3px; 19 | 20 | &:hover { 21 | background: lighten(#673AB7, 15); 22 | } 23 | 24 | &:disabled { 25 | user-select: none; 26 | cursor: not-allowed; 27 | background: lighten(#673AB7, 30); 28 | } 29 | } 30 | 31 | .change-error { 32 | margin-top: 15px; 33 | font-size: 16px; 34 | color: #F57170; 35 | } 36 | } -------------------------------------------------------------------------------- /client/components/Admin/AdminSponsors/AdminSponsorsForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Field, reduxForm } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import formGroup from '../../FormComonent/FormComponent'; 5 | import { sponsorsFormValidator, submitAddSponsorsForm } from '../../../actions/sponsorActions'; 6 | import CubeLoader from '../../Decorative/CubeLoader/CubeLoader'; 7 | 8 | require('../../FormComonent/forms.scss'); 9 | 10 | let SponsorsTeamsForm = ({ handleSubmit, pristine, submittingForm, invalid, addSponsorError, submitText, submitTextSubmitting }) => ( // eslint-disable-line 11 |
12 | 22 | 23 | 33 | 34 | 44 | 45 | 55 | 56 | { addSponsorError &&
Error: {addSponsorError}
} 57 | 58 | 66 | 67 | ); 68 | 69 | SponsorsTeamsForm.defaultProps = { 70 | submitTextSubmitting: 'Adding', 71 | submitText: 'Add', 72 | }; 73 | 74 | const mapStateToProps = (state) => ({ 75 | submittingForm: state.sponsors.submitting, 76 | addSponsorError: state.sponsors.addSponsorError 77 | }); 78 | 79 | const validate = sponsorsFormValidator; 80 | 81 | SponsorsTeamsForm = reduxForm({ form: 'sponsorsForm', validate })(SponsorsTeamsForm); 82 | 83 | export default connect(mapStateToProps, { onSubmit: submitAddSponsorsForm })(SponsorsTeamsForm); 84 | -------------------------------------------------------------------------------- /client/components/Admin/AdminSponsors/_index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DecenterApps/Smart-Hackathon-DApp/31e6419a47c1a6c9885e35b359466c7fa26eb032/client/components/Admin/AdminSponsors/_index.scss -------------------------------------------------------------------------------- /client/components/Admin/AdminSponsors/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import OpenModalButton from '../../OpenModalButton/OpenModalButton'; 6 | import sponsorActions from '../../../actions/sponsorActions'; 7 | import Loader from '../../Decorative/Loader/index.jsx'; 8 | 9 | class AdminSponsors extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = {}; 14 | } 15 | componentWillMount() { 16 | this.props.fetchSponsors(); 17 | } 18 | render() { 19 | return ( 20 |
21 | { 22 | this.props.sponsors.isFetching && 23 |
24 |

Loading

25 |
26 | } 27 | { 28 | !this.props.sponsors.isFetching && 29 | this.props.sponsors.error && 30 |
31 |

{this.props.sponsors.error.toString()}

32 |
33 | } 34 | { 35 | !this.props.sponsors.isFetching && 36 | !this.props.sponsors.sponsors.length > 0 && 37 |
38 |
39 |

No sponsors

40 | 41 |
42 |
43 | } 44 | 45 | { 46 | !this.props.sponsors.isFetching && 47 | this.props.sponsors.sponsors.length > 0 && 48 | 49 | 50 | { 51 | this.props.sponsors.sponsors.map((sponsor) => ( 52 | 53 | 54 | 59 | 60 | 61 | )) 62 | } 63 | 64 |
{sponsor.args.sponsorName} 55 | 56 | {sponsor.args.sponsorSite.substr(0, 4) === 'http' ? sponsor.args.sponsorSite : 'http://' + sponsor.args.sponsorSite} 57 | 58 | {web3.fromWei(sponsor.args.amount).toString()} ETH
65 | } 66 |
67 | ); 68 | } 69 | } 70 | 71 | AdminSponsors.propTypes = { 72 | sponsors: PropTypes.shape({ 73 | sponsors: PropTypes.array, 74 | isFetching: PropTypes.bool, 75 | error: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), 76 | }), 77 | fetchSponsors: PropTypes.func.isRequired, 78 | }; 79 | AdminSponsors.defaultProps = { 80 | sponsors: { 81 | sponsors: [] 82 | }, 83 | isFetching: true 84 | }; 85 | 86 | const mapStateToProps = state => state; 87 | const mapDispatchToProps = dispatch => bindActionCreators({ 88 | ...sponsorActions 89 | }, dispatch); 90 | 91 | export default connect( 92 | mapStateToProps, 93 | mapDispatchToProps, 94 | )(AdminSponsors); 95 | 96 | -------------------------------------------------------------------------------- /client/components/Admin/AdminTeams/AdminTeamsForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Field, reduxForm } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import formGroup from '../../FormComonent/FormComponent'; 5 | import { teamsFormValidator, submitAddTeamsForm } from '../../../actions/teamActions'; 6 | import CubeLoader from '../../Decorative/CubeLoader/CubeLoader'; 7 | 8 | require('../../FormComonent/forms.scss'); 9 | 10 | let AdminTeamsForm = ({ handleSubmit, pristine, submittingForm, invalid, addTeamError }) => ( // eslint-disable-line 11 |
12 | 22 | 23 | 33 | 34 | 44 | 45 | 56 | 57 | { addTeamError &&
Error: {addTeamError}
} 58 | 59 | 67 | 68 | ); 69 | 70 | const mapStateToProps = (state) => ({ 71 | submittingForm: state.teams.submitting, 72 | addTeamError: state.teams.addTeamError 73 | }); 74 | 75 | const validate = teamsFormValidator; 76 | 77 | AdminTeamsForm = reduxForm({ form: 'teamsForm', validate })(AdminTeamsForm); 78 | 79 | export default connect(mapStateToProps, { onSubmit: submitAddTeamsForm })(AdminTeamsForm); 80 | -------------------------------------------------------------------------------- /client/components/Admin/AdminTeams/_index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DecenterApps/Smart-Hackathon-DApp/31e6419a47c1a6c9885e35b359466c7fa26eb032/client/components/Admin/AdminTeams/_index.scss -------------------------------------------------------------------------------- /client/components/Admin/AdminTeams/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import DollarIcon from '../../Decorative/DollarIcon/index.jsx'; 6 | import Loader from '../../Decorative/Loader/index.jsx'; 7 | import teamActions from '../../../actions/teamActions'; 8 | import OpenModalButton from '../../OpenModalButton/OpenModalButton'; 9 | 10 | require('./_index.scss'); 11 | 12 | class AdminTeams extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = {}; 17 | } 18 | componentWillMount() { 19 | this.props.fetchTeams(); 20 | } 21 | render() { 22 | return ( 23 |
24 | { 25 | this.props.teams.isFetching && 26 |
27 |

Loading

28 |
29 | } 30 | { 31 | !this.props.teams.isFetching && 32 | this.props.teams.error && 33 |
34 |

{this.props.teams.error.toString()}

35 |
36 | } 37 | { 38 | !this.props.teams.isFetching && 39 | !this.props.teams.error && 40 | !this.props.teams.teams.length > 0 && 41 |
42 |
43 |

No teams

44 | 45 |
46 |
47 | } 48 | { 49 | !this.props.teams.isFetching && 50 | !this.props.teams.error && 51 | this.props.teams.teams.length > 0 && 52 | 53 | 54 | { 55 | this.props.teams.teams.map((team) => ( 56 | 57 | 58 | 59 | 60 | 65 | 66 | )) 67 | } 68 | 69 |
{team.args.teamName}{team.args.memberNames} 61 | 62 | {team.args.teamAddress} 63 | 64 |
70 | } 71 |
72 | ); 73 | } 74 | } 75 | 76 | AdminTeams.propTypes = { 77 | teams: PropTypes.shape({ 78 | isFetching: PropTypes.bool.isRequired, 79 | teams: PropTypes.array, 80 | error: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired, 81 | }), 82 | fetchTeams: PropTypes.func.isRequired, 83 | }; 84 | 85 | AdminTeams.defaultProps = { 86 | teams: { 87 | isFetching: true, 88 | teams: [1] 89 | } 90 | }; 91 | 92 | const mapStateToProps = state => state; 93 | const mapDispatchToProps = dispatch => bindActionCreators({ 94 | ...teamActions 95 | }, dispatch); 96 | 97 | export default connect( 98 | mapStateToProps, 99 | mapDispatchToProps, 100 | )(AdminTeams); 101 | -------------------------------------------------------------------------------- /client/components/Admin/_index.scss: -------------------------------------------------------------------------------- 1 | $highlight: #673AB7; 2 | 3 | .empty-section { 4 | min-height: 150px; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | text-align: center; 9 | 10 | h1 { 11 | color: #777; 12 | font-size: 21px; 13 | margin: 20px; 14 | font-weight: normal; 15 | } 16 | 17 | button { 18 | border:0; 19 | outline: none; 20 | background-color: $highlight; 21 | color: white; 22 | padding: 10px 20px; 23 | border-radius: 3px; 24 | text-transform: uppercase; 25 | font-size: 14px; 26 | } 27 | } 28 | 29 | .tab-wrapper { 30 | display: flex; 31 | flex-direction: row; 32 | justify-content: space-between; 33 | border-bottom: 1px solid #eee; 34 | 35 | .left-section { 36 | a { 37 | user-select: none; 38 | display: inline-block; 39 | color: #BFBFBF; 40 | text-align: center; 41 | padding: 20px; 42 | margin: 0 10px; 43 | border-bottom: 3px solid white; 44 | cursor: pointer; 45 | 46 | &.active { 47 | color: $highlight; 48 | border-color: $highlight; 49 | } 50 | } 51 | } 52 | 53 | .right-section { 54 | cursor: pointer; 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | margin-right: 30px; 59 | 60 | button { 61 | color: $highlight; 62 | display: flex; 63 | align-items: center; 64 | justify-content: center; 65 | 66 | svg { 67 | margin-right: 7px; 68 | fill: $highlight; 69 | } 70 | 71 | &:hover { 72 | color: lighten($highlight, 20); 73 | svg { 74 | fill: lighten($highlight, 20); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | .admin-header { 82 | padding: 20px; 83 | h1 { 84 | color: #777; 85 | font-size: 21px; 86 | margin: 20px; 87 | font-weight: normal; 88 | } 89 | 90 | button { 91 | border:0; 92 | outline: none; 93 | background-color: $highlight; 94 | color: white; 95 | padding: 10px 20px; 96 | border-radius: 3px; 97 | text-transform: uppercase; 98 | font-size: 14px; 99 | } 100 | } 101 | 102 | .display-table { 103 | width: 100%; 104 | text-align: left; 105 | 106 | tr { 107 | border-top: 1px solid #eee; 108 | &:last-child { 109 | border-bottom: 1px solid #eee; 110 | } 111 | } 112 | 113 | th, td { 114 | &:first-child { 115 | padding-left: 30px; 116 | } 117 | &:last-child { 118 | padding-right: 30px; 119 | text-align: right; 120 | } 121 | 122 | padding: 20px 10px; 123 | 124 | &.highlighted { 125 | color: $highlight; 126 | } 127 | 128 | &.rewardable { 129 | width: 40px; 130 | line-height: 10px; 131 | padding-left: 20px; 132 | padding-right: 20px; 133 | } 134 | } 135 | th { 136 | font-weight: normal; 137 | font-size: 16px; 138 | 139 | &.order { 140 | font-size: 14px; 141 | color: #95989A; 142 | font-weight: normal; 143 | } 144 | 145 | } 146 | td { 147 | font-size: 14px; 148 | color: #777; 149 | } 150 | 151 | .clickable { 152 | cursor: pointer; 153 | 154 | &:active{ 155 | opacity: 0.8; 156 | } 157 | } 158 | .arrows { 159 | padding: 0px 10px; 160 | display: inline-block; 161 | float: right; 162 | 163 | &:first-of-type { 164 | padding-right: 17px; 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /client/components/Admin/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import { Redirect, Link } from 'react-router-dom'; 6 | import ModalWrapper from '../Modals/ModalWrapper/ModalWrapper'; 7 | import OpenModalButton from '../../components/OpenModalButton/OpenModalButton'; 8 | import userActions from '../../actions/userActions'; 9 | 10 | require('./_index.scss'); 11 | 12 | class Admin extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = {}; 17 | } 18 | componentDidMount() { 19 | this.props.checkUser(); 20 | } 21 | render() { 22 | return ( 23 |
24 | { 25 | this.props.user.isDetermined && 26 | this.props.user.type === 'administrator' && 27 |
28 |
29 |
30 |
31 | Teams 32 | Sponsors 33 | Judges 34 | Options 35 |
36 | 37 |
38 | { 39 | (location.hash === '#/admin/' || location.hash === '#/admin' || location.hash === '#/admin/teams') && 40 | this.props.teams.teams.length > 0 && 41 | 42 | } 43 | { 44 | (location.hash === '#/admin/sponsors') && this.props.sponsors.sponsors.length > 0 && 45 | 46 | } 47 | { 48 | (location.hash === '#/admin/judges') && this.props.judges.judges.length > 0 && 49 | 50 | } 51 |
52 |
53 | { this.props.children } 54 | 55 |
56 |
57 | } 58 | 59 | { 60 | this.props.user.isDetermined && 61 | this.props.user.type !== 'administrator' && 62 | 63 | } 64 | 65 | { 66 | this.props.user.isDetermined && 67 | this.props.user.type === 'administrator' && 68 | (location.hash === '#/admin/' || location.hash === '#/admin') && 69 | 70 | } 71 |
72 | ); 73 | } 74 | } 75 | 76 | Admin.propTypes = { 77 | teams: PropTypes.object.isRequired, 78 | sponsors: PropTypes.object.isRequired, 79 | judges: PropTypes.object.isRequired, 80 | children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, 81 | user: PropTypes.object.isRequired, 82 | checkUser: PropTypes.func.isRequired, 83 | }; 84 | 85 | 86 | const mapStateToProps = state => state; 87 | const mapDispatchToProps = dispatch => bindActionCreators({ 88 | ...userActions 89 | }, dispatch); 90 | 91 | export default connect( 92 | mapStateToProps, 93 | mapDispatchToProps, 94 | )(Admin); 95 | -------------------------------------------------------------------------------- /client/components/App/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | import Footer from '../Footer/Footer'; 5 | import AdminHeader from '../Header/HeaderStatus/index'; 6 | import AdminSponsorsForm from '../Admin/AdminSponsors/AdminSponsorsForm'; 7 | import { checkUser } from '../../actions/userActions'; 8 | import Scoreboard from '../Scoreboard/Scoreboard'; 9 | 10 | const styles = require('./app.scss'); 11 | 12 | class App extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = {}; 17 | } 18 | 19 | componentDidMount() { 20 | this.props.$checkUser(); 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 27 | 28 |
29 |
Contribute to prize pool
30 | 31 |
32 | 33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | App.propTypes = { 40 | $checkUser: PropTypes.func.isRequired 41 | }; 42 | 43 | export default connect(null, { $checkUser: checkUser })(App); 44 | -------------------------------------------------------------------------------- /client/components/App/app.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: "Open Sans", sans-serif; 4 | } 5 | 6 | html { 7 | font-size: 62.5%; 8 | min-height: 100%; 9 | width: 100%; 10 | } 11 | 12 | body { 13 | font-size: 1.6rem; 14 | font-family: Arial, sans-serif; 15 | margin: 0; 16 | padding: 0; 17 | min-height: 100%; 18 | width: 100%; 19 | background: #c05228; 20 | background: linear-gradient(45deg, #c05228 0%, #9F1B22 50%, #562889 100%); 21 | } 22 | 23 | .container { 24 | width: 100%; 25 | max-width: 1200px; 26 | margin: auto; 27 | 28 | &.white { 29 | background-color: white; 30 | border-radius: 3px; 31 | padding: 0; 32 | overflow: hidden; 33 | 34 | .content { 35 | padding: 20px; 36 | } 37 | } 38 | 39 | &.hero { 40 | height: calc(100vh - 200px); 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: center; 44 | text-align: center; 45 | 46 | h1 { 47 | color: #777; 48 | font-size: 36px; 49 | margin: 20px; 50 | font-weight: normal; 51 | } 52 | } 53 | } 54 | 55 | a { 56 | color: inherit; 57 | text-decoration: none; 58 | transition: 0.2s; 59 | 60 | &:focus, &:hover, &:link { 61 | text-decoration: none; 62 | } 63 | }; 64 | 65 | canvas { 66 | position: absolute; 67 | top: 0; 68 | right: 0; 69 | bottom: 0; 70 | left: 0; 71 | z-index: 0; 72 | } 73 | 74 | #root { 75 | position: relative; 76 | z-index: 1; 77 | } 78 | 79 | button { 80 | padding: 0; 81 | border: 0; 82 | background: none; 83 | cursor: pointer; 84 | 85 | &:focus { 86 | outline: 0; 87 | } 88 | } 89 | .app { 90 | text-align: center; 91 | 92 | .form-wrapper { 93 | display: inline-block; 94 | max-width: 470px; 95 | background: rgba(0, 0, 0, 0.1); 96 | border-radius: 3px; 97 | margin: 60px 0 40px 0; 98 | padding: 30px; 99 | 100 | form { 101 | width: 100%; 102 | max-width: 390px; 103 | display: inline-block; 104 | } 105 | 106 | .form-item { 107 | background: transparent; 108 | border-radius: 3px; 109 | border: 2px solid #fff; 110 | padding: 16px 20px; 111 | text-align: left; 112 | color: white; 113 | 114 | $gray: #D4D4D4; 115 | 116 | &::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 117 | color: $gray; 118 | } 119 | &::-moz-placeholder { /* Firefox 19+ */ 120 | color: $gray; 121 | } 122 | &:-ms-input-placeholder { /* IE 10+ */ 123 | color: $gray; 124 | } 125 | &:-moz-placeholder { /* Firefox 18- */ 126 | color: $gray; 127 | } 128 | } 129 | 130 | .form-item-error { 131 | border-color: #F57170; 132 | outline-color: #F57170; 133 | } 134 | 135 | .submit-button { 136 | background: white; 137 | color: #a72823; 138 | font-weight: 700; 139 | 140 | .sk-cube-grid .sk-cube { 141 | background-color: #FF9800; 142 | } 143 | 144 | &:hover:not(:disabled) { 145 | color: lighten(#FF9800, 15); 146 | } 147 | 148 | &:disabled { 149 | user-select: none; 150 | cursor: not-allowed; 151 | opacity: 0.5; 152 | } 153 | } 154 | } 155 | 156 | .form-name { 157 | color: #fff; 158 | font-size: 34px; 159 | font-weight: 700; 160 | } 161 | } 162 | 163 | .table { 164 | display: flex; 165 | flex-flow: column nowrap; 166 | font-size: 14px; 167 | line-height: 1.5; 168 | flex: 1 1 auto; 169 | color: #777; 170 | } 171 | 172 | .table-info-header { 173 | border-top: 0 !important; 174 | font-weight: bold; 175 | } 176 | 177 | .th { 178 | text-align: left; 179 | } 180 | 181 | .th > .td { 182 | white-space: normal; 183 | justify-content: center; 184 | } 185 | 186 | .tr { 187 | width: 100%; 188 | display: flex; 189 | flex-flow: row nowrap; 190 | border-bottom: 1px solid #eee; 191 | &:first-child { 192 | border-top: 1px solid #eee; 193 | } 194 | } 195 | 196 | .td { 197 | display: flex; 198 | flex-flow: row nowrap; 199 | align-items: center; 200 | flex-grow: 1; 201 | flex-basis: 0; 202 | padding: 20px 5px; 203 | overflow: hidden; 204 | text-overflow: ellipsis; 205 | min-width: 0; 206 | white-space: nowrap; 207 | } 208 | -------------------------------------------------------------------------------- /client/components/Decorative/AddCircleIcon/AddCircleIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const AddCircleIcon = ({ size }) => ( 5 | 6 | 7 | 11 | 12 | 13 | ); 14 | 15 | AddCircleIcon.propTypes = { 16 | size: PropTypes.string 17 | }; 18 | AddCircleIcon.defaultProps = { 19 | size: '18' 20 | }; 21 | 22 | export default AddCircleIcon; 23 | -------------------------------------------------------------------------------- /client/components/Decorative/CloseIcon/CloseIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const CloseIcon = ({ size }) => ( 5 | 16 | 25 | 26 | ); 27 | 28 | CloseIcon.propTypes = { 29 | size: PropTypes.string 30 | }; 31 | CloseIcon.defaultProps = { 32 | size: '25px' 33 | }; 34 | 35 | export default CloseIcon; 36 | -------------------------------------------------------------------------------- /client/components/Decorative/CubeLoader/CubeLoader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | require('./cubeLoader.scss'); 4 | 5 | const CubeLoader = () => ( 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ); 18 | 19 | export default CubeLoader; 20 | -------------------------------------------------------------------------------- /client/components/Decorative/CubeLoader/cubeLoader.scss: -------------------------------------------------------------------------------- 1 | .sk-cube-grid { 2 | width: 15px; 3 | height: 15px; 4 | margin-right: 15px; 5 | } 6 | 7 | .sk-cube-grid .sk-cube { 8 | width: 33%; 9 | height: 33%; 10 | background-color: white; 11 | float: left; 12 | -webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; 13 | animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; 14 | } 15 | .sk-cube-grid .sk-cube1 { 16 | -webkit-animation-delay: 0.2s; 17 | animation-delay: 0.2s; } 18 | .sk-cube-grid .sk-cube2 { 19 | -webkit-animation-delay: 0.3s; 20 | animation-delay: 0.3s; } 21 | .sk-cube-grid .sk-cube3 { 22 | -webkit-animation-delay: 0.4s; 23 | animation-delay: 0.4s; } 24 | .sk-cube-grid .sk-cube4 { 25 | -webkit-animation-delay: 0.1s; 26 | animation-delay: 0.1s; } 27 | .sk-cube-grid .sk-cube5 { 28 | -webkit-animation-delay: 0.2s; 29 | animation-delay: 0.2s; } 30 | .sk-cube-grid .sk-cube6 { 31 | -webkit-animation-delay: 0.3s; 32 | animation-delay: 0.3s; } 33 | .sk-cube-grid .sk-cube7 { 34 | -webkit-animation-delay: 0s; 35 | animation-delay: 0s; } 36 | .sk-cube-grid .sk-cube8 { 37 | -webkit-animation-delay: 0.1s; 38 | animation-delay: 0.1s; } 39 | .sk-cube-grid .sk-cube9 { 40 | -webkit-animation-delay: 0.2s; 41 | animation-delay: 0.2s; } 42 | 43 | @-webkit-keyframes sk-cubeGridScaleDelay { 44 | 0%, 70%, 100% { 45 | -webkit-transform: scale3D(1, 1, 1); 46 | transform: scale3D(1, 1, 1); 47 | } 35% { 48 | -webkit-transform: scale3D(0, 0, 1); 49 | transform: scale3D(0, 0, 1); 50 | } 51 | } 52 | 53 | @keyframes sk-cubeGridScaleDelay { 54 | 0%, 70%, 100% { 55 | -webkit-transform: scale3D(1, 1, 1); 56 | transform: scale3D(1, 1, 1); 57 | } 35% { 58 | -webkit-transform: scale3D(0, 0, 1); 59 | transform: scale3D(0, 0, 1); 60 | } 61 | } -------------------------------------------------------------------------------- /client/components/Decorative/DollarIcon/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const DollarIcon = ({ color, size }) => ( 5 | 6 | 15 | 16 | 17 | ); 18 | 19 | DollarIcon.propTypes = { 20 | color: PropTypes.string, 21 | size: PropTypes.string 22 | }; 23 | DollarIcon.defaultProps = { 24 | color: '#FFC107', 25 | size: '20px' 26 | }; 27 | 28 | export default DollarIcon; 29 | -------------------------------------------------------------------------------- /client/components/Decorative/EthereumIcon/EthereumIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const EthereumIcon = ({ size }) => ( 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | EthereumIcon.propTypes = { 26 | size: PropTypes.string 27 | }; 28 | EthereumIcon.defaultProps = { 29 | size: '20px' 30 | }; 31 | 32 | export default EthereumIcon; 33 | -------------------------------------------------------------------------------- /client/components/Decorative/Loader/_index.scss: -------------------------------------------------------------------------------- 1 | .loader { 2 | display: inline-block; 3 | width: 40px; 4 | height: 40px; 5 | background-color: #333; 6 | border-radius: 100%; 7 | vertical-align: bottom; 8 | margin: 0 5px; 9 | animation: sk-scaleout 1.0s infinite ease-in-out; 10 | } 11 | 12 | @-webkit-keyframes sk-scaleout { 13 | 0% { -webkit-transform: scale(0) } 14 | 100% { 15 | -webkit-transform: scale(1.0); 16 | opacity: 0; 17 | } 18 | } 19 | 20 | @keyframes sk-scaleout { 21 | 0% { 22 | -webkit-transform: scale(0); 23 | transform: scale(0); 24 | } 100% { 25 | -webkit-transform: scale(1.0); 26 | transform: scale(1.0); 27 | opacity: 0; 28 | } 29 | } -------------------------------------------------------------------------------- /client/components/Decorative/Loader/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | require('./_index.scss'); 5 | 6 | const Loader = ({ color, size }) => ( 7 | 8 | ); 9 | 10 | Loader.propTypes = { 11 | color: PropTypes.string, 12 | size: PropTypes.string 13 | }; 14 | Loader.defaultProps = { 15 | color: 'black', 16 | size: '30px' 17 | }; 18 | 19 | export default Loader; 20 | -------------------------------------------------------------------------------- /client/components/Decorative/MedalIcon/MedalIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const MedalIcon = ({ size, color }) => ( 5 | 6 | 7 | 12 | 13 | 14 | ); 15 | 16 | MedalIcon.propTypes = { 17 | size: PropTypes.string, 18 | color: PropTypes.string, 19 | }; 20 | MedalIcon.defaultProps = { 21 | size: '22', 22 | color: '#000' 23 | }; 24 | 25 | export default MedalIcon; 26 | -------------------------------------------------------------------------------- /client/components/Decorative/StarIcon/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const StarIcon = ({ color, size }) => ( 5 | 6 | 7 | 8 | 13 | 14 | 15 | ); 16 | 17 | 18 | StarIcon.propTypes = { 19 | color: PropTypes.string, 20 | size: PropTypes.string 21 | }; 22 | StarIcon.defaultProps = { 23 | color: '#FFC107', 24 | size: '20px' 25 | }; 26 | 27 | export default StarIcon; 28 | -------------------------------------------------------------------------------- /client/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | require('./footer.scss'); 4 | 5 | const Footer = () => ( 6 |
7 | Hackathon organized by Startit
8 | Hackathon DApp developed by Decenter 9 |
10 | ); 11 | 12 | export default Footer; 13 | -------------------------------------------------------------------------------- /client/components/Footer/footer.scss: -------------------------------------------------------------------------------- 1 | .footer{ 2 | text-align: center; 3 | bottom: 0; 4 | width: 100%; 5 | color: white; 6 | font-size: 16px; 7 | margin: 50px 0 35px; 8 | a:hover { 9 | color: white; 10 | text-decoration: underline; 11 | } 12 | } -------------------------------------------------------------------------------- /client/components/FormComonent/FormComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const FormGroup = ({ 5 | input, placeholder, wrapperClassName, inputClassName, errorClassName, showErrorText, 6 | type, id, showLabel, labelText, labelClass, meta: { touched, error } 7 | }) => ( 8 |
9 | 10 | {showLabel && } 11 | {touched && ((error && showErrorText &&
{error}
))} 12 |
13 | ); 14 | 15 | FormGroup.defaultProps = { 16 | showLabel: false, 17 | labelText: '', 18 | labelClass: '', 19 | id: '', 20 | placeholder: '', 21 | showErrorText: false 22 | }; 23 | 24 | FormGroup.propTypes = { 25 | input: PropTypes.any.isRequired, 26 | placeholder: PropTypes.string, 27 | wrapperClassName: PropTypes.string.isRequired, 28 | inputClassName: PropTypes.string.isRequired, 29 | errorClassName: PropTypes.string.isRequired, 30 | type: PropTypes.string.isRequired, 31 | id: PropTypes.string, 32 | showLabel: PropTypes.bool, 33 | labelText: PropTypes.string, 34 | labelClass: PropTypes.string, 35 | meta: PropTypes.object.isRequired, 36 | showErrorText: PropTypes.bool 37 | }; 38 | 39 | export default FormGroup; 40 | -------------------------------------------------------------------------------- /client/components/FormComonent/forms.scss: -------------------------------------------------------------------------------- 1 | .form-name { 2 | font-size: 24px; 3 | text-align: center; 4 | margin-bottom: 30px; 5 | } 6 | 7 | .form-wrapper { 8 | width: 100%; 9 | 10 | $errorColor: #F57170; 11 | 12 | .form-item-wrapper { 13 | position: relative; 14 | margin-bottom: 35px; 15 | 16 | .form-item { 17 | border: 2px solid #E2E2E2; 18 | border-radius: 3px; 19 | padding: 19px 0 19px 25px; 20 | width: 100%; 21 | font-size: 14px; 22 | } 23 | 24 | .form-item-error { 25 | border-color: $errorColor; 26 | outline-color: $errorColor; 27 | } 28 | 29 | div.form-item-error { 30 | position: absolute; 31 | color: $errorColor; 32 | top: 61px; 33 | } 34 | } 35 | 36 | .form-item-wrapper-checkbox { 37 | margin-bottom: 20px; 38 | display: flex; 39 | align-items: center; 40 | 41 | label { 42 | cursor: pointer; 43 | user-select: none; 44 | 45 | &:hover { 46 | color: lighten(black, 50); 47 | } 48 | } 49 | 50 | .form-item-checkbox { 51 | cursor: pointer; 52 | display: inline-block; 53 | position: relative; 54 | top: -2px; 55 | height: 24px; 56 | width: 24px; 57 | margin-right: 10px; 58 | } 59 | } 60 | 61 | .submit-error { 62 | font-size: 18px; 63 | margin-bottom: 20px; 64 | color: $errorColor; 65 | } 66 | 67 | .submit-button { 68 | display: flex; 69 | align-items: center; 70 | justify-content: center; 71 | width: 100%; 72 | color: white; 73 | background: #673AB7; 74 | padding: 19px 0; 75 | font-size: 16px; 76 | border-radius: 3px; 77 | 78 | &:hover { 79 | background: lighten(#673AB7, 15); 80 | } 81 | 82 | &:disabled { 83 | user-select: none; 84 | cursor: not-allowed; 85 | background: lighten(#673AB7, 30); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /client/components/Header/HeaderStatus/_index.scss: -------------------------------------------------------------------------------- 1 | .admin-header-period { 2 | display: flex; 3 | align-items: center; 4 | background-color: transparent; 5 | color: white; 6 | font-size: 18px; 7 | 8 | .period { 9 | margin-right: 10px; 10 | } 11 | } 12 | 13 | .user-type-view-link a { 14 | font-family: "Open Sans", sans-serif; 15 | font-weight: 700; 16 | margin-right: 25px; 17 | font-size: 18px; 18 | background: #842e77; 19 | padding: 12px; 20 | color: white; 21 | border-radius: 3px; 22 | 23 | &:hover { 24 | text-decoration: underline; 25 | } 26 | } -------------------------------------------------------------------------------- /client/components/Header/HeaderStatus/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | import { fetchPhase } from '../../../actions/userActions'; 6 | import Header from '../index.jsx'; 7 | import CubeLoader from '../../Decorative/CubeLoader/CubeLoader'; 8 | 9 | require('./_index.scss'); 10 | 11 | class AdminHeader extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = {}; 16 | } 17 | componentWillMount() { 18 | this.props.$fetchPhase(); 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |
25 | 26 | { 27 | this.props.isDetermined && 28 | this.props.userType === 'administrator' && 29 | (location.hash.indexOf('#/admin') === -1) && 30 | Administrator view 31 | } 32 | { 33 | this.props.isDetermined && 34 | this.props.userType === 'jury' && 35 | (location.hash.indexOf('#/jury') === -1) && 36 | Vote 37 | } 38 | 39 | 40 | 41 | Period: 42 | { this.props.isFetching && } 43 | 44 | { 45 | !this.props.isFetching && !this.props.phaseError && 46 | {this.props.phases[this.props.phase]} 47 | } 48 | 49 | { 50 | !this.props.isFetching && 51 | this.props.phaseError && 52 | {this.props.phaseError} 53 | } 54 | 55 |
56 |
57 | ); 58 | } 59 | } 60 | 61 | AdminHeader.propTypes = { 62 | isDetermined: PropTypes.bool.isRequired, 63 | userType: PropTypes.string.isRequired, 64 | phase: PropTypes.number.isRequired, 65 | $fetchPhase: PropTypes.func.isRequired, 66 | isFetching: PropTypes.bool.isRequired, 67 | phaseError: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired, 68 | phases: PropTypes.array.isRequired 69 | }; 70 | 71 | const mapStateToProps = (state) => ({ 72 | isDetermined: state.user.isDetermined, 73 | userType: state.user.type, 74 | phase: state.user.phase, 75 | isFetching: state.user.isFetching, 76 | phaseError: state.user.phaseError, 77 | phases: state.user.phases 78 | }); 79 | 80 | export default connect(mapStateToProps, { $fetchPhase: fetchPhase })(AdminHeader); 81 | 82 | -------------------------------------------------------------------------------- /client/components/Header/_index.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | max-width: 1200px; 4 | margin: auto; 5 | padding: 30px 0; 6 | display: flex; 7 | justify-content: space-between; 8 | 9 | .title { 10 | text-align: left; 11 | 12 | h1 { 13 | color: white; 14 | line-height: 30px; 15 | font-size: 26px; 16 | margin: 0; 17 | a { 18 | transition: all .1; 19 | } 20 | &:hover a { 21 | color: white; 22 | opacity: .8; 23 | } 24 | } 25 | h2 { 26 | color: white; 27 | opacity: .7; 28 | font-size: 16px; 29 | margin: 0; 30 | font-weight: normal; 31 | } 32 | } 33 | 34 | .other { 35 | display: flex; 36 | align-items: center; 37 | justify-content: center; 38 | } 39 | } -------------------------------------------------------------------------------- /client/components/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | require('./_index.scss'); 6 | 7 | const Header = ({ children, noLink }) => ( 8 |
9 |
10 |

11 | {noLink &&
Blockchain Community Hackathon
} 12 | {!noLink && Blockchain Community Hackathon} 13 |

14 |

Belgrade 25/26/27 August 2017

15 |
16 |
17 | {children} 18 |
19 |
20 | ); 21 | 22 | Header.propTypes = { 23 | children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, 24 | noLink: PropTypes.bool 25 | }; 26 | 27 | Header.defaultProps = { 28 | noLink: false, 29 | children: [] 30 | }; 31 | 32 | export default Header; 33 | -------------------------------------------------------------------------------- /client/components/Jury/_index.scss: -------------------------------------------------------------------------------- 1 | .table-header{ 2 | padding: 26px 21px 10px 21px; 3 | display: flex; 4 | 5 | &.jury-table-header { 6 | display: flex; 7 | justify-content: space-between; 8 | } 9 | 10 | .button-status-wrapper { 11 | display: flex; 12 | 13 | .voting-status { 14 | margin-right: 15px; 15 | margin-top: 5px; 16 | 17 | &.voting-error { 18 | color: #F57170; 19 | } 20 | 21 | &.voting-success { 22 | color: #28CC9E; 23 | } 24 | } 25 | } 26 | 27 | .title{ 28 | font-size: 20px; 29 | color: #212121; 30 | margin-bottom: 8px; 31 | line-height: 20px; 32 | } 33 | 34 | .subtitle { 35 | color: #757575; 36 | font-size: 14px; 37 | line-height: 14px; 38 | } 39 | 40 | .submit-button{ 41 | margin-left: auto; 42 | width: 100px; 43 | height: 38px; 44 | background-color: #673AB7; 45 | border-radius: 2px; 46 | font-size: 13px; 47 | color: #fff; 48 | 49 | &:hover:not(:disabled) { 50 | background: lighten(#673AB7, 15); 51 | } 52 | 53 | &:disabled { 54 | user-select: none; 55 | cursor: not-allowed; 56 | background: lighten(#673AB7, 30); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /client/components/Jury/assets/001-arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DecenterApps/Smart-Hackathon-DApp/31e6419a47c1a6c9885e35b359466c7fa26eb032/client/components/Jury/assets/001-arrows.png -------------------------------------------------------------------------------- /client/components/Jury/assets/002-arrows-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DecenterApps/Smart-Hackathon-DApp/31e6419a47c1a6c9885e35b359466c7fa26eb032/client/components/Jury/assets/002-arrows-1.png -------------------------------------------------------------------------------- /client/components/Jury/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { bindActionCreators } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | 7 | import * as teamActions from '../../actions/teamActions'; 8 | import userActions from '../../actions/userActions'; 9 | 10 | import Loader from '../Decorative/Loader/'; 11 | import arrowup from './assets/001-arrows.png'; 12 | import arrowdown from './assets/002-arrows-1.png'; 13 | 14 | require('./_index.scss'); 15 | 16 | class Jury extends Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = {}; 21 | 22 | this.voteForTeams = this.voteForTeams.bind(this); 23 | } 24 | 25 | componentDidMount() { 26 | this.props.checkUser(); 27 | this.props.fetchTeams(); 28 | this.props.checkIfJuryVoted(); 29 | } 30 | 31 | voteForTeams() { 32 | const votes = this.props.teams.teams.map((elem) => elem.args.teamAddress); 33 | 34 | console.log(votes); 35 | 36 | this.props.vote(votes); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 | { 43 | this.props.user.isDetermined && 44 | this.props.user.type === 'jury' && 45 |
46 |
47 |
48 |
49 |

Cast Your Votes

50 |

Arrange the teams in desired order.

51 |
52 | 53 | { 54 | this.props.user.votingError && 55 |
{this.props.user.votingError}
56 | } 57 | { 58 | this.props.user.votingSuccess && 59 |
Votes sent to contract
60 | } 61 | 66 |
67 |
68 | { 69 | this.props.teams.isFetching && 70 |
71 |

Loading

72 |
73 | } 74 | { 75 | !this.props.teams.isFetching && 76 | this.props.teams.error && 77 |
78 |

{this.props.teams.error.toString()}

79 |
80 | } 81 | { 82 | !this.props.teams.isFetching && 83 | !this.props.teams.error && 84 | !this.props.teams.teams.length > 0 && 85 |
86 |
87 |

No teams

88 |
89 |
90 | } 91 | 92 | { 93 | this.props.teams && 94 | 95 | 96 | { 97 | this.props.teams.teams.map((team, i) => ( 98 | 99 | 100 | 101 | 102 | 120 | 121 | )) 122 | } 123 | 124 |
{i + 1}.{team.args.teamName}{team.args.memberNames} 103 | this.props.moveTeamDown(i)} 106 | role="button" 107 | tabIndex="-1" 108 | > 109 | Move a team down 110 | 111 | this.props.moveTeamUp(i)} 114 | role="button" 115 | tabIndex="-1" 116 | > 117 | Move a team up 118 | 119 |
125 | } 126 |
127 |
128 | } 129 | 130 | { 131 | this.props.user.isDetermined && 132 | this.props.user.type !== 'jury' && 133 | 134 | } 135 | 136 |
137 | ); 138 | } 139 | } 140 | 141 | Jury.propTypes = { 142 | teams: PropTypes.shape({ 143 | isFetching: PropTypes.bool.isRequired, 144 | teams: PropTypes.array, 145 | error: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), 146 | }), 147 | moveTeamUp: PropTypes.func.isRequired, 148 | moveTeamDown: PropTypes.func.isRequired, 149 | vote: PropTypes.func.isRequired, 150 | user: PropTypes.object.isRequired, 151 | checkUser: PropTypes.func.isRequired, 152 | fetchTeams: PropTypes.func.isRequired, 153 | alreadyVoted: PropTypes.bool.isRequired, 154 | voting: PropTypes.bool.isRequired, 155 | votingError: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired, 156 | votingSuccess: PropTypes.bool.isRequired, 157 | checkIfJuryVoted: PropTypes.func.isRequired 158 | }; 159 | 160 | Jury.defaultProps = { 161 | alreadyVoted: false, 162 | votingError: false, 163 | voting: false, 164 | votingSuccess: false, 165 | teams: { 166 | isFetching: true, 167 | teams: [1] 168 | } 169 | }; 170 | 171 | const mapStateToProps = state => state; 172 | const mapDispatchToProps = dispatch => bindActionCreators({ 173 | ...teamActions, 174 | ...userActions 175 | }, dispatch); 176 | 177 | export default connect( 178 | mapStateToProps, 179 | mapDispatchToProps, 180 | )(Jury); 181 | 182 | -------------------------------------------------------------------------------- /client/components/Landing/_index.scss: -------------------------------------------------------------------------------- 1 | .top-level-wrapper { 2 | overflow: hidden; 3 | max-width: 1200px; 4 | margin: auto; 5 | padding: 0 30px; 6 | } 7 | 8 | .landing-wrapper { 9 | text-align: center; 10 | padding: 40px 0 !important; 11 | min-height: 300px; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | 16 | .end-wrapper { 17 | .table-wrapper { 18 | margin-top: 30px; 19 | 20 | .scoreboard-header { 21 | margin-bottom: 30px; 22 | } 23 | 24 | .table { 25 | margin-bottom: 0; 26 | 27 | .members { 28 | text-align: left; 29 | } 30 | 31 | .medal { 32 | flex-grow: 0.2; 33 | margin-right: 28px; 34 | } 35 | 36 | .last-child { 37 | //border-bottom: 0; 38 | } 39 | } 40 | } 41 | } 42 | 43 | &.last-phase { 44 | padding-bottom: 0 !important; 45 | overflow: visible; 46 | } 47 | 48 | &.chrome-metamask-wrapper { 49 | .browser-wrapper { 50 | h2 { 51 | margin-bottom: 0; 52 | } 53 | 54 | a { 55 | text-decoration: underline; 56 | margin: 0 5px; 57 | } 58 | 59 | &.not-chrome h2 { 60 | line-height: 35px; 61 | } 62 | } 63 | } 64 | 65 | p { 66 | margin: 0; 67 | a { 68 | text-decoration: underline; 69 | } 70 | } 71 | 72 | .sponsors-wrapper { 73 | margin: 50px 50px 30px; 74 | 75 | .prize-pool-wrapper { 76 | margin-top: 20px; 77 | } 78 | 79 | .sponsor-wrapper { 80 | display: inline-block; 81 | margin: 0 10px 10px; 82 | 83 | .logo { 84 | display: block !important; 85 | margin: 0 auto; 86 | height: 60px; 87 | width: 100px; 88 | background-size: contain !important; 89 | background-position: 50% !important; 90 | background-repeat: no-repeat !important; 91 | } 92 | } 93 | } 94 | 95 | .table-wrapper { 96 | margin: 50px 0 0; 97 | } 98 | 99 | .td.rewardable { 100 | max-width: 60px; 101 | padding-left: 20px; 102 | padding-right: 20px; 103 | } 104 | 105 | h2 { 106 | margin-bottom: 20px; 107 | } 108 | } -------------------------------------------------------------------------------- /client/components/Landing/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import { Link } from 'react-router-dom'; 6 | import userActions from '../../actions/userActions'; 7 | import teamActions from '../../actions/teamActions'; 8 | import sponsorActions from '../../actions/sponsorActions'; 9 | 10 | import Scoreboard from '../Scoreboard/Scoreboard'; 11 | import DollarIcon from '../Decorative/DollarIcon/index.jsx'; 12 | 13 | require('./_index.scss'); 14 | 15 | class Landing extends Component { 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = {}; 20 | } 21 | componentWillMount() { 22 | this.props.checkUser(); 23 | this.props.fetchSponsors(); 24 | this.props.fetchPrizePoolSize(); 25 | if(this.props.user.phase === 0) { 26 | this.props.fetchTeams(); 27 | } 28 | if(this.props.user.phase === 4) { 29 | this.props.fetchTeamScores(); 30 | } 31 | } 32 | componentWillReceiveProps(nextProps) { 33 | if(nextProps.user.phase !== this.props.user.phase) { 34 | console.log(nextProps.user.phase, this.props.user.phase); 35 | if(nextProps.user.phase === 4) { 36 | this.props.fetchTeamScores(); 37 | } else { 38 | this.props.fetchTeams(); 39 | } 40 | } 41 | } 42 | render() { 43 | return ( 44 |
45 |
46 | { 47 | this.props.user.phase === 0 && 48 |
49 |

Welcome to Blockchain Community Hackathon

50 |

51 | The competition has not started yet. 52 |
If you are a competitor, please make sure your details below are correct, 53 | especially Ethereum address! 54 |
55 | You can also contribute to the prize pool. 56 |

57 |
58 | } 59 | { 60 | this.props.user.phase === 1 && 61 |
62 |

Competition is ongoing.

63 |

64 | Feel free to contribute to the prize pool. 65 |

66 |
67 | } 68 | { 69 | (this.props.user.phase === 2 || 70 | this.props.user.phase === 3) && 71 |
72 |

The judges are voting, please be patient.

73 |

74 | Feel free to contribute to the prize pool. 75 |

76 |
77 | } 78 | { 79 | this.props.user.phase === 4 && 80 |
81 |

Thanks for being a part of Blockchain Community Hackathon!

82 | 83 |
84 |

Here is the final scoreboard:

85 | 86 |
87 |
88 | } 89 | { 90 | this.props.sponsors.sponsors.length > 0 && 91 |
92 |

Sponsors:

93 | { 94 | this.props.sponsors.sponsors.map((sponsor) => ( 95 | 112 | )) 113 | } 114 | { 115 | this.props.sponsors.ethPrize && 116 |

117 | Total prize pool size: 118 | { this.props.sponsors.ethPrize } ETH 119 | { 120 | this.props.sponsors.eurPrize && 121 | ({ this.props.sponsors.eurPrize } EUR) 122 | } 123 | 124 |

125 | } 126 |
127 | } 128 | { 129 | (this.props.user.phase === 0 || 130 | this.props.user.phase === 1 || 131 | this.props.user.phase === 2 || 132 | this.props.user.phase === 3) && 133 | this.props.teams.teams.length > 0 && 134 |
135 |

Teams:

136 | 137 | 138 | { 139 | this.props.teams.teams.filter(team => !team.args.disqualified).map((team) => ( 140 | 141 | 144 | 145 | 146 | 151 | 152 | )) 153 | } 154 | 155 |
142 | 143 | {team.args.teamName}{team.args.memberNames} 147 | 148 | {team.args.teamAddress} 149 | 150 |
156 |
157 | } 158 |
159 |
160 | ); 161 | } 162 | } 163 | 164 | Landing.propTypes = { 165 | user: PropTypes.object.isRequired, 166 | teams: PropTypes.shape({ 167 | teams: PropTypes.array, 168 | }), 169 | sponsors: PropTypes.shape({ 170 | sponsors: PropTypes.array, 171 | ethPrize: PropTypes.string, 172 | eurPrize: PropTypes.string, 173 | }), 174 | checkUser: PropTypes.func.isRequired, 175 | fetchTeams: PropTypes.func.isRequired, 176 | fetchTeamScores: PropTypes.func.isRequired, 177 | fetchSponsors: PropTypes.func.isRequired, 178 | fetchPrizePoolSize: PropTypes.func.isRequired, 179 | }; 180 | 181 | Landing.defaultProps = { 182 | teams: { 183 | teams: [] 184 | }, 185 | sponsors: { 186 | sponsors: [] 187 | }, 188 | }; 189 | 190 | const mapStateToProps = state => state; 191 | const mapDispatchToProps = dispatch => bindActionCreators({ 192 | ...userActions, 193 | ...teamActions, 194 | ...sponsorActions, 195 | }, dispatch); 196 | 197 | export default connect( 198 | mapStateToProps, 199 | mapDispatchToProps, 200 | )(Landing); 201 | 202 | -------------------------------------------------------------------------------- /client/components/Modals/JudgeModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AdminJudgesForm from '../Admin/AdminJudges/AdminJudgesForm'; 3 | 4 | const JudgeModal = () => ( 5 |
6 |
Add a judge
7 | 8 |
9 | ); 10 | 11 | export default JudgeModal; 12 | -------------------------------------------------------------------------------- /client/components/Modals/ModalWrapper/ModalWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | import { Modal } from 'react-modal-bootstrap'; 5 | import toggleModal from '../../../actions/modalsActions'; 6 | import CloseIcon from '../../Decorative/CloseIcon/CloseIcon'; 7 | 8 | require('./modalWrapper.scss'); 9 | 10 | const ModalWrapper = ({ isOpen, $toggleModal, currentModal, $style }) => ( 11 | 12 |
13 | $toggleModal(location.hash, false)} 17 | > 18 | 19 | 20 |
21 |
22 | { currentModal() } 23 |
24 |
25 | ); 26 | 27 | ModalWrapper.propTypes = { 28 | isOpen: PropTypes.bool.isRequired, 29 | $toggleModal: PropTypes.func.isRequired, 30 | currentModal: PropTypes.func.isRequired, 31 | $style: PropTypes.object.isRequired 32 | }; 33 | 34 | const mapStateToProps = (state) => ({ 35 | isOpen: state.modals.currentOpen, 36 | currentModal: state.modals.currentModal, 37 | $style: { open: { top: 95 } } 38 | }); 39 | 40 | export default connect(mapStateToProps, { $toggleModal: toggleModal })(ModalWrapper); 41 | -------------------------------------------------------------------------------- /client/components/Modals/ModalWrapper/modalWrapper.scss: -------------------------------------------------------------------------------- 1 | .modal-header { 2 | justify-content: flex-end; 3 | border-bottom: 0; 4 | 5 | span { 6 | display: flex; 7 | cursor: pointer; 8 | margin: 5px 10px 0 0; 9 | } 10 | } 11 | 12 | .modal-wrapper-body { 13 | padding: 6px 60px 60px 60px; 14 | 15 | display: flex; 16 | justify-content: center; 17 | } -------------------------------------------------------------------------------- /client/components/Modals/SponsorModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AdminSponsorsForm from '../Admin/AdminSponsors/AdminSponsorsForm'; 3 | 4 | const SponsorModal = () => ( 5 |
6 |
Add a sponsor
7 | 8 |
9 | ); 10 | 11 | export default SponsorModal; 12 | -------------------------------------------------------------------------------- /client/components/Modals/TeamModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AdminTeamsForm from '../Admin/AdminTeams/AdminTeamsForm'; 3 | 4 | const TeamModal = () => ( 5 |
6 |
Add a team
7 | 8 |
9 | ); 10 | 11 | export default TeamModal; 12 | -------------------------------------------------------------------------------- /client/components/NoMetamask/NoMetamask.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Header from '../Header/index'; 4 | 5 | const NoMetamask = ({ isChrome }) => ( 6 |
7 |
8 | 9 |
10 | { 11 | isChrome && 12 |
13 |

14 | In order to use this app you need to install the 15 | Metamask 16 | extension. 17 |

18 |
19 | } 20 | { 21 | !isChrome && 22 |
23 |

24 | In order to use this app you must access it 25 | through the Chrome web browser 26 |
27 | and install the 28 | Metamask 29 | extension. 30 |

31 |
32 | } 33 |
34 |
35 | ); 36 | 37 | NoMetamask.propTypes = { 38 | isChrome: PropTypes.bool.isRequired 39 | }; 40 | 41 | export default NoMetamask; 42 | -------------------------------------------------------------------------------- /client/components/OpenModalButton/OpenModalButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import toggleModal from '../../actions/modalsActions'; 5 | import AddCircleIcon from '../Decorative/AddCircleIcon/AddCircleIcon'; 6 | 7 | const OpenModalButton = ({ text, $toggleModal, showIcon, phase }) => ( 8 | 9 | { 10 | (phase === 0) && 11 | 15 | } 16 | 17 | ); 18 | 19 | OpenModalButton.defaultProps = { 20 | showIcon: false 21 | }; 22 | 23 | OpenModalButton.propTypes = { 24 | $toggleModal: PropTypes.func.isRequired, 25 | text: PropTypes.string.isRequired, 26 | showIcon: PropTypes.bool, 27 | phase: PropTypes.number.isRequired 28 | }; 29 | 30 | const mapStateToProps = (state) => ({ 31 | phase: state.user.phase 32 | }); 33 | 34 | export default connect(mapStateToProps, { $toggleModal: toggleModal })(OpenModalButton); 35 | -------------------------------------------------------------------------------- /client/components/Scoreboard/Scoreboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | import Loader from '../Decorative/Loader/index'; 5 | import { fetchTeamScores } from '../../actions/teamActions'; 6 | import MedalIcon from '../Decorative/MedalIcon/MedalIcon'; 7 | 8 | require('./scoreboard.scss'); 9 | 10 | let prizeIndex = 0; 11 | 12 | class Scoreboard extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = {}; 17 | 18 | this.renderReward = this.renderReward.bind(this); 19 | } 20 | 21 | renderReward(item) { 22 | // resets prizeIndex on re-rendering 23 | if(prizeIndex + 1 === this.props.teams.length) { 24 | prizeIndex = 0; 25 | } 26 | 27 | if (!item.args.rewardEligible) { 28 | return ( 29 | 'N/A' 30 | ); 31 | } 32 | 33 | prizeIndex++; 34 | 35 | return ( 36 | ( 37 | parseFloat(this.props.sponsors.ethPrize) 38 | / (2 ** (prizeIndex)) 39 | ).toString() + ' ETH' 40 | ); 41 | } 42 | 43 | render() { 44 | return ( 45 |
46 |
47 | { 48 | !this.props.isFetching && 49 | this.props.teamsFetchError && 50 | !this.props.teams.length > 0 && 51 |
52 | Error occurred 53 |
54 | } 55 | 56 | { 57 | this.props.isFetching && 58 |
59 |

Loading

60 |
61 | } 62 | 63 | { 64 | !this.props.isFetching && 65 | !this.props.teamsFetchError && 66 | this.props.teams.length > 0 && 67 |
68 |
69 |
Rank
70 |
71 |
Team
72 |
Members
73 |
Points
74 |
Reward
75 | 76 |
77 | { 78 | this.props.teams.filter(team => !team.args.disqualified).map((team, index) => ( 79 |
83 |
#{index + 1}
84 | 85 |
86 | {index === 0 && } 87 | {index === 1 && } 88 | {index === 2 && } 89 |
90 | 91 |
92 | {team.args.teamName} 93 |
94 | 95 |
{team.args.memberNames}
96 | 97 |
100 |
{team.args.totalScore}
101 | { 102 | team.args.scoreBreakdown && 103 | 107 | {team.args.scoreBreakdown.map((juryVote) => ( 108 | 109 | {juryVote.juryMemberName}: 110 | {juryVote.points} 111 | 112 | ))} 113 | 114 | } 115 |
116 | 117 | 118 | 119 | {this.renderReward(team)} 120 | 121 | 122 |
123 | )) 124 | } 125 |
126 | } 127 |
128 |
129 | ); 130 | } 131 | } 132 | 133 | Scoreboard.propTypes = { 134 | teams: PropTypes.array.isRequired, 135 | isFetching: PropTypes.bool.isRequired, 136 | teamsFetchError: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired, 137 | sponsors: PropTypes.object.isRequired, 138 | }; 139 | 140 | const mapStateToProps = (state) => ({ 141 | teams: state.teams.teams, 142 | isFetching: state.teams.isFetching, 143 | teamsFetchError: state.teams.error, 144 | sponsors: state.sponsors, 145 | }); 146 | 147 | export default connect(mapStateToProps, {})(Scoreboard); 148 | -------------------------------------------------------------------------------- /client/components/Scoreboard/scoreboard.scss: -------------------------------------------------------------------------------- 1 | .scoreboard { 2 | .table { 3 | color: black !important; 4 | } 5 | 6 | .container.white { 7 | padding: 0; 8 | overflow: visible; 9 | } 10 | 11 | .container-title { 12 | padding: 30px; 13 | font-size: 20px; 14 | border-bottom: 1px solid #dadada; 15 | } 16 | 17 | .scoreboard-regular-header { 18 | padding: 30px; 19 | font-size: 20px; 20 | text-align: left; 21 | border-top: 1px solid #dadada; 22 | } 23 | 24 | .teams { 25 | .team { 26 | 27 | .team-rank { 28 | font-size: 14px; 29 | justify-content: center; 30 | flex-grow: 0.4; 31 | } 32 | 33 | .medal, .reward-amount, .total-points { 34 | font-size: 14px; 35 | flex-grow: 0.4; 36 | justify-content: center; 37 | } 38 | 39 | .team-name-wrapper { 40 | .rewardable { 41 | position: relative; 42 | top: 3px; 43 | margin-right: 10px; 44 | } 45 | 46 | .team-name { 47 | font-weight: 500; 48 | font-size: 14px; 49 | white-space: nowrap; 50 | overflow: hidden; 51 | text-overflow: ellipsis; 52 | } 53 | } 54 | 55 | .total-points { 56 | align-items: center; 57 | justify-content: center; 58 | display: flex; 59 | flex-grow: 0.3; 60 | overflow: visible; 61 | cursor: pointer; 62 | } 63 | 64 | .members { 65 | flex-grow: 2; 66 | display: block; 67 | line-height: 27px; 68 | } 69 | 70 | .reward-amount { 71 | padding-right: 20px; 72 | line-height: 25px; 73 | flex-grow: 0.4; 74 | text-align: right; 75 | display: flex; 76 | align-items: center; 77 | justify-content: flex-end; 78 | 79 | .reward-amount-eth { 80 | white-space: nowrap; 81 | overflow: hidden; 82 | text-overflow: ellipsis; 83 | } 84 | 85 | .eth-icon-wrapper { 86 | margin-left: 7px; 87 | display: flex; 88 | align-items: center; 89 | justify-content: flex-end; 90 | } 91 | } 92 | } 93 | 94 | &:last-child { 95 | .team .td { 96 | //border-bottom: 0; 97 | } 98 | } 99 | } 100 | } 101 | 102 | div.tooltips { 103 | position: relative; 104 | display: inline; 105 | } 106 | div.tooltips .tooltip-wrapper { 107 | position: absolute; 108 | width: 250px; 109 | color: #FFFFFF; 110 | background: #673AB7; 111 | top: 49px; 112 | line-height: 25px; 113 | text-align: center; 114 | visibility: hidden; 115 | border-radius: 3px; 116 | 117 | .jury-row { 118 | display: flex; 119 | align-items: center; 120 | justify-content: space-between; 121 | font-size: 12px; 122 | padding: 3px 8px; 123 | 124 | .jury-name { 125 | max-width: 185px; 126 | white-space: nowrap; 127 | overflow: hidden; 128 | text-overflow: ellipsis; 129 | } 130 | } 131 | } 132 | div.tooltips .tooltip-wrapper:after { 133 | content: ''; 134 | position: absolute; 135 | top: -8px; 136 | transform: rotate(180deg); 137 | margin-left: -8px; 138 | width: 0; height: 0; 139 | border-top: 8px solid #673AB7; 140 | border-right: 8px solid transparent; 141 | border-left: 8px solid transparent; 142 | right: 19px; 143 | } 144 | div:hover.tooltips .tooltip-wrapper { 145 | visibility: visible; 146 | opacity: 0.9; 147 | bottom: 30px; 148 | right: 10%; 149 | z-index: 999; 150 | } 151 | -------------------------------------------------------------------------------- /client/components/Sponsor/_index.scss: -------------------------------------------------------------------------------- 1 | .table-header{ 2 | display: flex; 3 | 4 | .title{ 5 | font-size: 20px; 6 | color: #212121; 7 | margin-bottom: 8px; 8 | line-height: 20px; 9 | } 10 | 11 | .subtitle { 12 | color: #757575; 13 | font-size: 14px; 14 | line-height: 14px; 15 | } 16 | 17 | .submit-button{ 18 | margin-left: auto; 19 | width: 100px; 20 | height: 38px; 21 | background-color: #673AB7; 22 | border-radius: 2px; 23 | font-size: 13px; 24 | color: #fff; 25 | 26 | &:hover:not(:disabled) { 27 | background: lighten(#673AB7, 15); 28 | } 29 | 30 | &:disabled { 31 | user-select: none; 32 | cursor: not-allowed; 33 | background: lighten(#673AB7, 30); 34 | } 35 | } 36 | } 37 | 38 | 39 | 40 | .sponsor-thanks { 41 | font-size: 18px; 42 | margin-bottom: 20px; 43 | background-color: #43b743; 44 | padding: 20px; 45 | color: white; 46 | border-radius: 3px; 47 | } -------------------------------------------------------------------------------- /client/components/Sponsor/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | 4 | import PropTypes from 'prop-types'; 5 | import { bindActionCreators } from 'redux'; 6 | import { connect } from 'react-redux'; 7 | import * as userActions from '../../actions/userActions'; 8 | import AdminSponsorsForm from '../Admin/AdminSponsors/AdminSponsorsForm'; 9 | 10 | require('./_index.scss'); 11 | 12 | class Sponsor extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = {}; 17 | } 18 | 19 | componentDidMount() { 20 | this.props.checkUser(); 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | { 27 | this.props.user.phase > 2 && 28 | 29 | } 30 | 31 | { 32 | this.props.sponsors.showThanks && 33 |
Thanks for contributing!
34 | } 35 | 36 | { 37 | !this.props.sponsors.showThanks && 38 |
39 |
Contribute to prize pool
40 | 41 |
42 | } 43 |
44 | ); 45 | } 46 | } 47 | 48 | Sponsor.propTypes = { 49 | checkUser: PropTypes.func.isRequired, 50 | user: PropTypes.shape({ 51 | phase: PropTypes.number.isRequired 52 | }).isRequired, 53 | sponsors: PropTypes.object.isRequired 54 | }; 55 | const mapStateToProps = state => state; 56 | 57 | const mapDispatchToProps = dispatch => bindActionCreators({ 58 | ...userActions 59 | }, dispatch); 60 | 61 | export default connect( 62 | mapStateToProps, 63 | mapDispatchToProps, 64 | )(Sponsor); 65 | -------------------------------------------------------------------------------- /client/config/Routes.jsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux'; 2 | import { HashRouter, Route, Switch } from 'react-router-dom'; 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import App from '../components/App/App.jsx'; 6 | import Jury from '../components/Jury/index.jsx'; 7 | import Sponsor from '../components/Sponsor/index.jsx'; 8 | import Admin from '../components/Admin/index.jsx'; 9 | import AdminTeams from '../components/Admin/AdminTeams/index.jsx'; 10 | import AdminSponsors from '../components/Admin/AdminSponsors/index.jsx'; 11 | import AdminJudges from '../components/Admin/AdminJudges/index.jsx'; 12 | import Landing from '../components/Landing/index.jsx'; 13 | import HeaderStatus from '../components/Header/HeaderStatus/index'; 14 | import Footer from '../components/Footer/Footer'; 15 | import AdminOptions from '../components/Admin/AdminOptions/AdminOptions'; 16 | 17 | import { periodChangedListener } from '../actions/userActions'; 18 | import { judgeEventListener } from '../actions/judgeActions'; 19 | import { teamsEventListener } from '../actions/teamActions'; 20 | import { sponsorEventListener } from '../actions/sponsorActions'; 21 | 22 | class Routes extends Component { 23 | componentWillMount() { 24 | this.props.store.dispatch(periodChangedListener()); 25 | this.props.store.dispatch(judgeEventListener()); 26 | this.props.store.dispatch(teamsEventListener()); 27 | this.props.store.dispatch(sponsorEventListener()); 28 | } 29 | 30 | render() { 31 | return ( 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | 56 | Routes.propTypes = { 57 | store: PropTypes.object.isRequired, 58 | }; 59 | 60 | export default Routes; 61 | -------------------------------------------------------------------------------- /client/config/initWeb3.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default () => { 3 | if (typeof web3 !== 'undefined') { 4 | window.web3 = new Web3(web3.currentProvider); 5 | return true; 6 | } else { 7 | return false; 8 | } 9 | 10 | /*const web3 = require('web3'); 11 | 12 | if (!new Web3.providers.HttpProvider('http://localhost:8545')) { 13 | alert('Unable to connect to Web3 provider'); // make a page that says that it can't be used 14 | } 15 | 16 | window.web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));*/ 17 | }; 18 | -------------------------------------------------------------------------------- /client/config/storeGenerator.js: -------------------------------------------------------------------------------- 1 | import thunk from 'redux-thunk'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import { hashHistory } from 'react-router'; 4 | import { syncHistoryWithStore } from 'react-router-redux'; 5 | import reducers from '../reducers/index'; 6 | 7 | 8 | const reduxDevToolsEnchancer = window.__REDUX_DEVTOOLS_EXTENSION__ && 9 | window.__REDUX_DEVTOOLS_EXTENSION__(); 10 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore); 11 | const store = createStoreWithMiddleware(reducers, reduxDevToolsEnchancer); 12 | 13 | module.exports = { store }; 14 | -------------------------------------------------------------------------------- /client/external/particlesConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "particles": { 3 | "number": { 4 | "value": 50, 5 | "density": { 6 | "enable": true, 7 | "value_area": 800 8 | } 9 | }, 10 | "color": { 11 | "value": "#ffffff" 12 | }, 13 | "shape": { 14 | "type": "circle", 15 | "stroke": { 16 | "width": 0, 17 | "color": "#000000" 18 | }, 19 | "polygon": { 20 | "nb_sides": 5 21 | }, 22 | "image": { 23 | "src": "img/github.svg", 24 | "width": 100, 25 | "height": 100 26 | } 27 | }, 28 | "opacity": { 29 | "value": 0.5, 30 | "random": false, 31 | "anim": { 32 | "enable": false, 33 | "speed": 1, 34 | "opacity_min": 0.1, 35 | "sync": false 36 | } 37 | }, 38 | "size": { 39 | "value": 3, 40 | "random": true, 41 | "anim": { 42 | "enable": false, 43 | "speed": 40, 44 | "size_min": 0.1, 45 | "sync": false 46 | } 47 | }, 48 | "line_linked": { 49 | "enable": true, 50 | "distance": 150, 51 | "color": "#ffffff", 52 | "opacity": 0.4, 53 | "width": 1 54 | }, 55 | "move": { 56 | "enable": true, 57 | "speed": 3, 58 | "direction": "none", 59 | "random": false, 60 | "straight": false, 61 | "out_mode": "out", 62 | "bounce": false, 63 | "attract": { 64 | "enable": false, 65 | "rotateX": 600, 66 | "rotateY": 1200 67 | } 68 | } 69 | }, 70 | "interactivity": { 71 | "detect_on": "canvas", 72 | "events": { 73 | "onhover": { 74 | "enable": false, 75 | "mode": "grab" 76 | }, 77 | "onclick": { 78 | "enable": true, 79 | "mode": "push" 80 | }, 81 | "resize": true 82 | }, 83 | "modes": { 84 | "grab": { 85 | "distance": 140, 86 | "line_linked": { 87 | "opacity": 1 88 | } 89 | }, 90 | "bubble": { 91 | "distance": 400, 92 | "size": 40, 93 | "duration": 2, 94 | "opacity": 8, 95 | "speed": 3 96 | }, 97 | "repulse": { 98 | "distance": 200, 99 | "duration": 0.4 100 | }, 101 | "push": { 102 | "particles_nb": 4 103 | }, 104 | "remove": { 105 | "particles_nb": 2 106 | } 107 | } 108 | }, 109 | "retina_detect": true 110 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blockchain Community Hackathon 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Routes from './config/Routes.jsx'; 4 | import NoMetamask from './components/NoMetamask/NoMetamask'; 5 | import initWeb3 from './config/initWeb3'; 6 | import { history, store } from './config/storeGenerator'; 7 | import ParticlesJson from './external/particlesConfig.json'; 8 | 9 | particlesJS("particles-js", ParticlesJson); // eslint-disable-line 10 | 11 | const startApp = () => { 12 | const hasMetamask = initWeb3(); 13 | 14 | if (!hasMetamask) { 15 | let isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); 16 | 17 | ReactDOM.render(, document.getElementById('root')); 18 | } else { 19 | ReactDOM.render( 20 |
21 | 22 |
, document.getElementById('root')); 23 | } 24 | }; 25 | 26 | window.addEventListener('load', startApp); 27 | -------------------------------------------------------------------------------- /client/modules/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractAddress": "0x879716da78A75a44bdfa8F038ce875f99586940a", 3 | "startingBlock": 4202429, 4 | "abi": [{"constant":true,"inputs":[],"name":"currentPeriod","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalContribution","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"returnContributionsToTheSponsors","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_siteUrl","type":"string"},{"name":"_logoUrl","type":"string"}],"name":"contributeToPrizePool","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_teamAddress","type":"address"},{"name":"_memberNames","type":"string"},{"name":"_rewardEligible","type":"bool"}],"name":"registerTeam","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_ethAddress","type":"address"}],"name":"registerJuryMember","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_sortedTeams","type":"address[]"}],"name":"payoutPrizes","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_teamAddress","type":"address"}],"name":"disqualifyTeam","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_address","type":"address"}],"name":"getUserType","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"switchToNextPeriod","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getPrizePoolSize","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"restartPeriod","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_votes","type":"address[]"}],"name":"vote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_juryAddress","type":"address"}],"name":"checkJuryVoted","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newPeriod","type":"uint8"}],"name":"PeriodChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"teamName","type":"string"},{"indexed":false,"name":"teamAddress","type":"address"},{"indexed":false,"name":"memberNames","type":"string"},{"indexed":false,"name":"rewardEligible","type":"bool"}],"name":"TeamRegistered","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"juryMemberName","type":"string"},{"indexed":false,"name":"juryMemberAddress","type":"address"}],"name":"JuryMemberAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sponsorName","type":"string"},{"indexed":false,"name":"sponsorSite","type":"string"},{"indexed":false,"name":"sponsorLogoUrl","type":"string"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"SponsorshipReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"juryMemberName","type":"string"},{"indexed":true,"name":"teamAddress","type":"address"},{"indexed":false,"name":"points","type":"uint256"}],"name":"VoteReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"teamName","type":"string"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"PrizePaid","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"teamAddress","type":"address"}],"name":"TeamDisqualified","type":"event"}] 5 | } -------------------------------------------------------------------------------- /client/modules/ethereumService.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import contract from './config.json'; 3 | 4 | const networkIds = { 5 | mainnet: '1', 6 | morden: '2', 7 | ropsten: '3', 8 | kovan: '42', 9 | }; 10 | 11 | let hackathonContract; 12 | 13 | window.onload = () => { 14 | hackathonContract = web3.eth.contract(contract.abi).at(contract.contractAddress); 15 | // console.log(hackathonContract); 16 | }; 17 | 18 | export const getWeb3Status = () => 19 | new Promise((resolve, reject) => { 20 | if (!web3) { 21 | return reject({ 22 | message: 'NOT_FOUND', 23 | }); 24 | } 25 | 26 | return web3.version.getNetwork((err, netId) => { 27 | if (netId.toString() !== networkIds.kovan) { 28 | return reject({ 29 | message: 'WRONG_NETWORK', 30 | }); 31 | } 32 | 33 | return resolve(); 34 | }); 35 | }); 36 | 37 | export const getAccount = () => { 38 | if (!web3.eth.accounts || !web3.eth.accounts.length) { return false; } 39 | 40 | return web3.eth.accounts[0]; 41 | }; 42 | 43 | export const getBlockNumber = () => 44 | new Promise((resolve, reject) => { 45 | web3.eth.getBlockNumber((error, latestBlock) => { 46 | if (error) { 47 | return reject(error); 48 | } 49 | 50 | return resolve(latestBlock); 51 | }); 52 | }); 53 | 54 | /* Events */ 55 | 56 | export const JuryMemberAddedEvent = (callback) => { 57 | getBlockNumber() 58 | .then(latestBlock => { 59 | hackathonContract.JuryMemberAdded({}, { fromBlock: latestBlock, toBlock: 'latest' }) 60 | .watch((error, event) => { 61 | if (error) { 62 | return callback(error, null); 63 | } 64 | 65 | return callback(null, event); 66 | }); 67 | }) 68 | .catch(error => { 69 | return callback(error, null); 70 | }); 71 | }; 72 | 73 | export const PeriodChangedEvent = (callback) => { 74 | getBlockNumber() 75 | .then(latestBlock => { 76 | hackathonContract.PeriodChanged({}, { fromBlock: latestBlock, toBlock: 'latest' }) 77 | .watch((error, event) => { 78 | if (error) { 79 | return callback(error, null); 80 | } 81 | 82 | console.log(`Period changed event: ${event}`); 83 | return callback(null, event); 84 | }); 85 | }) 86 | .catch(error => { 87 | return callback(error, null); 88 | }); 89 | }; 90 | 91 | export const PrizePaidEvent = (callback) => { 92 | getBlockNumber() 93 | .then(latestBlock => { 94 | hackathonContract.PrizePaid({}, { fromBlock: latestBlock, toBlock: 'latest' }) 95 | .watch((error, event) => { 96 | if (error) { 97 | return callback(error, null); 98 | } 99 | 100 | console.log(`Prize paid event: ${event}`); 101 | return callback(null, event); 102 | }); 103 | }) 104 | .catch(error => { 105 | callback(error, null); 106 | }); 107 | }; 108 | 109 | export const SponsorshipReceivedEvent = (callback) => { 110 | getBlockNumber() 111 | .then(latestBlock => { 112 | hackathonContract.SponsorshipReceived({}, { fromBlock: latestBlock, toBlock: 'latest' }) 113 | .watch((error, event) => { 114 | if (error) { 115 | return callback(error, null); 116 | } 117 | 118 | console.log(`Sponsorship received event: ${event}`); 119 | return callback(null, event); 120 | }); 121 | }) 122 | .catch(error => { 123 | callback(error, null); 124 | }); 125 | }; 126 | 127 | export const TeamRegisteredEvent = (callback) => { 128 | getBlockNumber() 129 | .then(latestBlock => { 130 | hackathonContract.TeamRegistered({}, { fromBlock: latestBlock, toBlock: 'latest' }) 131 | .watch((error, event) => { 132 | if (error) { 133 | return callback(error, null); 134 | } 135 | 136 | return callback(null, event); 137 | }); 138 | }) 139 | .catch(error => { 140 | callback(error, null); 141 | }); 142 | }; 143 | 144 | export const VotesReceivedEvent = (callback) => { 145 | getBlockNumber() 146 | .then(latestBlock => { 147 | hackathonContract.VotesReceived({}, { fromBlock: latestBlock, toBlock: 'latest' }) 148 | .watch((error, event) => { 149 | if (error) { 150 | return callback(error, null); 151 | } 152 | 153 | return callback(null, event); 154 | }); 155 | }) 156 | .catch(error => { 157 | callback(error, null); 158 | }); 159 | }; 160 | 161 | /* Contract functions (prefixed by "_") */ 162 | 163 | export const _switchToNextPeriod = () => 164 | new Promise((resolve, reject) => { 165 | hackathonContract.switchToNextPeriod((error, result) => { 166 | if (error) { 167 | return reject({ 168 | message: error, 169 | }); 170 | } 171 | 172 | return resolve(result); 173 | }); 174 | }); 175 | 176 | export const _registerTeam = (name, teamAddress, memberNames, rewardEligible) => 177 | new Promise((resolve, reject) => { 178 | if (!web3.isAddress(teamAddress)) { 179 | return reject({ 180 | message: 'Invalid team address.', 181 | }); 182 | } 183 | 184 | return hackathonContract.registerTeam(name, teamAddress, memberNames, !!rewardEligible, 185 | (error, result) => { 186 | if (error) { 187 | return reject({ 188 | message: error, 189 | }); 190 | } 191 | 192 | return resolve(result); 193 | }); 194 | }); 195 | 196 | export const _registerJuryMember = (juryMemberName, juryMemberAddress) => 197 | new Promise((resolve, reject) => { 198 | if (!web3.isAddress(juryMemberAddress)) { 199 | return reject({ 200 | message: 'Invalid jury member address.', 201 | }); 202 | } 203 | 204 | return hackathonContract.registerJuryMember(juryMemberName, juryMemberAddress, (error, result) => { 205 | if (error) { 206 | return reject({ 207 | message: error, 208 | }); 209 | } 210 | 211 | return resolve(result); 212 | }); 213 | }); 214 | 215 | export const _contributeToPrizePool = (name, amount, siteUrl, logoUrl) => 216 | new Promise((resolve, reject) => { 217 | hackathonContract.contributeToPrizePool( 218 | name.toString(), 219 | siteUrl.toString(), 220 | logoUrl.toString(), 221 | { value: web3.toWei(amount, 'ether') }, 222 | (error, result) => { 223 | if (error) { 224 | return reject({ 225 | message: error, 226 | }); 227 | } 228 | 229 | return resolve(result); 230 | }); 231 | }); 232 | 233 | export const _vote = (votes) => 234 | new Promise((resolve, reject) => { 235 | for (let i = 0; i < votes.length; i += 1) { 236 | if (!web3.isAddress(votes[i])) { 237 | return reject({ 238 | message: `Team at index ${i} has an invalid address.`, 239 | }); 240 | } 241 | } 242 | 243 | return hackathonContract.vote(votes, (error, result) => { 244 | if (error) { 245 | return reject({ 246 | message: error, 247 | }); 248 | } 249 | 250 | return resolve(result); 251 | }); 252 | }); 253 | 254 | export const _payoutPrizes = (teamAddresses) => 255 | new Promise((resolve, reject) => { 256 | console.log(teamAddresses); 257 | hackathonContract.payoutPrizes( 258 | teamAddresses, 259 | (error, result) => { 260 | if (error) { 261 | return reject({ 262 | message: error, 263 | }); 264 | } 265 | 266 | return resolve(result); 267 | }); 268 | }); 269 | 270 | /* Getters for contract state */ 271 | 272 | export const getTeams = () => 273 | new Promise((resolve, reject) => { 274 | getDisqualifiedTeams() 275 | .then(disqualifiedTeams => { 276 | hackathonContract.TeamRegistered({}, { 277 | fromBlock: contract.startingBlock, toBlock: 'latest' 278 | }) 279 | .get((error, events) => { 280 | if (error) { 281 | return reject({ 282 | message: error, 283 | }); 284 | } 285 | let alteredEvents = events.map(item => { 286 | if (disqualifiedTeams.indexOf(item.args.teamAddress) === -1) return item; 287 | let alteredEvent = item; 288 | alteredEvent.args.disqualified = true; 289 | return alteredEvent 290 | }); 291 | return resolve(alteredEvents); 292 | }); 293 | }); 294 | }); 295 | 296 | export const getDisqualifiedTeams = () => 297 | new Promise((resolve, reject) => { 298 | hackathonContract.TeamDisqualified({}, { 299 | fromBlock: contract.startingBlock, toBlock: 'latest' 300 | }) 301 | .get((error, events) => { 302 | if (error) { 303 | return reject({ 304 | message: error, 305 | }); 306 | } 307 | 308 | return resolve(events.map(item => item.args.teamAddress)); 309 | }); 310 | }); 311 | 312 | export const getPhase = () => 313 | new Promise((resolve, reject) => { 314 | hackathonContract.currentPeriod((error, data) => { 315 | if (error) { 316 | return reject({ 317 | message: error, 318 | }); 319 | } 320 | 321 | return resolve(data.toString(10)); 322 | }); 323 | }); 324 | 325 | export const getJuries = () => 326 | new Promise((resolve, reject) => { 327 | hackathonContract.JuryMemberAdded({}, { 328 | fromBlock: contract.startingBlock, toBlock: 'latest' 329 | }) 330 | .get((error, events) => { 331 | if (error) { 332 | return reject({ 333 | message: error, 334 | }); 335 | } 336 | 337 | return resolve(events); 338 | }); 339 | }); 340 | 341 | export const getSponsors = () => 342 | new Promise((resolve, reject) => { 343 | hackathonContract.SponsorshipReceived({}, { 344 | fromBlock: contract.startingBlock, toBlock: 'latest', 345 | }) 346 | .get((error, events) => { 347 | if (error) { 348 | return reject({ 349 | message: error, 350 | }); 351 | } 352 | 353 | return resolve(events); 354 | }); 355 | }); 356 | 357 | export const getTeamScores = (teamAddresses) => 358 | new Promise((resolve, reject) => { 359 | hackathonContract.VoteReceived({}, { 360 | fromBlock: contract.startingBlock, toBlock: 'latest', 361 | }) 362 | .get((error, events) => { 363 | if (error) { 364 | return reject({ 365 | message: error, 366 | }); 367 | } 368 | 369 | return resolve(events); 370 | }); 371 | }); 372 | 373 | export const getUserType = () => 374 | new Promise((resolve, reject) => { 375 | hackathonContract.getUserType(getAccount(), (error, result) => { 376 | if (error) { 377 | return reject({ 378 | message: error, 379 | }); 380 | } 381 | 382 | return resolve(result); 383 | }); 384 | }); 385 | 386 | export const getUserTypeWithTimeout = () => 387 | new Promise((resolve, reject) => { 388 | if (getAccount()) { 389 | getUserType() 390 | .then((res) => resolve(res)) 391 | .catch((error) => reject(error)); 392 | } else { 393 | setTimeout(() => { 394 | getUserType() 395 | .then((res) => resolve(res)) 396 | .catch((error) => reject(error)); 397 | }, 500); 398 | } 399 | }); 400 | 401 | export const checkJuryVoted = () => { 402 | new Promise((resolve) => { 403 | hackathonContract.checkJuryVoted(getAccount(), (error, result) => { 404 | return resolve(result); 405 | }); 406 | }); 407 | 408 | }; 409 | 410 | export const getPrizePoolSize = () => 411 | new Promise((resolve, reject) => { 412 | hackathonContract.getPrizePoolSize((error, result) => { 413 | if (error) { 414 | return reject({ 415 | message: error, 416 | }); 417 | } 418 | 419 | return resolve(result); 420 | }); 421 | }); 422 | 423 | // setTimeout(() => { 424 | // getDisqualifiedTeams() 425 | // .then(data => console.log(data)); 426 | // }, 1000); 427 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { routerReducer } from 'react-router-redux'; 2 | import { reducer as formReducer } from 'redux-form'; 3 | import { combineReducers } from 'redux'; 4 | import userReducer from './userReducer'; 5 | import modalsReducer from './modalsReducer'; 6 | import teamsReducer from './teamsReducer'; 7 | import sponsorsReducer from './sponsorsReducer'; 8 | import judgesReducer from './judgesReducer'; 9 | 10 | 11 | export default combineReducers({ 12 | form: formReducer, 13 | routing: routerReducer, 14 | user: userReducer, 15 | modals: modalsReducer, 16 | teams: teamsReducer, 17 | sponsors: sponsorsReducer, 18 | judges: judgesReducer, 19 | }); 20 | -------------------------------------------------------------------------------- /client/reducers/judgesReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | JUDGES_FETCH, JUDGES_SUCCESS, JUDGES_ERROR, ADD_JUDGE, ADD_JUDGE_SUCCESS, 3 | ADD_JUDGE_ERROR, NEW_JUDGE 4 | } from '../actions/types'; 5 | 6 | 7 | const INITIAL_STATE = { 8 | isFetching: true, 9 | judges: [], 10 | submitting: false, 11 | addJudgeError: false 12 | }; 13 | 14 | export default (state = INITIAL_STATE, action) => { 15 | switch (action.type) { 16 | case JUDGES_FETCH: 17 | return { 18 | ...state, 19 | isFetching: true 20 | }; 21 | 22 | case JUDGES_SUCCESS: 23 | return { 24 | ...state, 25 | isFetching: false, 26 | judges: action.judges 27 | }; 28 | 29 | case JUDGES_ERROR: 30 | return { 31 | ...state, 32 | isFetching: false, 33 | error: action.error 34 | }; 35 | 36 | case NEW_JUDGE: 37 | return { 38 | ...state, 39 | judges: [ 40 | ...state.judges, 41 | action.event 42 | ] 43 | }; 44 | 45 | case ADD_JUDGE: 46 | return { 47 | ...state, 48 | submitting: true 49 | }; 50 | 51 | case ADD_JUDGE_SUCCESS: 52 | return { 53 | ...state, 54 | submitting: false, 55 | addJudgeError: false, 56 | }; 57 | 58 | case ADD_JUDGE_ERROR: 59 | return { 60 | ...state, 61 | submitting: false, 62 | addJudgeError: action.payload.addSponsorError 63 | }; 64 | 65 | default: 66 | return state; 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /client/reducers/modalsReducer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TOGGLE_MODAL } from '../actions/types'; 3 | 4 | const INITIAL_STATE = { 5 | currentOpen: false, 6 | currentModal: () =>
error
7 | }; 8 | 9 | export default (state = INITIAL_STATE, action) => { 10 | switch (action.type) { 11 | case TOGGLE_MODAL: 12 | return { 13 | ...state, 14 | currentModal: action.payload.modalComponent, 15 | currentOpen: action.payload.state 16 | }; 17 | 18 | default: 19 | return state; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /client/reducers/sponsorsReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SPONSORS_FETCH, SPONSORS_SUCCESS, SPONSORS_ERROR, ADD_SPONSOR, ADD_SPONSOR_SUCCESS, 3 | ADD_SPONSOR_ERROR, NEW_SPONSOR, SPONSORS_PRIZE_ETH, SPONSORS_PRIZE_EUR 4 | } from '../actions/types'; 5 | 6 | const INITIAL_STATE = { 7 | isFetching: true, 8 | sponsors: [], 9 | submitting: false, 10 | addSponsorError: false, 11 | ethPrize: '0', 12 | eurPrize: '0', 13 | showThanks: false, 14 | }; 15 | 16 | export default (state = INITIAL_STATE, action) => { 17 | switch (action.type) { 18 | case SPONSORS_FETCH: 19 | return { 20 | ...state, 21 | isFetching: true 22 | }; 23 | 24 | case SPONSORS_SUCCESS: 25 | return { 26 | ...state, 27 | isFetching: false, 28 | sponsors: action.sponsors 29 | }; 30 | 31 | case SPONSORS_ERROR: 32 | return { 33 | ...state, 34 | isFetching: false, 35 | error: action.error 36 | }; 37 | 38 | case NEW_SPONSOR: 39 | return { 40 | ...state, 41 | sponsors: [ 42 | ...state.sponsors, 43 | action.event, 44 | ] 45 | }; 46 | 47 | case ADD_SPONSOR: 48 | return { 49 | ...state, 50 | submitting: true 51 | }; 52 | 53 | case ADD_SPONSOR_SUCCESS: 54 | return { 55 | ...state, 56 | submitting: false, 57 | addSponsorError: false, 58 | showThanks: true, 59 | }; 60 | 61 | case ADD_SPONSOR_ERROR: 62 | return { 63 | ...state, 64 | submitting: false, 65 | addSponsorError: action.payload.addSponsorError, 66 | showThanks: false, 67 | }; 68 | 69 | case SPONSORS_PRIZE_ETH: 70 | return { 71 | ...state, 72 | ethPrize: action.prize, 73 | eurPrize: undefined, 74 | }; 75 | 76 | case SPONSORS_PRIZE_EUR: 77 | return { 78 | ...state, 79 | eurPrize: action.prize, 80 | }; 81 | 82 | default: 83 | return state; 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /client/reducers/teamsReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | NEW_TEAM, 3 | TEAM_UP, 4 | TEAM_DOWN, 5 | TEAMS_FETCH, 6 | TEAMS_SUCCESS, 7 | TEAMS_ERROR, 8 | ADD_TEAM, 9 | ADD_TEAM_ERROR, 10 | ADD_TEAM_SUCCESS 11 | } from '../actions/types'; 12 | 13 | const INITIAL_STATE = { 14 | isFetching: true, 15 | teams: [], 16 | submitting: false, 17 | addTeamError: false, 18 | error: false 19 | }; 20 | 21 | export default (state = INITIAL_STATE, action) => { 22 | switch (action.type) { 23 | case TEAMS_FETCH: 24 | return { 25 | ...state, 26 | isFetching: true, 27 | teams: [], 28 | }; 29 | 30 | case TEAMS_SUCCESS: 31 | return { 32 | ...state, 33 | isFetching: false, 34 | teams: action.teams, 35 | error: false, 36 | }; 37 | 38 | case TEAMS_ERROR: 39 | return { 40 | ...state, 41 | isFetching: false, 42 | error: action.error 43 | }; 44 | 45 | case NEW_TEAM: 46 | return { 47 | ...state, 48 | teams: [ 49 | ...state.teams, 50 | action.event, 51 | ] 52 | }; 53 | 54 | case ADD_TEAM: 55 | return { 56 | ...state, 57 | submitting: true 58 | }; 59 | 60 | case ADD_TEAM_SUCCESS: 61 | return { 62 | ...state, 63 | submitting: false, 64 | addTeamError: false, 65 | }; 66 | 67 | case ADD_TEAM_ERROR: 68 | return { 69 | ...state, 70 | submitting: false, 71 | addTeamError: action.payload.addTeamError 72 | }; 73 | 74 | case TEAM_UP: 75 | return { 76 | ...state, 77 | teams: [ 78 | ...state.teams.slice(0, action.index - 1), 79 | state.teams[action.index], 80 | state.teams[action.index - 1], 81 | ...state.teams.slice(action.index + 1), 82 | ] 83 | }; 84 | 85 | case TEAM_DOWN: 86 | return { 87 | ...state, 88 | teams: [ 89 | ...state.teams.slice(0, action.index), 90 | state.teams[action.index + 1], 91 | state.teams[action.index], 92 | ...state.teams.slice(action.index + 2), 93 | ] 94 | }; 95 | 96 | default: 97 | return state; 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /client/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | USER_CHECKING, USER_FOUND, PHASE_FETCH, PHASE_FETCH_SUCCESS, PHASE_FETCH_ERROR, 3 | CHANGE_PHASE, CHANGE_PHASE_SUCCESS, CHANGE_PHASE_ERROR, 4 | SUBMIT_PAYOUT, SUBMIT_PAYOUT_SUCCESS, SUBMIT_PAYOUT_ERROR, UPDATE_PHASE, 5 | ALREADY_VOTED, VOTE, VOTE_SUCCESS, VOTE_ERROR 6 | } from '../actions/types'; 7 | 8 | 9 | const INITIAL_STATE = { 10 | isDetermined: false, 11 | type: 'other', 12 | phases: ['Registration', 'Competition', 'Voting', 'Verification', 'End'], 13 | phase: 0, 14 | isFetching: false, 15 | phaseError: false, 16 | changingError: false, 17 | changingPhase: false, 18 | submittingPayout: false, 19 | submittingPayoutError: false, 20 | alreadyVoted: false, 21 | voting: false, 22 | votingError: false, 23 | votingSuccess: false 24 | }; 25 | 26 | export default (state = INITIAL_STATE, action) => { 27 | switch (action.type) { 28 | case USER_CHECKING: 29 | return state; 30 | case USER_FOUND: 31 | return { 32 | ...state, 33 | isDetermined: true, 34 | type: action.userType, 35 | }; 36 | case PHASE_FETCH: 37 | return { 38 | ...state, 39 | isFetching: true 40 | }; 41 | 42 | case PHASE_FETCH_SUCCESS: 43 | return { 44 | ...state, 45 | isFetching: false, 46 | phase: action.payload.phase, 47 | phaseError: false 48 | }; 49 | 50 | case PHASE_FETCH_ERROR: 51 | return { 52 | ...state, 53 | isFetching: false, 54 | phaseError: 'Error occurred' 55 | }; 56 | 57 | case CHANGE_PHASE: 58 | return { 59 | ...state, 60 | changingPhase: true 61 | }; 62 | 63 | case CHANGE_PHASE_SUCCESS: 64 | return { 65 | ...state, 66 | changingPhase: false, 67 | changingError: false 68 | }; 69 | 70 | case UPDATE_PHASE: 71 | return { 72 | ...state, 73 | phase: action.payload.phase, 74 | }; 75 | 76 | case CHANGE_PHASE_ERROR: 77 | return { 78 | ...state, 79 | changingPhase: false, 80 | changingError: 'Error occurred while changing period' 81 | }; 82 | 83 | case SUBMIT_PAYOUT: 84 | return { 85 | ...state, 86 | submittingPayout: true 87 | }; 88 | 89 | case SUBMIT_PAYOUT_SUCCESS: 90 | return { 91 | ...state, 92 | submittingPayout: false, 93 | submittingPayoutError: false 94 | }; 95 | 96 | case SUBMIT_PAYOUT_ERROR: 97 | return { 98 | ...state, 99 | submittingPayout: false, 100 | submittingPayoutError: action.message 101 | }; 102 | 103 | case ALREADY_VOTED: 104 | return { 105 | ...state, 106 | alreadyVoted: true 107 | }; 108 | 109 | case VOTE: 110 | return { 111 | ...state, 112 | voting: true 113 | }; 114 | 115 | case VOTE_SUCCESS: 116 | return { 117 | ...state, 118 | voting: false, 119 | votingError: false, 120 | votingSuccess: true 121 | }; 122 | 123 | case VOTE_ERROR: 124 | return { 125 | ...state, 126 | voting: false, 127 | votingSuccess: false, 128 | votingError: 'Error occurred while voting' 129 | }; 130 | 131 | default: 132 | return state; 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /dist/521fb616232adb3eb64828d5a3d820a1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DecenterApps/Smart-Hackathon-DApp/31e6419a47c1a6c9885e35b359466c7fa26eb032/dist/521fb616232adb3eb64828d5a3d820a1.png -------------------------------------------------------------------------------- /dist/eecc5a0815a9233ebd31eb7d77eda4b1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DecenterApps/Smart-Hackathon-DApp/31e6419a47c1a6c9885e35b359466c7fa26eb032/dist/eecc5a0815a9233ebd31eb7d77eda4b1.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blockchain Community Hackathon 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /dist/main.css: -------------------------------------------------------------------------------- 1 | .footer{text-align:center;bottom:0;width:100%;color:#fff;font-size:16px;margin:50px 0 35px}.footer a:hover{color:#fff;text-decoration:underline}.header{width:100%;max-width:1200px;margin:auto;padding:30px 0;display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between}.header .title{text-align:left}.header .title h1{color:#fff;line-height:30px;font-size:26px;margin:0}.header .title h1 a{transition:all .1}.header .title h1:hover a{color:#fff;opacity:.8}.header .title h2{color:#fff;opacity:.7;font-size:16px;margin:0;font-weight:400}.header .other{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.sk-cube-grid{width:15px;height:15px;margin-right:15px}.sk-cube-grid .sk-cube{width:33%;height:33%;background-color:#fff;float:left;animation:sk-cubeGridScaleDelay 1.3s infinite ease-in-out}.sk-cube-grid .sk-cube1{animation-delay:.2s}.sk-cube-grid .sk-cube2{animation-delay:.3s}.sk-cube-grid .sk-cube3{animation-delay:.4s}.sk-cube-grid .sk-cube4{animation-delay:.1s}.sk-cube-grid .sk-cube5{animation-delay:.2s}.sk-cube-grid .sk-cube6{animation-delay:.3s}.sk-cube-grid .sk-cube7{animation-delay:0s}.sk-cube-grid .sk-cube8{animation-delay:.1s}.sk-cube-grid .sk-cube9{animation-delay:.2s}@keyframes sk-cubeGridScaleDelay{0%,70%,to{transform:scale3D(1,1,1)}35%{transform:scale3D(0,0,1)}}.admin-header-period{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;background-color:transparent;color:#fff;font-size:18px}.admin-header-period .period{margin-right:10px}.user-type-view-link a{font-family:Open Sans,sans-serif;font-weight:700;margin-right:25px;font-size:18px;background:#842e77;padding:12px;color:#fff;border-radius:3px}.user-type-view-link a:hover{text-decoration:underline}.form-name{font-size:24px;text-align:center;margin-bottom:30px}.form-wrapper{width:100%}.form-wrapper .form-item-wrapper{position:relative;margin-bottom:35px}.form-wrapper .form-item-wrapper .form-item{border:2px solid #e2e2e2;border-radius:3px;padding:19px 0 19px 25px;width:100%;font-size:14px}.form-wrapper .form-item-wrapper .form-item-error{border-color:#f57170;outline-color:#f57170}.form-wrapper .form-item-wrapper div.form-item-error{position:absolute;color:#f57170;top:61px}.form-wrapper .form-item-wrapper-checkbox{margin-bottom:20px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.form-wrapper .form-item-wrapper-checkbox label{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.form-wrapper .form-item-wrapper-checkbox label:hover{color:gray}.form-wrapper .form-item-wrapper-checkbox .form-item-checkbox{cursor:pointer;display:inline-block;position:relative;top:-2px;height:24px;width:24px;margin-right:10px}.form-wrapper .submit-error{font-size:18px;margin-bottom:20px;color:#f57170}.form-wrapper .submit-button{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:100%;color:#fff;background:#673ab7;padding:19px 0;font-size:16px;border-radius:3px}.form-wrapper .submit-button:hover{background:#916dd1}.form-wrapper .submit-button:disabled{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:not-allowed;background:#bda7e3}.loader{display:inline-block;width:40px;height:40px;background-color:#333;border-radius:100%;vertical-align:bottom;margin:0 5px;animation:sk-scaleout 1s infinite ease-in-out}@keyframes sk-scaleout{0%{transform:scale(0)}to{transform:scale(1);opacity:0}}.scoreboard .table{color:#000!important}.scoreboard .container.white{padding:0;overflow:visible}.scoreboard .container-title{padding:30px;font-size:20px;border-bottom:1px solid #dadada}.scoreboard .scoreboard-regular-header{padding:30px;font-size:20px;text-align:left;border-top:1px solid #dadada}.scoreboard .teams .team .medal,.scoreboard .teams .team .reward-amount,.scoreboard .teams .team .team-rank,.scoreboard .teams .team .total-points{font-size:14px;-ms-flex-pack:center;justify-content:center;-ms-flex-positive:0.4;flex-grow:0.4}.scoreboard .teams .team .team-name-wrapper .rewardable{position:relative;top:3px;margin-right:10px}.scoreboard .teams .team .team-name-wrapper .team-name{font-weight:500;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.scoreboard .teams .team .total-points{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;display:-ms-flexbox;display:flex;-ms-flex-positive:0.3;flex-grow:0.3;overflow:visible;cursor:pointer}.scoreboard .teams .team .members{-ms-flex-positive:2;flex-grow:2;display:block;line-height:27px}.scoreboard .teams .team .reward-amount{padding-right:20px;line-height:25px;-ms-flex-positive:0.4;flex-grow:0.4;text-align:right;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end}.scoreboard .teams .team .reward-amount .reward-amount-eth{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.scoreboard .teams .team .reward-amount .eth-icon-wrapper{margin-left:7px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end}div.tooltips{position:relative;display:inline}div.tooltips .tooltip-wrapper{position:absolute;width:250px;color:#fff;background:#673ab7;top:49px;line-height:25px;text-align:center;visibility:hidden;border-radius:3px}div.tooltips .tooltip-wrapper .jury-row{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;font-size:12px;padding:3px 8px}div.tooltips .tooltip-wrapper .jury-row .jury-name{max-width:185px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}div.tooltips .tooltip-wrapper:after{content:"";position:absolute;top:-8px;transform:rotate(180deg);margin-left:-8px;width:0;height:0;border-top:8px solid #673ab7;border-right:8px solid transparent;border-left:8px solid transparent;right:19px}div:hover.tooltips .tooltip-wrapper{visibility:visible;opacity:.9;bottom:30px;right:10%;z-index:999}*{box-sizing:border-box;font-family:Open Sans,sans-serif}html{font-size:62.5%}body,html{min-height:100%;width:100%}body{font-size:1.6rem;font-family:Arial,sans-serif;margin:0;padding:0;background:#c05228;background:linear-gradient(45deg,#c05228,#9f1b22 50%,#562889)}.container{width:100%;max-width:1200px;margin:auto}.container.white{background-color:#fff;border-radius:3px;padding:0;overflow:hidden}.container.white .content{padding:20px}.container.hero{height:calc(100vh - 200px);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;text-align:center}.container.hero h1{color:#777;font-size:36px;margin:20px;font-weight:400}a{color:inherit;transition:.2s}a,a:focus,a:hover,a:link{text-decoration:none}canvas{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0}#root{position:relative;z-index:1}button{padding:0;border:0;background:none;cursor:pointer}button:focus{outline:0}.app{text-align:center}.app .form-wrapper{display:inline-block;max-width:470px;background:rgba(0,0,0,.1);border-radius:3px;margin:60px 0 40px;padding:30px}.app .form-wrapper form{width:100%;max-width:390px;display:inline-block}.app .form-wrapper .form-item{background:transparent;border-radius:3px;border:2px solid #fff;padding:16px 20px;text-align:left;color:#fff}.app .form-wrapper .form-item::-webkit-input-placeholder{color:#d4d4d4}.app .form-wrapper .form-item::-moz-placeholder{color:#d4d4d4}.app .form-wrapper .form-item:-ms-input-placeholder{color:#d4d4d4}.app .form-wrapper .form-item:-moz-placeholder{color:#d4d4d4}.app .form-wrapper .form-item-error{border-color:#f57170;outline-color:#f57170}.app .form-wrapper .submit-button{background:#fff;color:#a72823;font-weight:700}.app .form-wrapper .submit-button .sk-cube-grid .sk-cube{background-color:#ff9800}.app .form-wrapper .submit-button:hover:not(:disabled){color:#ffb74d}.app .form-wrapper .submit-button:disabled{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:not-allowed;opacity:.5}.app .form-name{color:#fff;font-size:34px;font-weight:700}.table{display:-ms-flexbox;display:flex;-ms-flex-flow:column nowrap;flex-flow:column nowrap;font-size:14px;line-height:1.5;-ms-flex:1 1 auto;flex:1 1 auto;color:#777}.table-info-header{border-top:0!important;font-weight:700}.th{text-align:left}.th>.td{white-space:normal;-ms-flex-pack:center;justify-content:center}.tr{width:100%;display:-ms-flexbox;display:flex;-ms-flex-flow:row nowrap;flex-flow:row nowrap;border-bottom:1px solid #eee}.tr:first-child{border-top:1px solid #eee}.td{display:-ms-flexbox;display:flex;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-align:center;align-items:center;-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;padding:20px 5px;overflow:hidden;text-overflow:ellipsis;min-width:0;white-space:nowrap}.table-header{padding:26px 21px 10px;display:-ms-flexbox;display:flex}.table-header.jury-table-header{-ms-flex-pack:justify;justify-content:space-between}.table-header .button-status-wrapper,.table-header.jury-table-header{display:-ms-flexbox;display:flex}.table-header .button-status-wrapper .voting-status{margin-right:15px;margin-top:5px}.table-header .button-status-wrapper .voting-status.voting-error{color:#f57170}.table-header .button-status-wrapper .voting-status.voting-success{color:#28cc9e}.table-header .title{font-size:20px;color:#212121;margin-bottom:8px;line-height:20px}.table-header .subtitle{color:#757575;font-size:14px;line-height:14px}.table-header .submit-button{margin-left:auto;width:100px;height:38px;background-color:#673ab7;border-radius:2px;font-size:13px;color:#fff}.table-header .submit-button:hover:not(:disabled){background:#916dd1}.table-header .submit-button:disabled{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:not-allowed;background:#bda7e3}.table-header{display:-ms-flexbox;display:flex}.table-header .title{font-size:20px;color:#212121;margin-bottom:8px;line-height:20px}.table-header .subtitle{color:#757575;font-size:14px;line-height:14px}.table-header .submit-button{margin-left:auto;width:100px;height:38px;background-color:#673ab7;border-radius:2px;font-size:13px;color:#fff}.table-header .submit-button:hover:not(:disabled){background:#916dd1}.table-header .submit-button:disabled{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:not-allowed;background:#bda7e3}.modal-header{-ms-flex-pack:end;justify-content:flex-end;border-bottom:0}.modal-header span{display:-ms-flexbox;display:flex;cursor:pointer;margin:5px 10px 0 0}.modal-wrapper-body{padding:6px 60px 60px;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center}.empty-section{min-height:150px;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;text-align:center}.empty-section h1{color:#777;font-size:21px;margin:20px;font-weight:400}.empty-section button{border:0;outline:none;background-color:#673ab7;color:#fff;padding:10px 20px;border-radius:3px;text-transform:uppercase;font-size:14px}.tab-wrapper{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-pack:justify;justify-content:space-between;border-bottom:1px solid #eee}.tab-wrapper .left-section a{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:inline-block;color:#bfbfbf;text-align:center;padding:20px;margin:0 10px;border-bottom:3px solid #fff;cursor:pointer}.tab-wrapper .left-section a.active{color:#673ab7;border-color:#673ab7}.tab-wrapper .right-section{cursor:pointer;margin-right:30px}.tab-wrapper .right-section,.tab-wrapper .right-section button{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.tab-wrapper .right-section button{color:#673ab7}.tab-wrapper .right-section button svg{margin-right:7px;fill:#673ab7}.tab-wrapper .right-section button:hover{color:#9f80d7}.tab-wrapper .right-section button:hover svg{fill:#9f80d7}.admin-header{padding:20px}.admin-header h1{color:#777;font-size:21px;margin:20px;font-weight:400}.admin-header button{border:0;outline:none;background-color:#673ab7;color:#fff;padding:10px 20px;border-radius:3px;text-transform:uppercase;font-size:14px}.display-table{width:100%;text-align:left}.display-table tr{border-top:1px solid #eee}.display-table tr:last-child{border-bottom:1px solid #eee}.display-table td,.display-table th{padding:20px 10px}.display-table td:first-child,.display-table th:first-child{padding-left:30px}.display-table td:last-child,.display-table th:last-child{padding-right:30px;text-align:right}.display-table td.highlighted,.display-table th.highlighted{color:#673ab7}.display-table td.rewardable,.display-table th.rewardable{width:40px;line-height:10px;padding-left:20px;padding-right:20px}.display-table th{font-weight:400;font-size:16px}.display-table th.order{font-size:14px;color:#95989a;font-weight:400}.display-table td{font-size:14px;color:#777}.display-table .clickable{cursor:pointer}.display-table .clickable:active{opacity:.8}.display-table .arrows{padding:0 10px;display:inline-block;float:right}.display-table .arrows:first-of-type{padding-right:17px}.top-level-wrapper{overflow:hidden;max-width:1200px;margin:auto;padding:0 30px}.landing-wrapper{text-align:center;padding:40px 0!important;min-height:300px;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center}.landing-wrapper .end-wrapper .table-wrapper{margin-top:30px}.landing-wrapper .end-wrapper .table-wrapper .scoreboard-header{margin-bottom:30px}.landing-wrapper .end-wrapper .table-wrapper .table{margin-bottom:0}.landing-wrapper .end-wrapper .table-wrapper .table .members{text-align:left}.landing-wrapper .end-wrapper .table-wrapper .table .medal{-ms-flex-positive:0.2;flex-grow:0.2;margin-right:28px}.landing-wrapper.last-phase{padding-bottom:0!important;overflow:visible}.landing-wrapper.chrome-metamask-wrapper .browser-wrapper h2{margin-bottom:0}.landing-wrapper.chrome-metamask-wrapper .browser-wrapper a{text-decoration:underline;margin:0 5px}.landing-wrapper.chrome-metamask-wrapper .browser-wrapper.not-chrome h2{line-height:35px}.landing-wrapper p{margin:0}.landing-wrapper p a{text-decoration:underline}.landing-wrapper .sponsors-wrapper{margin:50px 50px 30px}.landing-wrapper .sponsors-wrapper .prize-pool-wrapper{margin-top:20px}.landing-wrapper .sponsors-wrapper .sponsor-wrapper{display:inline-block;margin:0 10px 10px}.landing-wrapper .sponsors-wrapper .sponsor-wrapper .logo{display:block!important;margin:0 auto;height:60px;width:100px;background-size:contain!important;background-position:50%!important;background-repeat:no-repeat!important}.landing-wrapper .table-wrapper{margin:50px 0 0}.landing-wrapper .td.rewardable{max-width:60px;padding-left:20px;padding-right:20px}.landing-wrapper h2{margin-bottom:20px}.payout-wrapper{border-top:1px solid #d0d0d0}.payout-wrapper .payout-error{color:#f57170;font-size:16px;margin-left:20px}.payout-wrapper button{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;color:#fff;background:#673ab7;padding:10px 20px;font-size:16px;border-radius:3px;margin-left:10px}.payout-wrapper button:hover{background:#916dd1}.payout-wrapper button:disabled{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:not-allowed;background:#bda7e3}.payout-wrapper .pay-button-wrapper{font-size:18px;font-weight:700;margin:20px}.payout-wrapper .table-header{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:center;align-items:center;border-top:1px solid #d0d0d0;font-size:20px;padding:30px}.payout-wrapper .table-header .tip{font-size:14px}.payout-wrapper .table{border-bottom:0;margin-bottom:0}.payout-wrapper .table .team-name{-ms-flex-positive:0.5;flex-grow:0.5}.payout-wrapper .table .arrows,.payout-wrapper .table .team-points,.payout-wrapper .table .team-rank{-ms-flex-positive:0.2;flex-grow:0.2;-ms-flex-pack:center;justify-content:center}.payout-wrapper .table .team-points{-ms-flex-positive:0.6;flex-grow:0.6}.payout-wrapper .table .arrows{cursor:pointer;-ms-flex-positive:0.2;flex-grow:0.2}.payout-wrapper .table .team-address{-ms-flex-pack:start;justify-content:flex-start}.admin-change-phase{padding:50px 10px;text-align:center;font-size:20px}.admin-change-phase span{margin-right:10px}.admin-change-phase button{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;color:#fff;background:#673ab7;padding:10px 20px;font-size:16px;border-radius:3px}.admin-change-phase button:hover{background:#916dd1}.admin-change-phase button:disabled{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:not-allowed;background:#bda7e3}.admin-change-phase .change-error{margin-top:15px;font-size:16px;color:#f57170} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DecenterHackaton", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/DecenterApps/DecenterHackaton.git", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "webpack-dev-server --history-api-fallback --compress --config ./webpack/webpack-dev.config.js", 9 | "prod": "webpack --config ./webpack/webpack-prod.config.js", 10 | "prod-serve": "webpack-dev-server --history-api-fallback --compress --config ./webpack/webpack-prod.config.js" 11 | }, 12 | "dependencies": { 13 | "autoprefixer-loader": "^3.2.0", 14 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 15 | "clean-webpack-plugin": "^0.1.16", 16 | "css-loader": "^0.28.4", 17 | "eslint": "^4.4.1", 18 | "eslint-config-airbnb": "^15.1.0", 19 | "eslint-loader": "^1.9.0", 20 | "eslint-plugin-import": "^2.7.0", 21 | "eslint-plugin-jsx-a11y": "^6.0.2", 22 | "eslint-plugin-react": "^7.2.1", 23 | "extract-text-webpack-plugin": "^3.0.0", 24 | "file-loader": "^0.11.2", 25 | "html-webpack-plugin": "^2.30.1", 26 | "image-loader": "^0.0.1", 27 | "image-webpack-loader": "^3.3.1", 28 | "node-sass": "^4.5.3", 29 | "optimize-css-assets-webpack-plugin": "^3.0.0", 30 | "path": "^0.12.7", 31 | "prop-types": "^15.5.10", 32 | "react": "^15.6.1", 33 | "react-dom": "^15.6.1", 34 | "react-modal-bootstrap": "^1.1.1", 35 | "react-redux": "^5.0.6", 36 | "react-router": "^4.1.2", 37 | "react-router-dom": "^4.1.2", 38 | "react-router-redux": "^4.0.8", 39 | "redux": "^3.7.2", 40 | "redux-form": "^7.0.3", 41 | "redux-thunk": "^2.2.0", 42 | "sass-loader": "^6.0.6", 43 | "scriptjs": "^2.5.8", 44 | "style-loader": "^0.18.2", 45 | "web3": "^0.20.1", 46 | "webpack": "^3.5.4", 47 | "webpack-dev-server": "^2.7.1" 48 | }, 49 | "devDependencies": { 50 | "babel-core": "^6.25.0", 51 | "babel-loader": "^7.1.1", 52 | "babel-plugin-transform-exponentiation-operator": "^6.24.1", 53 | "babel-preset-es2015": "^6.24.1", 54 | "babel-preset-react": "^6.24.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /solidity/DecenterHackathon.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.15; 2 | 3 | contract DecenterHackathon { 4 | 5 | struct Team { 6 | string name; 7 | string memberNames; 8 | uint score; 9 | uint reward; 10 | bool rewardEligible; 11 | bool submittedByAdmin; 12 | bool disqualified; 13 | mapping(address => bool) votedForByJuryMember; 14 | } 15 | 16 | struct JuryMember { 17 | string name; 18 | bool hasVoted; 19 | } 20 | 21 | struct Sponsor { 22 | string name; 23 | string siteUrl; 24 | string logoUrl; 25 | address ethAddress; 26 | uint contribution; 27 | } 28 | 29 | enum Period { Registration, Competition, Voting, Verification, End } 30 | 31 | uint public totalContribution; 32 | Period public currentPeriod; 33 | 34 | mapping(address => Team) teams; 35 | mapping(address => JuryMember) juryMembers; 36 | 37 | address administrator; 38 | address[] teamAddresses; 39 | address[] juryMemberAddresses; 40 | Sponsor[] sponsors; 41 | 42 | event PeriodChanged(Period newPeriod); 43 | event TeamRegistered(string teamName, address teamAddress, string memberNames, bool rewardEligible); 44 | event JuryMemberAdded(string juryMemberName, address juryMemberAddress); 45 | event SponsorshipReceived(string sponsorName, string sponsorSite, string sponsorLogoUrl, uint amount); 46 | event VoteReceived(string juryMemberName, address indexed teamAddress, uint points); 47 | event PrizePaid(string teamName, uint amount); 48 | event TeamDisqualified(address teamAddress); 49 | 50 | modifier onlyOwner { 51 | require(msg.sender == administrator); 52 | _; 53 | } 54 | 55 | modifier onlyJury { 56 | require(bytes(juryMembers[msg.sender].name).length > 0); 57 | _; 58 | } 59 | 60 | function DecenterHackathon() { 61 | administrator = msg.sender; 62 | currentPeriod = Period.Registration; 63 | } 64 | 65 | // Administrator is able to switch between periods at any time 66 | function switchToNextPeriod() onlyOwner { 67 | if(currentPeriod == Period.Verification || currentPeriod == Period.End) { 68 | return; 69 | } 70 | 71 | currentPeriod = Period(uint(currentPeriod) + 1); 72 | 73 | PeriodChanged(currentPeriod); 74 | } 75 | 76 | // Administrator can add new teams during registration period, with an option to make a team non-eligible for the prize 77 | function registerTeam(string _name, address _teamAddress, string _memberNames, bool _rewardEligible) onlyOwner { 78 | require(currentPeriod == Period.Registration); 79 | require(bytes(teams[_teamAddress].name).length == 0); 80 | 81 | teams[_teamAddress] = Team({ 82 | name: _name, 83 | memberNames: _memberNames, 84 | score: 0, 85 | reward: 0, 86 | rewardEligible: _rewardEligible, 87 | submittedByAdmin: false, 88 | disqualified: false 89 | }); 90 | 91 | teamAddresses.push(_teamAddress); 92 | TeamRegistered(_name, _teamAddress, _memberNames, _rewardEligible); 93 | } 94 | 95 | // Administrator can add new jury members during registration period 96 | function registerJuryMember(string _name, address _ethAddress) onlyOwner { 97 | require(currentPeriod == Period.Registration); 98 | 99 | juryMemberAddresses.push(_ethAddress); 100 | juryMembers[_ethAddress] = JuryMember({ 101 | name: _name, 102 | hasVoted: false 103 | }); 104 | 105 | JuryMemberAdded(_name, _ethAddress); 106 | } 107 | 108 | // Anyone can contribute to the prize pool (i.e. either sponsor himself or administrator on behalf of the sponsor) during registration period 109 | function contributeToPrizePool(string _name, string _siteUrl, string _logoUrl) payable { 110 | require(currentPeriod != Period.End); 111 | require(msg.value >= 0.1 ether); 112 | 113 | sponsors.push(Sponsor({ 114 | name: _name, 115 | siteUrl: _siteUrl, 116 | logoUrl: _logoUrl, 117 | ethAddress: msg.sender, 118 | contribution: msg.value 119 | })); 120 | 121 | totalContribution += msg.value; 122 | SponsorshipReceived(_name, _siteUrl, _logoUrl, msg.value); 123 | } 124 | 125 | // Jury members can vote during voting period 126 | // The _votes parameter should be an array of team addresses, sorted by score from highest to lowest based on jury member's preferences 127 | function vote(address[] _votes) onlyJury { 128 | require(currentPeriod == Period.Voting); 129 | require(_votes.length == teamAddresses.length); 130 | require(juryMembers[msg.sender].hasVoted == false); 131 | 132 | uint _points = _votes.length; 133 | 134 | for(uint i = 0; i < _votes.length; i++) { 135 | address teamAddress = _votes[i]; 136 | 137 | // All submitted teams must be registered 138 | require(bytes(teams[teamAddress].name).length > 0); 139 | 140 | // Judge should not be able to vote for the same team more than once 141 | require(teams[teamAddress].votedForByJuryMember[msg.sender] == false); 142 | 143 | teams[teamAddress].score += _points; 144 | teams[teamAddress].votedForByJuryMember[msg.sender] = true; 145 | 146 | VoteReceived(juryMembers[msg.sender].name, teamAddress, _points); 147 | _points--; 148 | } 149 | 150 | // This will prevent jury members from voting more than once 151 | juryMembers[msg.sender].hasVoted = true; 152 | } 153 | 154 | // Administrator can initiate prize payout during final period 155 | // The _sortedTeams parameter should be an array of correctly sorted teams by score, from highest to lowest 156 | function payoutPrizes(address[] _sortedTeams) onlyOwner { 157 | require(currentPeriod == Period.Verification); 158 | require(_sortedTeams.length == teamAddresses.length); 159 | 160 | for(uint i = 0; i < _sortedTeams.length; i++) { 161 | // All submitted teams must be registered 162 | require(bytes(teams[_sortedTeams[i]].name).length > 0); 163 | 164 | // Teams must be sorted correctly 165 | require(i == _sortedTeams.length - 1 || teams[_sortedTeams[i + 1]].score <= teams[_sortedTeams[i]].score); 166 | 167 | teams[_sortedTeams[i]].submittedByAdmin = true; 168 | } 169 | 170 | // Prizes are paid based on logarithmic scale, where first teams receives 1/2 of the prize pool, second 1/4 and so on 171 | uint prizePoolDivider = 2; 172 | 173 | for(i = 0; i < _sortedTeams.length; i++) { 174 | // Make sure all teams are included in _sortedTeams array 175 | // (i.e. the array should contain unique elements) 176 | require(teams[_sortedTeams[i]].submittedByAdmin); 177 | 178 | uint _prizeAmount = totalContribution / prizePoolDivider; 179 | 180 | if(teams[_sortedTeams[i]].rewardEligible && !teams[_sortedTeams[i]].disqualified) { 181 | _sortedTeams[i].transfer(_prizeAmount); 182 | teams[_sortedTeams[i]].reward = _prizeAmount; 183 | prizePoolDivider *= 2; 184 | PrizePaid(teams[_sortedTeams[i]].name, _prizeAmount); 185 | } 186 | } 187 | 188 | // Some small amount of ETH might remain in the contract after payout, becuase rewards are determened logarithmically 189 | // This amount is returned to contract owner to cover deployment and transaction costs 190 | // In case this amount turns out to be significantly larger than these costs, the administrator will distribute it to all teams equally 191 | administrator.transfer(this.balance); 192 | 193 | currentPeriod = Period.End; 194 | PeriodChanged(currentPeriod); 195 | } 196 | 197 | // Administrator can disqualify team 198 | function disqualifyTeam(address _teamAddress) onlyOwner { 199 | require(bytes(teams[_teamAddress].name).length > 0); 200 | 201 | teams[_teamAddress].disqualified = true; 202 | TeamDisqualified(_teamAddress); 203 | } 204 | 205 | // In case something goes wrong and contract needs to be redeployed, this is a way to return all contributions to the sponsors 206 | function returnContributionsToTheSponsors() onlyOwner { 207 | for(uint i = 0; i < sponsors.length; i++) { 208 | sponsors[i].ethAddress.transfer(sponsors[i].contribution); 209 | } 210 | } 211 | 212 | // Public function that returns user type for the given address 213 | function getUserType(address _address) constant returns (string) { 214 | if(_address == administrator) { 215 | return "administrator"; 216 | } else if(bytes(juryMembers[_address].name).length > 0) { 217 | return "jury"; 218 | } else { 219 | return "other"; 220 | } 221 | } 222 | 223 | // Check if jury member voted 224 | function checkJuryVoted(address _juryAddress) constant returns (bool){ 225 | require(bytes(juryMembers[_juryAddress].name).length != 0); 226 | 227 | return juryMembers[_juryAddress].hasVoted; 228 | } 229 | 230 | // Returns total prize pool size 231 | function getPrizePoolSize() constant returns (uint) { 232 | return totalContribution; 233 | } 234 | 235 | function restartPeriod() onlyOwner { 236 | currentPeriod = Period.Registration; 237 | } 238 | } -------------------------------------------------------------------------------- /webpack/webpack-dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | 5 | const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({ 6 | template: './client/index.html', 7 | filename: 'index.html', 8 | inject: 'body' 9 | }); 10 | 11 | module.exports = { 12 | entry: './client/index.js', 13 | devtool: 'inline-source-map', 14 | output: { 15 | path: path.resolve('dist'), 16 | filename: 'index_bundle.js' 17 | }, 18 | module: { 19 | loaders: [ 20 | { test: /\.js$/, loaders: ['babel-loader', 'eslint-loader'], exclude: /node_modules/ }, 21 | { test: /\.jsx$/, loaders: ['babel-loader', 'eslint-loader'], exclude: /node_modules/ }, 22 | { 23 | test: /\.(jpe?g|png|gif|svg)$/i, 24 | loaders: [ 25 | 'file-loader?hash=sha512&digest=hex&name=[hash].[ext]', 26 | 'image-webpack-loader?bypassOnDebug&optimizationLevel=7&interlaced=false' 27 | ] 28 | }, 29 | { 30 | test: /\.scss$/, 31 | loader: 'style-loader!css-loader?modules&importLoaders=2&sourceMap&localIdentName=[local]!autoprefixer-loader?browsers=last 2 version!sass-loader?outputStyle=expanded&sourceMap' 32 | } 33 | ] 34 | }, 35 | resolve: { 36 | extensions: ['.js', '.jsx'] 37 | }, 38 | plugins: [ 39 | HtmlWebpackPluginConfig, 40 | new webpack.DefinePlugin({ 41 | 'process.env': { 42 | env: '"development"' 43 | } 44 | }) 45 | ] 46 | }; 47 | -------------------------------------------------------------------------------- /webpack/webpack-prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | const CleanPlugin = require('clean-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 7 | 8 | const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({ 9 | template: './client/index.html', 10 | filename: 'index.html', 11 | inject: 'body' 12 | }); 13 | 14 | module.exports = { 15 | entry: './client/index.js', 16 | output: { 17 | path: path.resolve('dist'), 18 | filename: '[name].js', 19 | chunkFilename: '[chunkhash]-[chunkhash].js', 20 | }, 21 | module: { 22 | loaders: [ 23 | { test: /\.js$/, loaders: ['babel-loader', 'eslint-loader'], exclude: /node_modules/ }, 24 | { test: /\.jsx$/, loaders: ['babel-loader', 'eslint-loader'], exclude: /node_modules/ }, 25 | { 26 | test: /\.(jpe?g|png|gif|svg)$/i, 27 | loaders: [ 28 | 'file-loader?hash=sha512&digest=hex&name=[hash].[ext]', 29 | 'image-webpack-loader?bypassOnDebug&optimizationLevel=7&interlaced=false' 30 | ] 31 | }, 32 | { 33 | test: /\.scss$/, 34 | use: ExtractTextPlugin.extract({ 35 | fallback: 'style-loader', 36 | use: ['css-loader?modules&importLoaders=2&localIdentName=[local]&sourceMap&minimize=true', 'sass-loader?outputStyle=expanded&sourceMap=true&sourceMapContents=true', 'autoprefixer-loader?browsers=last 2 version'] 37 | }) 38 | } 39 | ] 40 | }, 41 | resolve: { 42 | extensions: ['.js', '.jsx'] 43 | }, 44 | plugins: [ 45 | new CleanPlugin([path.resolve('dist')], { root: path.resolve(__dirname, '../') }), 46 | new ExtractTextPlugin('[name].css', {allChunks: true}), 47 | HtmlWebpackPluginConfig, 48 | // optimizations 49 | new OptimizeCssAssetsPlugin({ 50 | assetNameRegExp: /\.optimize\.css$/g, 51 | cssProcessor: require('cssnano'), 52 | cssProcessorOptions: { discardComments: {removeAll: true } }, 53 | canPrint: true 54 | }), 55 | new webpack.optimize.OccurrenceOrderPlugin(), 56 | new webpack.optimize.UglifyJsPlugin({ 57 | compress: { 58 | warnings: false, 59 | }, 60 | }), 61 | new webpack.DefinePlugin({ 62 | 'process.env': { 63 | env: '"production"', 64 | NODE_ENV: '"production"', 65 | } 66 | }) 67 | ] 68 | }; 69 | --------------------------------------------------------------------------------