├── .gitignore ├── LICENSE ├── README.md ├── app ├── actions │ ├── AddCharacterActions.js │ ├── CharacterActions.js │ ├── CharacterListActions.js │ ├── FooterActions.js │ ├── HomeActions.js │ ├── NavbarActions.js │ └── StatsActions.js ├── alt.js ├── components │ ├── AddCharacter.js │ ├── App.js │ ├── Character.js │ ├── CharacterList.js │ ├── Footer.js │ ├── Home.js │ ├── Navbar.js │ └── Stats.js ├── main.js ├── routes.js ├── stores │ ├── AddCharacterStore.js │ ├── CharacterListStore.js │ ├── CharacterStore.js │ ├── FooterStore.js │ ├── HomeStore.js │ ├── NavbarStore.js │ └── StatsStore.js └── stylesheets │ └── main.less ├── bower.json ├── config.js ├── gulpfile.js ├── models ├── character.js └── subscriber.js ├── package.json ├── public ├── css │ └── .gitkeep ├── favicon.png ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── img │ ├── amarr_bg.jpg │ ├── caldari_bg.jpg │ ├── gallente_bg.jpg │ └── minmatar_bg.jpg └── js │ └── .gitkeep ├── server.js └── views └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | bower_components 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | 31 | # Project 32 | .idea 33 | *.iml 34 | .DS_Store 35 | 36 | # Compiled files 37 | public/css/* 38 | public/js/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sahat Yalkabov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # New Eden Faces (React) 2 | 3 | [![Donate](https://img.shields.io/badge/paypal-donate-blue.svg)](https://paypal.me/sahat) [![Book session on Codementor](https://cdn.codementor.io/badges/book_session_github.svg)](https://www.codementor.io/sahatyalkabov?utm_source=github&utm_medium=button&utm_term=sahatyalkabov&utm_campaign=github) 4 | 5 | **Source code** for [ 6 | Create a character voting app using React, Node.js, MongoDB and Socket.IO](http://sahatyalkabov.com/create-a-character-voting-app-using-react-nodejs-mongodb-and-socketio/). 7 | 8 | ![](https://lh3.googleusercontent.com/bTN84YkcbO_gXZm4qOrOYVTwUgwkOsrFfv8nrUe7aew=w2080-h1470-no) 9 | -------------------------------------------------------------------------------- /app/actions/AddCharacterActions.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | 3 | class AddCharacterActions { 4 | constructor() { 5 | this.generateActions( 6 | 'addCharacterSuccess', 7 | 'addCharacterFail', 8 | 'updateName', 9 | 'updateGender', 10 | 'invalidName', 11 | 'invalidGender' 12 | ); 13 | } 14 | 15 | addCharacter(name, gender) { 16 | $.ajax({ 17 | type: 'POST', 18 | url: '/api/characters', 19 | data: { name: name, gender: gender } 20 | }) 21 | .done((data) => { 22 | this.actions.addCharacterSuccess(data.message); 23 | }) 24 | .fail((jqXhr) => { 25 | this.actions.addCharacterFail(jqXhr.responseJSON.message); 26 | }); 27 | } 28 | } 29 | 30 | export default alt.createActions(AddCharacterActions); -------------------------------------------------------------------------------- /app/actions/CharacterActions.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | 3 | class CharacterActions { 4 | constructor() { 5 | this.generateActions( 6 | 'reportSuccess', 7 | 'reportFail', 8 | 'getCharacterSuccess', 9 | 'getCharacterFail' 10 | ); 11 | } 12 | 13 | getCharacter(characterId) { 14 | $.ajax({ url: '/api/characters/' + characterId }) 15 | .done((data) => { 16 | this.actions.getCharacterSuccess(data); 17 | }) 18 | .fail((jqXhr) => { 19 | this.actions.getCharacterFail(jqXhr); 20 | }); 21 | } 22 | 23 | report(characterId) { 24 | $.ajax({ 25 | type: 'POST', 26 | url: '/api/report', 27 | data: { characterId: characterId } 28 | }) 29 | .done(() => { 30 | this.actions.reportSuccess(); 31 | }) 32 | .fail((jqXhr) => { 33 | this.actions.reportFail(jqXhr); 34 | }); 35 | } 36 | } 37 | 38 | export default alt.createActions(CharacterActions); -------------------------------------------------------------------------------- /app/actions/CharacterListActions.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | 3 | class CharacterListActions { 4 | constructor() { 5 | this.generateActions( 6 | 'getCharactersSuccess', 7 | 'getCharactersFail' 8 | ); 9 | } 10 | 11 | getCharacters(payload) { 12 | let url = '/api/characters/top'; 13 | let params = { 14 | race: payload.race, 15 | bloodline: payload.bloodline 16 | }; 17 | 18 | if (payload.category === 'female') { 19 | params.gender = 'female'; 20 | } else if (payload.category === 'male') { 21 | params.gender = 'male'; 22 | } 23 | 24 | if (payload.category === 'shame') { 25 | url = '/api/characters/shame'; 26 | } 27 | 28 | $.ajax({ url: url, data: params }) 29 | .done((data) => { 30 | this.actions.getCharactersSuccess(data); 31 | }) 32 | .fail((jqXhr) => { 33 | this.actions.getCharactersFail(jqXhr); 34 | }); 35 | } 36 | } 37 | 38 | export default alt.createActions(CharacterListActions); -------------------------------------------------------------------------------- /app/actions/FooterActions.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | 3 | class FooterActions { 4 | constructor() { 5 | this.generateActions( 6 | 'getTopCharactersSuccess', 7 | 'getTopCharactersFail' 8 | ); 9 | } 10 | 11 | getTopCharacters() { 12 | $.ajax({ url: '/api/characters/top' }) 13 | .done((data) => { 14 | this.actions.getTopCharactersSuccess(data) 15 | }) 16 | .fail((jqXhr) => { 17 | this.actions.getTopCharactersFail(jqXhr) 18 | }); 19 | } 20 | } 21 | 22 | export default alt.createActions(FooterActions); -------------------------------------------------------------------------------- /app/actions/HomeActions.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | 3 | class HomeActions { 4 | constructor() { 5 | this.generateActions( 6 | 'getTwoCharactersSuccess', 7 | 'getTwoCharactersFail', 8 | 'voteFail' 9 | ); 10 | } 11 | 12 | getTwoCharacters() { 13 | $.ajax({ url: '/api/characters' }) 14 | .done(data => { 15 | this.actions.getTwoCharactersSuccess(data); 16 | }) 17 | .fail(jqXhr => { 18 | this.actions.getTwoCharactersFail(jqXhr.responseJSON.message); 19 | }); 20 | } 21 | 22 | vote(winner, loser) { 23 | $.ajax({ 24 | type: 'PUT', 25 | url: '/api/characters' , 26 | data: { winner: winner, loser: loser } 27 | }) 28 | .done(() => { 29 | this.actions.getTwoCharacters(); 30 | }) 31 | .fail((jqXhr) => { 32 | this.actions.voteFail(jqXhr.responseJSON.message); 33 | }); 34 | } 35 | } 36 | 37 | export default alt.createActions(HomeActions); -------------------------------------------------------------------------------- /app/actions/NavbarActions.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import {assign} from 'underscore'; 3 | 4 | class NavbarActions { 5 | constructor() { 6 | this.generateActions( 7 | 'updateOnlineUsers', 8 | 'updateAjaxAnimation', 9 | 'updateSearchQuery', 10 | 'getCharacterCountSuccess', 11 | 'getCharacterCountFail', 12 | 'findCharacterSuccess', 13 | 'findCharacterFail' 14 | ); 15 | } 16 | 17 | findCharacter(payload) { 18 | $.ajax({ 19 | url: '/api/characters/search', 20 | data: { name: payload.searchQuery } 21 | }) 22 | .done((data) => { 23 | assign(payload, data); 24 | this.actions.findCharacterSuccess(payload); 25 | }) 26 | .fail(() => { 27 | this.actions.findCharacterFail(payload); 28 | }); 29 | } 30 | 31 | getCharacterCount() { 32 | $.ajax({ url: '/api/characters/count' }) 33 | .done((data) => { 34 | this.actions.getCharacterCountSuccess(data) 35 | }) 36 | .fail((jqXhr) => { 37 | this.actions.getCharacterCountFail(jqXhr) 38 | }); 39 | } 40 | } 41 | 42 | export default alt.createActions(NavbarActions); -------------------------------------------------------------------------------- /app/actions/StatsActions.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | 3 | class StatsActions { 4 | constructor() { 5 | this.generateActions( 6 | 'getStatsSuccess', 7 | 'getStatsFail' 8 | ); 9 | } 10 | 11 | getStats() { 12 | $.ajax({ url: '/api/stats' }) 13 | .done((data) => { 14 | this.actions.getStatsSuccess(data); 15 | }) 16 | .fail((jqXhr) => { 17 | this.actions.getStatsFail(jqXhr); 18 | }); 19 | } 20 | } 21 | 22 | export default alt.createActions(StatsActions); -------------------------------------------------------------------------------- /app/alt.js: -------------------------------------------------------------------------------- 1 | import Alt from 'alt'; 2 | 3 | export default new Alt(); -------------------------------------------------------------------------------- /app/components/AddCharacter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AddCharacterStore from '../stores/AddCharacterStore'; 3 | import AddCharacterActions from '../actions/AddCharacterActions'; 4 | 5 | class AddCharacter extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = AddCharacterStore.getState(); 9 | this.onChange = this.onChange.bind(this); 10 | } 11 | 12 | componentDidMount() { 13 | AddCharacterStore.listen(this.onChange); 14 | } 15 | 16 | componentWillUnmount() { 17 | AddCharacterStore.unlisten(this.onChange); 18 | } 19 | 20 | onChange(state) { 21 | this.setState(state); 22 | } 23 | 24 | handleSubmit(event) { 25 | event.preventDefault(); 26 | 27 | var name = this.state.name.trim(); 28 | var gender = this.state.gender; 29 | 30 | if (!name) { 31 | AddCharacterActions.invalidName(); 32 | this.refs.nameTextField.focus(); 33 | } 34 | 35 | if (!gender) { 36 | AddCharacterActions.invalidGender(); 37 | } 38 | 39 | if (name && gender) { 40 | AddCharacterActions.addCharacter(name, gender); 41 | } 42 | } 43 | 44 | render() { 45 | return ( 46 |
47 |
48 |
49 |
50 |
Add Character
51 |
52 |
53 |
54 | 55 | 57 | {this.state.helpBlock} 58 |
59 |
60 |
61 | 63 | 64 |
65 |
66 | 68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 | ); 79 | } 80 | } 81 | 82 | export default AddCharacter; -------------------------------------------------------------------------------- /app/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from './Navbar'; 3 | import Footer from './Footer'; 4 | 5 | class App extends React.Component { 6 | render() { 7 | return ( 8 |
9 | 10 | {this.props.children} 11 |
13 | ); 14 | } 15 | } 16 | 17 | export default App; -------------------------------------------------------------------------------- /app/components/Character.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CharacterStore from '../stores/CharacterStore'; 3 | import CharacterActions from '../actions/CharacterActions' 4 | 5 | class Character extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = CharacterStore.getState(); 9 | this.onChange = this.onChange.bind(this); 10 | } 11 | 12 | componentDidMount() { 13 | CharacterStore.listen(this.onChange); 14 | CharacterActions.getCharacter(this.props.params.id); 15 | 16 | $('.magnific-popup').magnificPopup({ 17 | type: 'image', 18 | mainClass: 'mfp-zoom-in', 19 | closeOnContentClick: true, 20 | midClick: true, 21 | zoom: { 22 | enabled: true, 23 | duration: 300 24 | } 25 | }); 26 | } 27 | 28 | componentWillUnmount() { 29 | CharacterStore.unlisten(this.onChange); 30 | $(document.body).removeClass(); 31 | } 32 | 33 | componentDidUpdate(prevProps) { 34 | if (prevProps.params.id !== this.props.params.id) { 35 | CharacterActions.getCharacter(this.props.params.id); 36 | } 37 | } 38 | 39 | onChange(state) { 40 | this.setState(state); 41 | } 42 | 43 | render() { 44 | return ( 45 |
46 |
47 | 48 | 49 | 50 |
51 |
52 |

{this.state.name}

53 |

Race: {this.state.race}

54 |

Bloodline: {this.state.bloodline}

55 |

Gender: {this.state.gender}

56 | 61 |
62 |
63 | 68 |
69 |
70 | ); 71 | } 72 | } 73 | 74 | export default Character; -------------------------------------------------------------------------------- /app/components/CharacterList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import {isEqual} from 'underscore'; 4 | import CharacterListStore from '../stores/CharacterListStore'; 5 | import CharacterListActions from '../actions/CharacterListActions'; 6 | 7 | class CharacterList extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = CharacterListStore.getState(); 11 | this.onChange = this.onChange.bind(this); 12 | } 13 | 14 | componentDidMount() { 15 | CharacterListStore.listen(this.onChange); 16 | CharacterListActions.getCharacters(this.props.params); 17 | } 18 | 19 | componentWillUnmount() { 20 | CharacterListStore.unlisten(this.onChange); 21 | } 22 | 23 | componentDidUpdate(prevProps) { 24 | if (!isEqual(prevProps.params, this.props.params)) { 25 | CharacterListActions.getCharacters(this.props.params); 26 | } 27 | } 28 | 29 | onChange(state) { 30 | this.setState(state); 31 | } 32 | 33 | render() { 34 | let charactersList = this.state.characters.map((character, index) => { 35 | return ( 36 |
37 |
38 | {index + 1} 39 |
40 | 41 | 42 | 43 |
44 |
45 |

46 | {character.name} 47 |

48 | Race: {character.race} 49 |
50 | Bloodline: {character.bloodline} 51 |
52 | Wins: {character.wins} Losses: {character.losses} 53 |
54 |
55 |
56 | ); 57 | }); 58 | 59 | return ( 60 |
61 |
62 | {charactersList} 63 |
64 |
65 | ); 66 | } 67 | } 68 | 69 | export default CharacterList; -------------------------------------------------------------------------------- /app/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import FooterStore from '../stores/FooterStore' 4 | import FooterActions from '../actions/FooterActions'; 5 | 6 | class Footer extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = FooterStore.getState(); 10 | this.onChange = this.onChange.bind(this); 11 | } 12 | 13 | componentDidMount() { 14 | FooterStore.listen(this.onChange); 15 | FooterActions.getTopCharacters(); 16 | } 17 | 18 | componentWillUnmount() { 19 | FooterStore.unlisten(this.onChange); 20 | } 21 | 22 | onChange(state) { 23 | this.setState(state); 24 | } 25 | 26 | render() { 27 | let leaderboardCharacters = this.state.characters.map(function(character) { 28 | return ( 29 |
  • 30 | 31 | 32 | 33 |
  • 34 | ); 35 | }); 36 | 37 | return ( 38 | 56 | ); 57 | } 58 | } 59 | 60 | export default Footer; -------------------------------------------------------------------------------- /app/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import HomeStore from '../stores/HomeStore' 4 | import HomeActions from '../actions/HomeActions'; 5 | import {first, without, findWhere} from 'underscore'; 6 | 7 | class Home extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = HomeStore.getState(); 11 | this.onChange = this.onChange.bind(this); 12 | } 13 | 14 | componentDidMount() { 15 | HomeStore.listen(this.onChange); 16 | HomeActions.getTwoCharacters(); 17 | } 18 | 19 | componentWillUnmount() { 20 | HomeStore.unlisten(this.onChange); 21 | } 22 | 23 | onChange(state) { 24 | this.setState(state); 25 | } 26 | 27 | handleClick(character) { 28 | var winner = character.characterId; 29 | var loser = first(without(this.state.characters, findWhere(this.state.characters, { characterId: winner }))).characterId; 30 | HomeActions.vote(winner, loser); 31 | } 32 | 33 | render() { 34 | var characterNodes = this.state.characters.map((character, index) => { 35 | return ( 36 |
    37 |
    38 | 39 |
    40 |
      41 |
    • Race: {character.race}
    • 42 |
    • Bloodline: {character.bloodline}
    • 43 |
    44 |

    45 | {character.name} 46 |

    47 |
    48 |
    49 |
    50 | ); 51 | }); 52 | 53 | return ( 54 |
    55 |

    Click on the portrait. Select your favorite.

    56 |
    57 | {characterNodes} 58 |
    59 |
    60 | ); 61 | } 62 | } 63 | 64 | export default Home; -------------------------------------------------------------------------------- /app/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import NavbarStore from '../stores/NavbarStore'; 4 | import NavbarActions from '../actions/NavbarActions'; 5 | 6 | class Navbar extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = NavbarStore.getState(); 10 | this.onChange = this.onChange.bind(this); 11 | } 12 | 13 | componentDidMount() { 14 | NavbarStore.listen(this.onChange); 15 | NavbarActions.getCharacterCount(); 16 | 17 | let socket = io.connect(); 18 | 19 | socket.on('onlineUsers', (data) => { 20 | NavbarActions.updateOnlineUsers(data); 21 | }); 22 | 23 | $(document).ajaxStart(() => { 24 | NavbarActions.updateAjaxAnimation('fadeIn'); 25 | }); 26 | 27 | $(document).ajaxComplete(() => { 28 | setTimeout(() => { 29 | NavbarActions.updateAjaxAnimation('fadeOut'); 30 | }, 750); 31 | }); 32 | } 33 | 34 | componentWillUnmount() { 35 | NavbarStore.unlisten(this.onChange); 36 | } 37 | 38 | onChange(state) { 39 | this.setState(state); 40 | } 41 | 42 | handleSubmit(event) { 43 | event.preventDefault(); 44 | 45 | let searchQuery = this.state.searchQuery.trim(); 46 | 47 | if (searchQuery) { 48 | NavbarActions.findCharacter({ 49 | searchQuery: searchQuery, 50 | searchForm: this.refs.searchForm, 51 | history: this.props.history 52 | }); 53 | } 54 | } 55 | 56 | render() { 57 | return ( 58 | 214 | ); 215 | } 216 | } 217 | 218 | export default Navbar; -------------------------------------------------------------------------------- /app/components/Stats.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StatsStore from '../stores/StatsStore' 3 | import StatsActions from '../actions/StatsActions'; 4 | 5 | class Stats extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = StatsStore.getState(); 9 | this.onChange = this.onChange.bind(this); 10 | } 11 | 12 | componentDidMount() { 13 | StatsStore.listen(this.onChange); 14 | StatsActions.getStats(); 15 | } 16 | 17 | componentWillUnmount() { 18 | StatsStore.unlisten(this.onChange); 19 | } 20 | 21 | onChange(state) { 22 | this.setState(state); 23 | } 24 | 25 | render() { 26 | return ( 27 |
    28 |
    29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
    Stats
    Leading race in Top 100{this.state.leadingRace.race} with {this.state.leadingRace.count} characters
    Leading bloodline in Top 100{this.state.leadingBloodline.bloodline} with {this.state.leadingBloodline.count} characters 43 |
    Amarr Characters{this.state.amarrCount}
    Caldari Characters{this.state.caldariCount}
    Gallente Characters{this.state.gallenteCount}
    Minmatar Characters{this.state.minmatarCount}
    Total votes cast{this.state.totalVotes}
    Female characters{this.state.femaleCount}
    Male characters{this.state.maleCount}
    Total number of characters{this.state.totalCount}
    79 |
    80 |
    81 | ); 82 | } 83 | } 84 | 85 | export default Stats; -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router'; 3 | import ReactDOM from 'react-dom'; 4 | import createBrowserHistory from 'history/lib/createBrowserHistory'; 5 | import routes from './routes'; 6 | import Navbar from './components/Navbar'; 7 | 8 | let history = createBrowserHistory(); 9 | 10 | ReactDOM.render({routes}, document.getElementById('app')); -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route} from 'react-router'; 3 | import App from './components/App'; 4 | import Home from './components/Home'; 5 | import Stats from './components/Stats'; 6 | import Character from './components/Character'; 7 | import CharacterList from './components/CharacterList'; 8 | import AddCharacter from './components/AddCharacter'; 9 | 10 | export default ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /app/stores/AddCharacterStore.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import AddCharacterActions from '../actions/AddCharacterActions'; 3 | 4 | class AddCharacterStore { 5 | constructor() { 6 | this.bindActions(AddCharacterActions); 7 | this.name = ''; 8 | this.gender = ''; 9 | this.helpBlock = ''; 10 | this.nameValidationState = ''; 11 | this.genderValidationState = ''; 12 | } 13 | 14 | onAddCharacterSuccess(successMessage) { 15 | this.nameValidationState = 'has-success'; 16 | this.helpBlock = successMessage; 17 | } 18 | 19 | onAddCharacterFail(errorMessage) { 20 | this.nameValidationState = 'has-error'; 21 | this.helpBlock = errorMessage; 22 | } 23 | 24 | onUpdateName(event) { 25 | this.name = event.target.value; 26 | this.nameValidationState = ''; 27 | this.helpBlock = ''; 28 | } 29 | 30 | onUpdateGender(event) { 31 | this.gender = event.target.value; 32 | this.genderValidationState = ''; 33 | } 34 | 35 | onInvalidName() { 36 | this.nameValidationState = 'has-error'; 37 | this.helpBlock = 'Please enter a character name.'; 38 | } 39 | 40 | onInvalidGender() { 41 | this.genderValidationState = 'has-error'; 42 | } 43 | } 44 | 45 | export default alt.createStore(AddCharacterStore); -------------------------------------------------------------------------------- /app/stores/CharacterListStore.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import CharacterListActions from '../actions/CharacterListActions'; 3 | 4 | class CharacterListStore { 5 | constructor() { 6 | this.bindActions(CharacterListActions); 7 | this.characters = []; 8 | } 9 | 10 | onGetCharactersSuccess(data) { 11 | this.characters = data; 12 | } 13 | 14 | onGetCharactersFail(jqXhr) { 15 | toastr.error(jqXhr.responseJSON.message); 16 | } 17 | } 18 | 19 | export default alt.createStore(CharacterListStore); -------------------------------------------------------------------------------- /app/stores/CharacterStore.js: -------------------------------------------------------------------------------- 1 | import {assign, contains} from 'underscore'; 2 | import alt from '../alt'; 3 | import CharacterActions from '../actions/CharacterActions'; 4 | 5 | class CharacterStore { 6 | constructor() { 7 | this.bindActions(CharacterActions); 8 | this.characterId = 0; 9 | this.name = 'TBD'; 10 | this.race = 'TBD'; 11 | this.bloodline = 'TBD'; 12 | this.gender = 'TBD'; 13 | this.wins = 0; 14 | this.losses = 0; 15 | this.winLossRatio = 0; 16 | this.isReported = false; 17 | } 18 | 19 | onGetCharacterSuccess(data) { 20 | assign(this, data); 21 | $(document.body).attr('class', 'profile ' + this.race.toLowerCase()); 22 | let localData = localStorage.getItem('NEF') ? JSON.parse(localStorage.getItem('NEF')) : {}; 23 | let reports = localData.reports || []; 24 | this.isReported = contains(reports, this.characterId); 25 | this.winLossRatio = ((this.wins / (this.wins + this.losses) * 100) || 0).toFixed(1); 26 | } 27 | 28 | onGetCharacterFail(jqXhr) { 29 | toastr.error(jqXhr.responseJSON.message); 30 | } 31 | 32 | onReportSuccess() { 33 | this.isReported = true; 34 | let localData = localStorage.getItem('NEF') ? JSON.parse(localStorage.getItem('NEF')) : {}; 35 | localData.reports = localData.reports || []; 36 | localData.reports.push(this.characterId); 37 | localStorage.setItem('NEF', JSON.stringify(localData)); 38 | toastr.warning('Character has been reported.'); 39 | } 40 | 41 | onReportFail(jqXhr) { 42 | toastr.error(jqXhr.responseJSON.message); 43 | } 44 | } 45 | 46 | export default alt.createStore(CharacterStore); -------------------------------------------------------------------------------- /app/stores/FooterStore.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import FooterActions from '../actions/FooterActions'; 3 | 4 | class FooterStore { 5 | constructor() { 6 | this.bindActions(FooterActions); 7 | this.characters = []; 8 | } 9 | 10 | onGetTopCharactersSuccess(data) { 11 | this.characters = data.slice(0, 5); 12 | } 13 | 14 | onGetTopCharactersFail(jqXhr) { 15 | toastr.error(jqXhr.responseJSON && jqXhr.responseJSON.message || jqXhr.responseText || jqXhr.statusText); 16 | } 17 | } 18 | 19 | export default alt.createStore(FooterStore); -------------------------------------------------------------------------------- /app/stores/HomeStore.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import HomeActions from '../actions/HomeActions'; 3 | 4 | class HomeStore { 5 | constructor() { 6 | this.bindActions(HomeActions); 7 | this.characters = []; 8 | } 9 | 10 | onGetTwoCharactersSuccess(data) { 11 | this.characters = data; 12 | } 13 | 14 | onGetTwoCharactersFail(errorMessage) { 15 | toastr.error(errorMessage); 16 | } 17 | 18 | onVoteFail(errorMessage) { 19 | toastr.error(errorMessage); 20 | } 21 | } 22 | 23 | export default alt.createStore(HomeStore); -------------------------------------------------------------------------------- /app/stores/NavbarStore.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import NavbarActions from '../actions/NavbarActions'; 3 | 4 | class NavbarStore { 5 | constructor() { 6 | this.bindActions(NavbarActions); 7 | this.totalCharacters = 0; 8 | this.onlineUsers = 0; 9 | this.searchQuery = ''; 10 | this.ajaxAnimationClass = ''; 11 | } 12 | 13 | onFindCharacterSuccess(payload) { 14 | payload.history.pushState(null, '/characters/' + payload.characterId); 15 | } 16 | 17 | onFindCharacterFail(payload) { 18 | payload.searchForm.classList.add('shake'); 19 | setTimeout(() => { 20 | payload.searchForm.classList.remove('shake'); 21 | }, 1000); 22 | } 23 | 24 | onUpdateOnlineUsers(data) { 25 | this.onlineUsers = data.onlineUsers; 26 | } 27 | 28 | onUpdateAjaxAnimation(className) { 29 | this.ajaxAnimationClass = className; //fadein or fadeout 30 | } 31 | 32 | onUpdateSearchQuery(event) { 33 | this.searchQuery = event.target.value; 34 | } 35 | 36 | onGetCharacterCountSuccess(data) { 37 | this.totalCharacters = data.count; 38 | } 39 | 40 | onGetCharacterCountFail(jqXhr) { 41 | toastr.error(jqXhr.responseJSON.message); 42 | } 43 | } 44 | 45 | export default alt.createStore(NavbarStore); -------------------------------------------------------------------------------- /app/stores/StatsStore.js: -------------------------------------------------------------------------------- 1 | import {assign} from 'underscore'; 2 | import alt from '../alt'; 3 | import StatsActions from '../actions/StatsActions'; 4 | 5 | class StatsStore { 6 | constructor() { 7 | this.bindActions(StatsActions); 8 | this.leadingRace = { race: 'Unknown', count: 0 }; 9 | this.leadingBloodline = { bloodline: 'Unknown', count: 0 }; 10 | this.amarrCount = 0; 11 | this.caldariCount = 0; 12 | this.gallenteCount = 0; 13 | this.minmatarCount = 0; 14 | this.totalVotes = 0; 15 | this.femaleCount = 0; 16 | this.maleCount = 0; 17 | this.totalCount = 0; 18 | } 19 | 20 | onGetStatsSuccess(data) { 21 | assign(this, data); 22 | } 23 | 24 | onGetStatsFail(jqXhr) { 25 | toastr.error(jqXhr.responseJSON.message); 26 | } 27 | } 28 | 29 | export default alt.createStore(StatsStore); -------------------------------------------------------------------------------- /app/stylesheets/main.less: -------------------------------------------------------------------------------- 1 | @import '../../bower_components/bootstrap/less/bootstrap'; 2 | @import '../../bower_components/toastr/toastr'; 3 | @import (less) '../../bower_components/magnific-popup/dist/magnific-popup.css'; 4 | 5 | /* 6 | * Bootstrap overrides. 7 | */ 8 | @body-bg: #f0f3f4; 9 | @text-color: #58666f; 10 | @font-family-base: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; 11 | @link-color: #363f34; 12 | @link-hover-color: #141718; 13 | 14 | @navbar-default-bg: #fff; 15 | @navbar-default-link-color: #353f44; 16 | @navbar-default-link-hover-bg: #f6f7f7; 17 | @navbar-default-link-active-bg: #f6f7f7; 18 | 19 | @btn-default-color: #58666e; 20 | @btn-default-border: #dee5e7; 21 | 22 | @btn-primary-color: #fff; 23 | @btn-primary-bg: #7266bb; 24 | @btn-primary-border: #7266bb; 25 | 26 | @btn-success-color: #fff; 27 | @btn-success-bg: #27d24b; 28 | @btn-success-border: #27d24b; 29 | 30 | @btn-info-color: #fff; 31 | @btn-info-bg: #23b7f5; 32 | @btn-info-border: #23b7f5; 33 | 34 | @btn-warning-color: #fff; 35 | @btn-warning-bg: #fac732; 36 | @btn-warning-border: #fac732; 37 | 38 | @btn-danger-color: #fff; 39 | @btn-danger-bg: #f15051; 40 | @btn-danger-border: #f15051; 41 | 42 | @panel-default-border: #dee5e7; 43 | @panel-border-radius: 2px; 44 | @panel-default-heading-bg: #f6f8f8; 45 | 46 | @screen-sm: 800px; 47 | 48 | html { 49 | position: relative; 50 | min-height: 100%; 51 | } 52 | 53 | body { 54 | margin-bottom: 220px; 55 | -webkit-font-smoothing: antialiased; 56 | } 57 | 58 | footer { 59 | position: absolute; 60 | bottom: 0; 61 | width: 100%; 62 | color: #fff; 63 | background-color: #3b3f51; 64 | } 65 | 66 | footer a { 67 | color: #fff; 68 | text-decoration: underline; 69 | 70 | &:hover, 71 | &:focus { 72 | color: #fff; 73 | text-decoration: none; 74 | } 75 | } 76 | 77 | footer .thumb-md { 78 | border: 1px solid rgba(255,255,255,.15); 79 | transition: border .1s linear; 80 | 81 | &:hover { 82 | border-color: rgba(255,255,255,.3); 83 | } 84 | } 85 | 86 | .badge-danger { 87 | color: #fff; 88 | background-color: #f05150; 89 | } 90 | 91 | .badge-up { 92 | position: relative; 93 | top: -10px; 94 | padding: 2px 5px; 95 | } 96 | 97 | //.navbar-form .input-group-btn .btn-default { 98 | // padding: 9px 12px; 99 | //} 100 | 101 | .input-group-btn:last-child > .btn, 102 | .input-group-btn:last-child > .btn-group { 103 | margin-left: 0; 104 | border-left: 0; 105 | } 106 | 107 | /* 108 | * Character profile page styles. 109 | */ 110 | 111 | .profile { 112 | color: #fff; 113 | background-size: cover; 114 | } 115 | 116 | .profile.amarr { 117 | background-image: url('/img/amarr_bg.jpg'); 118 | } 119 | 120 | .profile.caldari { 121 | background-image: url('/img/caldari_bg.jpg'); 122 | } 123 | 124 | .profile.gallente { 125 | background-image: url('/img/gallente_bg.jpg'); 126 | } 127 | 128 | .profile.minmatar { 129 | background-image: url('/img/minmatar_bg.jpg'); 130 | } 131 | 132 | .profile footer { 133 | color: #fff; 134 | background: transparent; 135 | border-top: 1px solid rgba(255,255,255,.15); 136 | } 137 | 138 | .profile .navbar-default { 139 | background-color: transparent; 140 | border-bottom: 1px solid rgba(255,255,255,.15); 141 | box-shadow: none; 142 | } 143 | 144 | .profile .navbar-default .navbar-brand { 145 | color: #fff; 146 | } 147 | 148 | 149 | .profile .form-control { 150 | color: #fff; 151 | background: rgba(255,255,255,.15); 152 | border-color: rgba(255,255,255,.15); 153 | border-right: 0; 154 | 155 | &:focus { 156 | border-color: rgba(255,255,255,.3); 157 | box-shadow: inset 0 1px 1px rgba(0,0,0,.055),0 0 8px rgba(255,255,255,.3); 158 | } 159 | } 160 | 161 | .profile .input-group-btn:last-child > .btn { 162 | margin-left: 0; 163 | } 164 | 165 | .profile .btn-default { 166 | color: #fff; 167 | background-color: rgba(255,255,255,.15); 168 | border-color: rgba(255,255,255,.15); 169 | transition: background-color .3s; 170 | 171 | &:focus, 172 | &:hover { 173 | color: #fff; 174 | background-color: rgba(255,255,255,.3); 175 | } 176 | } 177 | 178 | 179 | .profile .tri { 180 | border-top-color: #fff; 181 | 182 | &.invert { 183 | border-bottom-color: #fff; 184 | } 185 | } 186 | 187 | .profile .navbar-default .navbar-nav > .open > a, 188 | .profile .navbar-default .navbar-nav > .open > a:hover, 189 | .profile .navbar-default .navbar-nav > .open > a:focus { 190 | background-color: rgba(255,255,255,.15); 191 | } 192 | 193 | .profile .navbar-default .navbar-nav > li > a:hover, 194 | .profile .navbar-default .navbar-nav > li > a:focus { 195 | color: #fff; 196 | background-color: rgba(255,255,255,.15); 197 | } 198 | 199 | .profile .navbar-default .navbar-nav > li > a { 200 | color: #fff; 201 | } 202 | 203 | .profile footer .col-sm-5 { 204 | border-right: 1px solid rgba(255,255,255,.15); 205 | } 206 | 207 | 208 | .table { 209 | font-size: inherit; 210 | } 211 | 212 | .table > thead > tr > th { 213 | padding: 8px 15px; 214 | border-bottom: 1px solid #eaeef0; 215 | } 216 | 217 | .table > tbody > tr > td { 218 | padding: 8px 15px; 219 | border-top: 1px solid #eaeef0; 220 | } 221 | 222 | .table-striped > tbody > tr:nth-of-type(odd) { 223 | background-color: #f9f9f9; 224 | } 225 | 226 | .list-group { 227 | border-radius: 2px; 228 | } 229 | 230 | .list-group .list-group-item { 231 | margin-bottom: 5px; 232 | border-radius: 3px; 233 | } 234 | 235 | .list-group-item:hover, 236 | .list-group-item:focus { 237 | background-color: #f6f8f8; 238 | } 239 | 240 | .thumb-md { 241 | display: inline-block; 242 | width: 64px; 243 | } 244 | 245 | .thumb-lg { 246 | display: inline-block; 247 | width: 96px; 248 | margin-right: 15px; 249 | } 250 | 251 | .thumb-lg img { 252 | height: auto; 253 | max-width: 100%; 254 | vertical-align: middle; 255 | } 256 | 257 | .position { 258 | font-size: 40px; 259 | font-weight: bold; 260 | color: #ddd; 261 | margin-right: 5px; 262 | line-height: 96px; 263 | } 264 | 265 | .btn { 266 | font-weight: 500; 267 | border-radius: 2px; 268 | outline: 0 !important; 269 | } 270 | 271 | .btn-addon i { 272 | position: relative; 273 | float: left; 274 | width: 34px; 275 | height: 34px; 276 | margin: -6px -12px; 277 | margin-right: 12px; 278 | line-height: 34px; 279 | text-align: center; 280 | background-color: rgba(0,0,0,.1); 281 | border-radius: 2px 0 0 2px; 282 | } 283 | 284 | .btn-addon i.pull-right { 285 | margin-right: -12px; 286 | margin-left: 12px; 287 | border-radius: 0 2px 2px 0; 288 | } 289 | 290 | .btn-addon.btn-sm i.pull-right { 291 | margin-right: -10px; 292 | margin-left: 10px; 293 | } 294 | 295 | .btn-default { 296 | box-shadow: 0 1px 1px rgba(91,91,91,.1); 297 | } 298 | 299 | .navbar { 300 | border: 0; 301 | box-shadow: 0 2px 2px rgba(0,0,0,.05), 0 1px 0 rgba(0,0,0,.05); 302 | } 303 | 304 | .navbar-default .navbar-brand { 305 | margin-left: 40px; 306 | font-size: 20px; 307 | font-weight: 700; 308 | } 309 | 310 | .dropdown-menu { 311 | box-shadow: 0 2px 6px rgba(0,0,0,.1); 312 | } 313 | 314 | .dropdown-submenu { 315 | position: relative; 316 | } 317 | 318 | .dropdown-submenu > .dropdown-menu { 319 | top: 0; 320 | left: 100%; 321 | margin-top: 0; 322 | margin-left: 1px; 323 | border-radius: 0 6px 6px 6px; 324 | } 325 | 326 | .dropdown-submenu:hover > .dropdown-menu { 327 | display: block; 328 | } 329 | 330 | .dropdown-submenu > a:after { 331 | display: block; 332 | content: ''; 333 | float: right; 334 | width: 0; 335 | height: 0; 336 | border: 5px solid transparent; 337 | border-right-width: 0; 338 | border-left-color: #353f44; 339 | margin-top: 5px; 340 | margin-right: -10px; 341 | } 342 | 343 | .panel { 344 | box-shadow: 0 1px 1px rgba(0,0,0,.05); 345 | } 346 | 347 | .panel-default > .panel-heading { 348 | color: #333; 349 | font-weight: 700; 350 | border-color: #edf2f2; 351 | } 352 | 353 | .dropdown-submenu.pull-left { 354 | float: none; 355 | } 356 | 357 | .dropdown-submenu.pull-left > .dropdown-menu { 358 | left: -100%; 359 | margin-left: 10px; 360 | border-radius: 6px 0 6px 6px; 361 | } 362 | 363 | .form-control { 364 | border-color: #cfdadc; 365 | border-radius: 2px; 366 | 367 | &:focus { 368 | border-color: #24b7e4; 369 | box-shadow: none; 370 | } 371 | } 372 | 373 | .thumbnail { 374 | background-color: #fff; 375 | padding: 0; 376 | border-radius: 2px; 377 | border-color: #dee5e7; 378 | box-shadow: 0 1px 1px rgba(0,0,0,.05); 379 | } 380 | 381 | .thumbnail img { 382 | padding: 6px; 383 | border-radius: 2px 2px 0 0; 384 | border: 0; 385 | background-color: #fff; 386 | cursor: pointer; 387 | transition: background-color 0.2s; 388 | 389 | &:hover { 390 | background-color: @btn-info-bg; 391 | } 392 | &:active { 393 | position: relative; 394 | top: 2px; 395 | } 396 | } 397 | 398 | .form-control { 399 | box-shadow: none; 400 | } 401 | 402 | label { 403 | font-weight: normal; 404 | } 405 | 406 | 407 | .profile-img { 408 | position: relative; 409 | margin-bottom: 20px; 410 | float: left; 411 | width: 256px; 412 | height: 256px; 413 | box-shadow: 0 2px 25px rgba(0,0,0,.25); 414 | } 415 | 416 | .profile-info { 417 | margin: 0 0 20px 286px; 418 | max-width: 405px; 419 | color: #fff; 420 | } 421 | 422 | .btn-transparent { 423 | border: 1px solid white; 424 | border-radius: 3px; 425 | padding: 10px 20px; 426 | color: #fff; 427 | text-transform: uppercase; 428 | cursor: pointer; 429 | background-color: transparent; 430 | box-shadow: none; 431 | transition: background-color .3s; 432 | 433 | &:focus, 434 | &:hover { 435 | color: #fff; 436 | background-color: rgba(255,255,255,.3); 437 | } 438 | } 439 | 440 | .profile-stats { 441 | margin: 30px 0 30px 0; 442 | padding: 20px 0; 443 | border-top: 1px solid #2098ca; 444 | border-bottom: 1px solid #2098ca; 445 | border-top: 1px solid rgba(255,255,255,.15); 446 | border-bottom: 1px solid rgba(255,255,255,.15) 447 | } 448 | 449 | 450 | @media (max-width: 510px) { 451 | .profile-stats { 452 | font-size: 12px 453 | } 454 | } 455 | 456 | @media (max-width: 360px) { 457 | .profile-stats { 458 | padding: 10px 0 459 | } 460 | } 461 | 462 | .profile-stats { 463 | display: block; 464 | color: #fff 465 | } 466 | 467 | .profile-stats ul { 468 | list-style-type: none 469 | } 470 | 471 | .profile-stats li { 472 | position: relative; 473 | float: left; 474 | width: 33.3%; 475 | font-size: 16px; 476 | line-height: 19px; 477 | text-align: center; 478 | overflow: hidden 479 | } 480 | 481 | @media (max-width: 360px) { 482 | .profile-stats li { 483 | font-size: 10px 484 | } 485 | } 486 | 487 | .profile-stats li .stats-number { 488 | display: block; 489 | margin-bottom: 15px; 490 | font-size: 40px; 491 | font-weight: 600; 492 | line-height: 40px 493 | } 494 | 495 | @media (max-width: 360px) { 496 | .profile-stats li .stats-number { 497 | margin-bottom: 5px; 498 | font-size: 34px 499 | } 500 | } 501 | 502 | .profile-stats li:first-child:after { 503 | content: ''; 504 | position: absolute; 505 | display: block; 506 | top: 50%; 507 | right: 0; 508 | margin-top: -27px; 509 | width: 1px; 510 | height: 55px; 511 | background: rgba(255,255,255,.15) 512 | } 513 | 514 | .profile-stats li:last-child:before { 515 | content: ''; 516 | position: absolute; 517 | display: block; 518 | top: 50%; 519 | left: 0; 520 | margin-top: -27px; 521 | width: 1px; 522 | height: 55px; 523 | background: rgba(255,255,255,.15) 524 | } 525 | 526 | .profile-stats li.last-child:before { 527 | background: #fff 528 | } 529 | 530 | .radio { 531 | margin-bottom: 10px; 532 | margin-top: 10px; 533 | padding-left: 20px; 534 | } 535 | 536 | .radio-inline + .radio-inline { 537 | margin-top: 10px; 538 | } 539 | 540 | .radio-inline, 541 | .checkbox-inline { 542 | cursor: default; 543 | } 544 | 545 | .radio label { 546 | display: inline-block; 547 | cursor: pointer; 548 | position: relative; 549 | padding-left: 5px; 550 | margin-right: 10px; 551 | } 552 | 553 | .radio label:before { 554 | content: ''; 555 | display: inline-block; 556 | width: 17px; 557 | height: 17px; 558 | margin-left: -20px; 559 | position: absolute; 560 | left: 0; 561 | background-color: #fff; 562 | border: 1px solid #d0d0d0; 563 | } 564 | 565 | .radio label:before { 566 | bottom: 2.5px; 567 | border-radius: 100px; 568 | transition: border .2s 0s cubic-bezier(.45,.04,.22,1.30); 569 | } 570 | 571 | .radio input[type=radio]:checked + label:before { 572 | border-width: 5px; 573 | } 574 | 575 | .radio input[type=radio] { 576 | display: none; 577 | } 578 | 579 | .radio input[type=radio][disabled] + label { 580 | opacity: .65; 581 | } 582 | 583 | .radio input[type=radio]:checked + label:before { 584 | border-color: #10cebd; 585 | } 586 | 587 | .animated { 588 | animation-fill-mode: both; 589 | animation-duration: 1s; 590 | } 591 | 592 | @keyframes fadeInUp { 593 | 0% { 594 | opacity: 0; 595 | transform: translateY(20px); 596 | } 597 | 598 | 100% { 599 | opacity: 1; 600 | transform: translateY(0); 601 | } 602 | } 603 | 604 | .fadeInUp { 605 | animation-name: fadeInUp; 606 | } 607 | 608 | @keyframes fadeIn { 609 | 0% { 610 | opacity: 0; 611 | } 612 | 100% { 613 | opacity: 1; 614 | } 615 | } 616 | 617 | .fadeIn { 618 | animation-name: fadeIn; 619 | } 620 | 621 | @keyframes fadeOut { 622 | 0% { 623 | opacity: 1; 624 | } 625 | 100% { 626 | opacity: 0; 627 | } 628 | } 629 | 630 | .fadeOut { 631 | animation-name: fadeOut; 632 | } 633 | 634 | @keyframes flipInX { 635 | 0% { 636 | transform: perspective(800px) rotate3d(1,0,0,90deg); 637 | transition-timing-function: ease-in; 638 | opacity: 0; 639 | } 640 | 641 | 40% { 642 | transform: perspective(800px) rotate3d(1,0,0,-20deg); 643 | transition-timing-function: ease-in; 644 | } 645 | 646 | 60% { 647 | transform: perspective(800px) rotate3d(1,0,0,10deg); 648 | opacity: 1; 649 | } 650 | 651 | 80% { 652 | transform: perspective(800px) rotate3d(1,0,0,-5deg); 653 | } 654 | 655 | 100% { 656 | transform: perspective(800px); 657 | } 658 | } 659 | 660 | .flipInX { 661 | backface-visibility: visible !important; 662 | animation-name: flipInX; 663 | } 664 | 665 | @keyframes flipOutX { 666 | 0% { 667 | transform: perspective(400px); 668 | } 669 | 670 | 30% { 671 | transform: perspective(400px) rotate3d(1,0,0,-20deg); 672 | opacity: 1; 673 | } 674 | 675 | 100% { 676 | transform: perspective(400px) rotate3d(1,0,0,90deg); 677 | opacity: 0; 678 | } 679 | } 680 | 681 | .flipOutX { 682 | animation-name: flipOutX; 683 | backface-visibility: visible !important; 684 | } 685 | 686 | @keyframes pulse { 687 | 0% { 688 | opacity: 1; 689 | } 690 | 16.666% { 691 | opacity: 1; 692 | } 693 | 100% { 694 | opacity: 0; 695 | } 696 | } 697 | 698 | @keyframes shake { 699 | 0%, 100% { 700 | transform: translate3d(0, 0, 0); 701 | } 702 | 703 | 10%, 30%, 50%, 70%, 90% { 704 | transform: translate3d(-10px, 0, 0); 705 | } 706 | 707 | 20%, 40%, 60%, 80% { 708 | transform: translate3d(10px, 0, 0); 709 | } 710 | } 711 | 712 | .shake { 713 | animation-name: shake; 714 | } 715 | 716 | @tricolor: @link-color; 717 | @triw: 10px; 718 | @trih: @triw*0.9; 719 | 720 | .triangles { 721 | position: absolute; 722 | top: 25px; 723 | left: 30px; 724 | height: @trih * 3; 725 | width: @triw * 3; 726 | transform: translate(-50%, -50%); 727 | opacity: 0; 728 | } 729 | 730 | .navbar-brand:hover .tri { 731 | animation-play-state: paused; 732 | } 733 | 734 | .tri { 735 | position: absolute; 736 | animation: pulse 750ms ease-in infinite; 737 | border-top: @trih solid @tricolor; 738 | border-left: @triw/2 solid transparent; 739 | border-right: @triw/2 solid transparent; 740 | border-bottom: 0; 741 | 742 | &.invert { 743 | border-top: 0; 744 | border-bottom: @trih solid @tricolor; 745 | border-left: @triw/2 solid transparent; 746 | border-right: @triw/2 solid transparent; 747 | } 748 | &:nth-child(1) { 749 | left: @triw; 750 | } 751 | &:nth-child(2) { 752 | left: @triw/2; 753 | top: @trih; 754 | animation-delay: -125ms; 755 | } 756 | &:nth-child(3) { 757 | left: @triw; 758 | top: @trih; 759 | } 760 | &:nth-child(4) { 761 | left: @triw*1.5; 762 | top: @trih; 763 | animation-delay: -625ms; 764 | } 765 | &:nth-child(5) { 766 | top: @trih*2; 767 | animation-delay: -250ms; 768 | } 769 | &:nth-child(6) { 770 | top: @trih*2; 771 | left: @triw/2; 772 | animation-delay: -250ms; 773 | } 774 | &:nth-child(7) { 775 | top: @trih*2; 776 | left: @triw; 777 | animation-delay: -375ms; 778 | } 779 | &:nth-child(8) { 780 | top: @trih*2; 781 | left: @triw*1.5; 782 | animation-delay: -500ms; 783 | } 784 | &:nth-child(9) { 785 | top: @trih*2; 786 | left: @triw*2; 787 | animation-delay: -500ms; 788 | } 789 | } 790 | 791 | 792 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "newedenfaces", 3 | "dependencies": { 4 | "jquery": "^2.1.4", 5 | "bootstrap": "^3.3.5", 6 | "magnific-popup": "^1.0.0", 7 | "toastr": "^2.1.1" 8 | } 9 | } -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | database: process.env.MONGO_URI || 'localhost' 3 | }; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gutil = require('gulp-util'); 3 | var gulpif = require('gulp-if'); 4 | var autoprefixer = require('gulp-autoprefixer'); 5 | var cssmin = require('gulp-cssmin'); 6 | var less = require('gulp-less'); 7 | var concat = require('gulp-concat'); 8 | var plumber = require('gulp-plumber'); 9 | var buffer = require('vinyl-buffer'); 10 | var source = require('vinyl-source-stream'); 11 | var babelify = require('babelify'); 12 | var browserify = require('browserify'); 13 | var watchify = require('watchify'); 14 | var uglify = require('gulp-uglify'); 15 | var sourcemaps = require('gulp-sourcemaps'); 16 | 17 | var production = process.env.NODE_ENV === 'production'; 18 | 19 | var dependencies = [ 20 | 'alt', 21 | 'react', 22 | 'react-dom', 23 | 'react-router', 24 | 'underscore' 25 | ]; 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Combine all JS libraries into a single file for fewer HTTP requests. 30 | |-------------------------------------------------------------------------- 31 | */ 32 | gulp.task('vendor', function() { 33 | return gulp.src([ 34 | 'bower_components/jquery/dist/jquery.js', 35 | 'bower_components/bootstrap/dist/js/bootstrap.js', 36 | 'bower_components/magnific-popup/dist/jquery.magnific-popup.js', 37 | 'bower_components/toastr/toastr.js' 38 | ]).pipe(concat('vendor.js')) 39 | .pipe(gulpif(production, uglify({ mangle: false }))) 40 | .pipe(gulp.dest('public/js')); 41 | }); 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Compile third-party dependencies separately for faster performance. 46 | |-------------------------------------------------------------------------- 47 | */ 48 | gulp.task('browserify-vendor', function() { 49 | return browserify() 50 | .require(dependencies) 51 | .bundle() 52 | .pipe(source('vendor.bundle.js')) 53 | .pipe(buffer()) 54 | .pipe(gulpif(production, uglify({ mangle: false }))) 55 | .pipe(gulp.dest('public/js')); 56 | }); 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Compile only project files, excluding all third-party dependencies. 61 | |-------------------------------------------------------------------------- 62 | */ 63 | gulp.task('browserify', ['browserify-vendor'], function() { 64 | return browserify({ entries: 'app/main.js', debug: true }) 65 | .external(dependencies) 66 | .transform(babelify, { presets: ['es2015', 'react'] }) 67 | .bundle() 68 | .pipe(source('bundle.js')) 69 | .pipe(buffer()) 70 | .pipe(sourcemaps.init({ loadMaps: true })) 71 | .pipe(gulpif(production, uglify({ mangle: false }))) 72 | .pipe(sourcemaps.write('.')) 73 | .pipe(gulp.dest('public/js')); 74 | }); 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Same as browserify task, but will also watch for changes and re-compile. 79 | |-------------------------------------------------------------------------- 80 | */ 81 | gulp.task('browserify-watch', ['browserify-vendor'], function() { 82 | var bundler = watchify(browserify({ entries: 'app/main.js', debug: true }, watchify.args)); 83 | bundler.external(dependencies); 84 | bundler.transform(babelify, { presets: ['es2015', 'react'] }); 85 | bundler.on('update', rebundle); 86 | return rebundle(); 87 | 88 | function rebundle() { 89 | var start = Date.now(); 90 | return bundler.bundle() 91 | .on('error', function(err) { 92 | gutil.log(gutil.colors.red(err.toString())); 93 | }) 94 | .on('end', function() { 95 | gutil.log(gutil.colors.green('Finished rebundling in', (Date.now() - start) + 'ms.')); 96 | }) 97 | .pipe(source('bundle.js')) 98 | .pipe(buffer()) 99 | .pipe(sourcemaps.init({ loadMaps: true })) 100 | .pipe(sourcemaps.write('.')) 101 | .pipe(gulp.dest('public/js/')); 102 | } 103 | }); 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Compile LESS stylesheets. 108 | |-------------------------------------------------------------------------- 109 | */ 110 | gulp.task('styles', function() { 111 | return gulp.src('app/stylesheets/main.less') 112 | .pipe(plumber()) 113 | .pipe(less()) 114 | .pipe(autoprefixer()) 115 | .pipe(gulpif(production, cssmin())) 116 | .pipe(gulp.dest('public/css')); 117 | }); 118 | 119 | gulp.task('watch', function() { 120 | gulp.watch('app/stylesheets/**/*.less', ['styles']); 121 | }); 122 | 123 | gulp.task('default', ['styles', 'vendor', 'browserify-watch', 'watch']); 124 | gulp.task('build', ['styles', 'vendor', 'browserify']); 125 | -------------------------------------------------------------------------------- /models/character.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var characterSchema = new mongoose.Schema({ 4 | characterId: { type: String, unique: true, index: true }, 5 | name: String, 6 | race: String, 7 | gender: String, 8 | bloodline: String, 9 | wins: { type: Number, default: 0 }, 10 | losses: { type: Number, default: 0 }, 11 | reports: { type: Number, default: 0 }, 12 | random: { type: [Number], index: '2d' }, 13 | voted: { type: Boolean, default: false } 14 | }); 15 | 16 | module.exports = mongoose.model('Character', characterSchema); -------------------------------------------------------------------------------- /models/subscriber.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var subscriberSchema = new mongoose.Schema({ 4 | email: { type: String, unique: true, lowercase: true }, 5 | characters: [{ type: String, ref: 'Character' }] 6 | }); 7 | 8 | module.exports = mongoose.model('Subscriber', subscriberSchema); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "newedenfaces", 3 | "description": "Character voting app for EVE Online", 4 | "version": "1.0.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/sahat/newedenfaces-react" 8 | }, 9 | "main": "server.js", 10 | "scripts": { 11 | "start": "node server.js", 12 | "watch": "nodemon server.js", 13 | "postinstall": "bower install && gulp build" 14 | }, 15 | "babel": { 16 | "presets": ["es2015", "react"] 17 | }, 18 | "dependencies": { 19 | "alt": "^0.17.8", 20 | "async": "^1.5.0", 21 | "body-parser": "^1.14.1", 22 | "colors": "^1.1.2", 23 | "compression": "^1.6.0", 24 | "express": "^4.13.3", 25 | "history": "^1.13.0", 26 | "mongoose": "^4.2.5", 27 | "morgan": "^1.6.1", 28 | "react": "^0.14.2", 29 | "react-dom": "^0.14.2", 30 | "react-router": "^1.0.0", 31 | "request": "^2.65.0", 32 | "serve-favicon": "^2.3.0", 33 | "socket.io": "^1.3.7", 34 | "swig": "^1.4.2", 35 | "underscore": "^1.8.3", 36 | "xml2js": "^0.4.15" 37 | }, 38 | "devDependencies": { 39 | "babel-core": "^6.1.19", 40 | "babel-preset-es2015": "^6.1.18", 41 | "babel-preset-react": "^6.1.18", 42 | "babel-register": "^6.3.13", 43 | "babelify": "^7.2.0", 44 | "bower": "^1.6.5", 45 | "browserify": "^12.0.1", 46 | "gulp": "^3.9.0", 47 | "gulp-autoprefixer": "^3.1.0", 48 | "gulp-concat": "^2.6.0", 49 | "gulp-cssmin": "^0.1.7", 50 | "gulp-if": "^2.0.0", 51 | "gulp-less": "^3.0.3", 52 | "gulp-plumber": "^1.0.1", 53 | "gulp-sourcemaps": "^1.6.0", 54 | "gulp-uglify": "^1.4.2", 55 | "gulp-util": "^3.0.7", 56 | "vinyl-buffer": "^1.0.0", 57 | "vinyl-source-stream": "^1.1.0", 58 | "watchify": "^3.6.0" 59 | }, 60 | "license": "MIT" 61 | } 62 | -------------------------------------------------------------------------------- /public/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/css/.gitkeep -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/favicon.png -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 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 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/img/amarr_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/img/amarr_bg.jpg -------------------------------------------------------------------------------- /public/img/caldari_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/img/caldari_bg.jpg -------------------------------------------------------------------------------- /public/img/gallente_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/img/gallente_bg.jpg -------------------------------------------------------------------------------- /public/img/minmatar_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/img/minmatar_bg.jpg -------------------------------------------------------------------------------- /public/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahat/newedenfaces-react/653bfa2f1470cbe44cb55167020f0551053ca756/public/js/.gitkeep -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Babel ES6/JSX Compiler 2 | require('babel-register'); 3 | 4 | var path = require('path'); 5 | var express = require('express'); 6 | var bodyParser = require('body-parser'); 7 | var compression = require('compression'); 8 | var favicon = require('serve-favicon'); 9 | var logger = require('morgan'); 10 | var async = require('async'); 11 | var colors = require('colors'); 12 | var mongoose = require('mongoose'); 13 | var request = require('request'); 14 | var React = require('react'); 15 | var ReactDOM = require('react-dom/server'); 16 | var Router = require('react-router'); 17 | var swig = require('swig'); 18 | var xml2js = require('xml2js'); 19 | var _ = require('underscore'); 20 | 21 | var config = require('./config'); 22 | var routes = require('./app/routes'); 23 | var Character = require('./models/character'); 24 | 25 | var app = express(); 26 | 27 | mongoose.connect(config.database); 28 | mongoose.connection.on('error', function() { 29 | console.info('Error: Could not connect to MongoDB. Did you forget to run `mongod`?'.red); 30 | }); 31 | 32 | app.set('port', process.env.PORT || 3000); 33 | app.use(compression()); 34 | app.use(logger('dev')); 35 | app.use(bodyParser.json()); 36 | app.use(bodyParser.urlencoded({ extended: false })); 37 | app.use(favicon(path.join(__dirname, 'public', 'favicon.png'))); 38 | app.use(express.static(path.join(__dirname, 'public'))); 39 | 40 | /** 41 | * GET /api/characters 42 | * Returns 2 random characters of the same gender that have not been voted yet. 43 | */ 44 | app.get('/api/characters', function(req, res, next) { 45 | var choices = ['Female', 'Male']; 46 | var randomGender = _.sample(choices); 47 | 48 | Character.find({ random: { $near: [Math.random(), 0] } }) 49 | .where('voted', false) 50 | .where('gender', randomGender) 51 | .limit(2) 52 | .exec(function(err, characters) { 53 | if (err) return next(err); 54 | 55 | if (characters.length === 2) { 56 | return res.send(characters); 57 | } 58 | 59 | var oppositeGender = _.first(_.without(choices, randomGender)); 60 | 61 | Character 62 | .find({ random: { $near: [Math.random(), 0] } }) 63 | .where('voted', false) 64 | .where('gender', oppositeGender) 65 | .limit(2) 66 | .exec(function(err, characters) { 67 | if (err) return next(err); 68 | 69 | if (characters.length === 2) { 70 | return res.send(characters); 71 | } 72 | 73 | Character.update({}, { $set: { voted: false } }, { multi: true }, function(err) { 74 | if (err) return next(err); 75 | res.send([]); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | /** 82 | * PUT /api/characters 83 | * Update winning and losing count for both characters. 84 | */ 85 | app.put('/api/characters', function(req, res, next) { 86 | var winner = req.body.winner; 87 | var loser = req.body.loser; 88 | 89 | if (!winner || !loser) { 90 | return res.status(400).send({ message: 'Voting requires two characters.' }); 91 | } 92 | 93 | if (winner === loser) { 94 | return res.status(400).send({ message: 'Cannot vote for and against the same character.' }); 95 | } 96 | 97 | async.parallel([ 98 | function(callback) { 99 | Character.findOne({ characterId: winner }, function(err, winner) { 100 | callback(err, winner); 101 | }); 102 | }, 103 | function(callback) { 104 | Character.findOne({ characterId: loser }, function(err, loser) { 105 | callback(err, loser); 106 | }); 107 | } 108 | ], 109 | function(err, results) { 110 | if (err) return next(err); 111 | 112 | var winner = results[0]; 113 | var loser = results[1]; 114 | 115 | if (!winner || !loser) { 116 | return res.status(404).send({ message: 'One of the characters no longer exists.' }); 117 | } 118 | 119 | if (winner.voted || loser.voted) { 120 | return res.status(200).end(); 121 | } 122 | 123 | async.parallel([ 124 | function(callback) { 125 | winner.wins++; 126 | winner.voted = true; 127 | winner.random = [Math.random(), 0]; 128 | winner.save(function(err) { 129 | callback(err); 130 | }); 131 | }, 132 | function(callback) { 133 | loser.losses++; 134 | loser.voted = true; 135 | loser.random = [Math.random(), 0]; 136 | loser.save(function(err) { 137 | callback(err); 138 | }); 139 | } 140 | ], function(err) { 141 | if (err) return next(err); 142 | res.status(200).end(); 143 | }); 144 | }); 145 | }); 146 | 147 | /** 148 | * GET /api/characters/shame 149 | * Returns 100 lowest ranked characters. 150 | */ 151 | app.get('/api/characters/shame', function(req, res, next) { 152 | Character 153 | .find() 154 | .sort('-losses') 155 | .limit(100) 156 | .exec(function(err, characters) { 157 | if (err) return next(err); 158 | res.send(characters); 159 | }); 160 | }); 161 | 162 | /** 163 | * GET /api/characters/top 164 | * Return 100 highest ranked characters. Filter by gender, race and bloodline. 165 | */ 166 | app.get('/api/characters/top', function(req, res, next) { 167 | var params = req.query; 168 | var conditions = {}; 169 | 170 | _.each(params, function(value, key) { 171 | conditions[key] = new RegExp('^' + value + '$', 'i'); 172 | }); 173 | 174 | Character 175 | .find(conditions) 176 | .sort('-wins') 177 | .limit(100) 178 | .exec(function(err, characters) { 179 | if (err) return next(err); 180 | 181 | characters.sort(function(a, b) { 182 | if (a.wins / (a.wins + a.losses) < b.wins / (b.wins + b.losses)) { return 1; } 183 | if (a.wins / (a.wins + a.losses) > b.wins / (b.wins + b.losses)) { return -1; } 184 | return 0; 185 | }); 186 | 187 | res.send(characters); 188 | }); 189 | }); 190 | 191 | /** 192 | * GET /api/characters/count 193 | * Returns the total number of characters. 194 | */ 195 | app.get('/api/characters/count', function(req, res, next) { 196 | Character.count({}, function(err, count) { 197 | if (err) return next(err); 198 | res.send({ count: count }); 199 | }); 200 | }); 201 | 202 | /** 203 | * GET /api/characters/search 204 | * Looks up a character by name. (case-insensitive) 205 | */ 206 | app.get('/api/characters/search', function(req, res, next) { 207 | var characterName = new RegExp(req.query.name, 'i'); 208 | 209 | Character.findOne({ name: characterName }, function(err, character) { 210 | if (err) return next(err); 211 | 212 | if (!character) { 213 | return res.status(404).send({ message: 'Character not found.' }); 214 | } 215 | 216 | res.send(character); 217 | }); 218 | }); 219 | 220 | /** 221 | * GET /api/characters/:id 222 | * Returns detailed character information. 223 | */ 224 | app.get('/api/characters/:id', function(req, res, next) { 225 | var id = req.params.id; 226 | 227 | Character.findOne({ characterId: id }, function(err, character) { 228 | if (err) return next(err); 229 | 230 | if (!character) { 231 | return res.status(404).send({ message: 'Character not found.' }); 232 | } 233 | 234 | res.send(character); 235 | }); 236 | }); 237 | 238 | /** 239 | * POST /api/characters 240 | * Adds new character to the database. 241 | */ 242 | app.post('/api/characters', function(req, res, next) { 243 | var gender = req.body.gender; 244 | var characterName = req.body.name; 245 | var characterIdLookupUrl = 'https://api.eveonline.com/eve/CharacterID.xml.aspx?names=' + characterName; 246 | 247 | var parser = new xml2js.Parser(); 248 | 249 | async.waterfall([ 250 | function(callback) { 251 | request.get(characterIdLookupUrl, function(err, request, xml) { 252 | if (err) return next(err); 253 | parser.parseString(xml, function(err, parsedXml) { 254 | if (err) return next(err); 255 | try { 256 | var characterId = parsedXml.eveapi.result[0].rowset[0].row[0].$.characterID; 257 | 258 | Character.findOne({ characterId: characterId }, function(err, character) { 259 | if (err) return next(err); 260 | 261 | if (character) { 262 | return res.status(409).send({ message: character.name + ' is already in the database.' }); 263 | } 264 | 265 | callback(err, characterId); 266 | }); 267 | } catch (e) { 268 | return res.status(400).send({ message: 'XML Parse Error' }); 269 | } 270 | }); 271 | }); 272 | }, 273 | function(characterId) { 274 | var characterInfoUrl = 'https://api.eveonline.com/eve/CharacterInfo.xml.aspx?characterID=' + characterId; 275 | 276 | request.get({ url: characterInfoUrl }, function(err, request, xml) { 277 | if (err) return next(err); 278 | parser.parseString(xml, function(err, parsedXml) { 279 | if (err) return res.send(err); 280 | try { 281 | var name = parsedXml.eveapi.result[0].characterName[0]; 282 | var race = parsedXml.eveapi.result[0].race[0]; 283 | var bloodline = parsedXml.eveapi.result[0].bloodline[0]; 284 | 285 | var character = new Character({ 286 | characterId: characterId, 287 | name: name, 288 | race: race, 289 | bloodline: bloodline, 290 | gender: gender, 291 | random: [Math.random(), 0] 292 | }); 293 | 294 | character.save(function(err) { 295 | if (err) return next(err); 296 | res.send({ message: characterName + ' has been added successfully!' }); 297 | }); 298 | } catch (e) { 299 | res.status(404).send({ message: characterName + ' is not a registered citizen of New Eden.' }); 300 | } 301 | }); 302 | }); 303 | } 304 | ]); 305 | }); 306 | 307 | /** 308 | * GET /api/stats 309 | * Returns characters statistics. 310 | */ 311 | app.get('/api/stats', function(req, res, next) { 312 | async.parallel([ 313 | function(callback) { 314 | Character.count({}, function(err, count) { 315 | callback(err, count); 316 | }); 317 | }, 318 | function(callback) { 319 | Character.count({ race: 'Amarr' }, function(err, amarrCount) { 320 | callback(err, amarrCount); 321 | }); 322 | }, 323 | function(callback) { 324 | Character.count({ race: 'Caldari' }, function(err, caldariCount) { 325 | callback(err, caldariCount); 326 | }); 327 | }, 328 | function(callback) { 329 | Character.count({ race: 'Gallente' }, function(err, gallenteCount) { 330 | callback(err, gallenteCount); 331 | }); 332 | }, 333 | function(callback) { 334 | Character.count({ race: 'Minmatar' }, function(err, minmatarCount) { 335 | callback(err, minmatarCount); 336 | }); 337 | }, 338 | function(callback) { 339 | Character.count({ gender: 'Male' }, function(err, maleCount) { 340 | callback(err, maleCount); 341 | }); 342 | }, 343 | function(callback) { 344 | Character.count({ gender: 'Female' }, function(err, femaleCount) { 345 | callback(err, femaleCount); 346 | }); 347 | }, 348 | function(callback) { 349 | Character.aggregate({ $group: { _id: null, total: { $sum: '$wins' } } }, function(err, totalVotes) { 350 | var total = totalVotes.length ? totalVotes[0].total : 0; 351 | callback(err, total); 352 | } 353 | ); 354 | }, 355 | function(callback) { 356 | Character 357 | .find() 358 | .sort('-wins') 359 | .limit(100) 360 | .select('race') 361 | .exec(function(err, characters) { 362 | if (err) return next(err); 363 | 364 | var raceCount = _.countBy(characters, function(character) { return character.race; }); 365 | var max = _.max(raceCount, function(race) { return race }); 366 | var inverted = _.invert(raceCount); 367 | var topRace = inverted[max]; 368 | var topCount = raceCount[topRace]; 369 | 370 | callback(err, { race: topRace, count: topCount }); 371 | }); 372 | }, 373 | function(callback) { 374 | Character 375 | .find() 376 | .sort('-wins') 377 | .limit(100) 378 | .select('bloodline') 379 | .exec(function(err, characters) { 380 | if (err) return next(err); 381 | 382 | var bloodlineCount = _.countBy(characters, function(character) { return character.bloodline; }); 383 | var max = _.max(bloodlineCount, function(bloodline) { return bloodline }); 384 | var inverted = _.invert(bloodlineCount); 385 | var topBloodline = inverted[max]; 386 | var topCount = bloodlineCount[topBloodline]; 387 | 388 | callback(err, { bloodline: topBloodline, count: topCount }); 389 | }); 390 | } 391 | ], 392 | function(err, results) { 393 | if (err) return next(err); 394 | 395 | res.send({ 396 | totalCount: results[0], 397 | amarrCount: results[1], 398 | caldariCount: results[2], 399 | gallenteCount: results[3], 400 | minmatarCount: results[4], 401 | maleCount: results[5], 402 | femaleCount: results[6], 403 | totalVotes: results[7], 404 | leadingRace: results[8], 405 | leadingBloodline: results[9] 406 | }); 407 | }); 408 | }); 409 | 410 | 411 | /** 412 | * POST /api/report 413 | * Reports a character. Character is removed after 4 reports. 414 | */ 415 | app.post('/api/report', function(req, res, next) { 416 | var characterId = req.body.characterId; 417 | 418 | Character.findOne({ characterId: characterId }, function(err, character) { 419 | if (err) return next(err); 420 | 421 | if (!character) { 422 | return res.status(404).send({ message: 'Character not found.' }); 423 | } 424 | 425 | character.reports++; 426 | 427 | if (character.reports > 4) { 428 | character.remove(); 429 | return res.send({ message: character.name + ' has been deleted.' }); 430 | } 431 | 432 | character.save(function(err) { 433 | if (err) return next(err); 434 | res.send({ message: character.name + ' has been reported.' }); 435 | }); 436 | }); 437 | }); 438 | 439 | app.use(function(req, res) { 440 | Router.match({ routes: routes.default, location: req.url }, function(err, redirectLocation, renderProps) { 441 | if (err) { 442 | res.status(500).send(err.message) 443 | } else if (redirectLocation) { 444 | res.status(302).redirect(redirectLocation.pathname + redirectLocation.search) 445 | } else if (renderProps) { 446 | var html = ReactDOM.renderToString(React.createElement(Router.RoutingContext, renderProps)); 447 | var page = swig.renderFile('views/index.html', { html: html }); 448 | res.status(200).send(page); 449 | } else { 450 | res.status(404).send('Page Not Found') 451 | } 452 | }); 453 | }); 454 | 455 | app.use(function(err, req, res, next) { 456 | console.log(err.stack.red); 457 | res.status(err.status || 500); 458 | res.send({ message: err.message }); 459 | }); 460 | 461 | /** 462 | * Socket.io stuff. 463 | */ 464 | var server = require('http').createServer(app); 465 | var io = require('socket.io')(server); 466 | var onlineUsers = 0; 467 | 468 | io.sockets.on('connection', function(socket) { 469 | onlineUsers++; 470 | 471 | io.sockets.emit('onlineUsers', { onlineUsers: onlineUsers }); 472 | 473 | socket.on('disconnect', function() { 474 | onlineUsers--; 475 | io.sockets.emit('onlineUsers', { onlineUsers: onlineUsers }); 476 | }); 477 | }); 478 | 479 | server.listen(app.get('port'), function() { 480 | console.log('Express server listening on port ' + app.get('port')); 481 | }); 482 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | New Eden Faces 8 | 9 | 10 | 11 | 12 |
    {{html|safe}}
    13 | 14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------