├── .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 | [](https://paypal.me/sahat) [](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 | 
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 |
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 |
12 |
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 |
51 |
52 |
{this.state.name}
53 | Race: {this.state.race}
54 | Bloodline: {this.state.bloodline}
55 | Gender: {this.state.gender}
56 |
61 |
62 |
63 |
64 | - {this.state.winLossRatio}Winning Percentage
65 | - {this.state.wins} Wins
66 | - {this.state.losses} Losses
67 |
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 | Stats |
33 |
34 |
35 |
36 |
37 | Leading race in Top 100 |
38 | {this.state.leadingRace.race} with {this.state.leadingRace.count} characters |
39 |
40 |
41 | Leading bloodline in Top 100 |
42 | {this.state.leadingBloodline.bloodline} with {this.state.leadingBloodline.count} characters
43 | |
44 |
45 |
46 | Amarr Characters |
47 | {this.state.amarrCount} |
48 |
49 |
50 | Caldari Characters |
51 | {this.state.caldariCount} |
52 |
53 |
54 | Gallente Characters |
55 | {this.state.gallenteCount} |
56 |
57 |
58 | Minmatar Characters |
59 | {this.state.minmatarCount} |
60 |
61 |
62 | Total votes cast |
63 | {this.state.totalVotes} |
64 |
65 |
66 | Female characters |
67 | {this.state.femaleCount} |
68 |
69 |
70 | Male characters |
71 | {this.state.maleCount} |
72 |
73 |
74 | Total number of characters |
75 | {this.state.totalCount} |
76 |
77 |
78 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------