Add judo heroes example universal/isomorphic javascript app
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["react", "es2015"]
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
# EditorConfig is awesome: http://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = false
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = tab
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{cpp,c,h,hpp,cxx}]
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
# Yaml files
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
|
@ -0,0 +1,4 @@
|
||||||
|
dist/
|
||||||
|
babel_cache/
|
||||||
|
node_modules/
|
||||||
|
public/js/bundle.js
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"babel-cli": "^6.11.4",
|
||||||
|
"babel-core": "^6.13.2",
|
||||||
|
"babel-preset-es2015": "^6.13.2",
|
||||||
|
"babel-preset-react": "^6.11.1",
|
||||||
|
"ejs": "^2.5.1",
|
||||||
|
"express": "^4.14.0",
|
||||||
|
"react": "^15.3.1",
|
||||||
|
"react-dom": "^15.3.1",
|
||||||
|
"react-router": "^2.6.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-loader": "^6.2.5",
|
||||||
|
"http-server": "^0.9.0",
|
||||||
|
"webpack": "^1.13.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "NODE_ENV=production babel-node src/server.js",
|
||||||
|
"build": "npm run build:client && npm run build:server",
|
||||||
|
"build:client": "NODE_ENV=production webpack -p",
|
||||||
|
"build:server": "NODE_ENV=production babel src -d dist",
|
||||||
|
"buildrun": "npm run build && npm run serve",
|
||||||
|
"clean": "rm -rf ./dist && rm -rf ./babel_cache && rm ./public/js/bundle.js",
|
||||||
|
"serve": "node dist/server.js"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,246 @@
|
||||||
|
/*! CSS reset from benfrain/app-reset */
|
||||||
|
*,:after,:before{box-sizing:inherit}html{box-sizing:border-box}
|
||||||
|
*{user-select:none;-webkit-tap-highlight-color:rgba(255,255,255,0);-webkit-tap-highlight-color:transparent}
|
||||||
|
[contenteditable],input[type]{user-select:text}body,h1,h2,h3,h4,h5,h6,p{margin:0;font-size:1rem;font-weight:400}
|
||||||
|
a{text-decoration:none;color:inherit}b{font-weight:400}em,i{font-style:normal}a:focus,input:focus{outline:0}
|
||||||
|
fieldset,input{appearance:none;border:0;padding:0;margin:0;min-width:0;font-size:1rem;font-family:inherit}
|
||||||
|
input::-ms-clear{display:none}input[type=number]{-moz-appearance:textfield}
|
||||||
|
input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{appearance:none}
|
||||||
|
svg{display:inline-flex}img{max-width:100%;display:block}
|
||||||
|
|
||||||
|
html {
|
||||||
|
color: #030303;
|
||||||
|
font: caption;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1f4ba0;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .logo {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin: 4em auto;
|
||||||
|
padding: 2em;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer p {
|
||||||
|
line-height: 1.2em;
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 2em;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found h1 {
|
||||||
|
font-size: 3em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home .athletes-selector {
|
||||||
|
clear: both;
|
||||||
|
padding: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home .athletes-selector .athlete-preview {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 1em 1em 0;
|
||||||
|
max-width: 200px;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home .athletes-selector .athlete-preview img {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home .athletes-selector .athlete-preview .name {
|
||||||
|
color: #030303;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.6em;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: .2em;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home .athletes-selector .athlete-preview .medals-count {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: .2em;
|
||||||
|
padding: .2em .4em;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
top: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.atheletes-menu {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 2em;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.atheletes-menu a {
|
||||||
|
font-size: 1.6em;
|
||||||
|
margin: 0 1em 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.atheletes-menu a.active {
|
||||||
|
color: #030303;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete header {
|
||||||
|
background: #ccc;
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .picture-container {
|
||||||
|
margin: -160px 0 0 0;
|
||||||
|
padding: 0 1em 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .picture-container img {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 8px solid #fff;
|
||||||
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
|
||||||
|
display: inline;
|
||||||
|
margin: 0 0 0 1em;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .picture-container h2 {
|
||||||
|
display: inline;
|
||||||
|
font-size: 3em;
|
||||||
|
padding: 0 0 0 .5em;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .flag .icon {
|
||||||
|
display: inline-block;
|
||||||
|
padding-bottom: .1em;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .description,
|
||||||
|
.athlete .medals {
|
||||||
|
font-size: 1.6em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .medals p {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .medals li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .medals .symbol {
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: .8em;
|
||||||
|
height: 1.6em;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 5px;
|
||||||
|
text-align: center;
|
||||||
|
width: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .medals .symbol.symbol-G {
|
||||||
|
color: #daa520;
|
||||||
|
background-color: #fff6de;
|
||||||
|
border: 2px solid #daa520;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .medals .symbol.symbol-S {
|
||||||
|
color: #383738;
|
||||||
|
background-color: #b9b5b5;
|
||||||
|
border: 2px solid #383738;
|
||||||
|
}
|
||||||
|
|
||||||
|
.athlete .medals .symbol.symbol-B {
|
||||||
|
color: #6b1919;
|
||||||
|
background-color: #ea96a1;
|
||||||
|
border: 2px solid #6E1924;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigateBack {
|
||||||
|
font-size: 1.6em;
|
||||||
|
padding: 2em;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#abar {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#abar a {
|
||||||
|
background: #ffa500;
|
||||||
|
color: #000;
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#abar a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 563 B |
After Width: | Height: | Size: 545 B |
After Width: | Height: | Size: 420 B |
After Width: | Height: | Size: 453 B |
After Width: | Height: | Size: 515 B |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 634 B |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 145 KiB |
After Width: | Height: | Size: 44 KiB |
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Judo Heroes - A Universal Javascript demo application with React</title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main"></div>
|
||||||
|
<script src="/js/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,5 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import AppRoutes from './components/AppRoutes';
|
||||||
|
|
||||||
|
ReactDOM.render(<AppRoutes />, document.getElementById('main'));
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Router, browserHistory } from 'react-router';
|
||||||
|
import routes from '../routes';
|
||||||
|
|
||||||
|
class AppRoutes extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Router history={browserHistory} routes={routes} onUpdate={() => window.scrollTo(0, 0)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppRoutes;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import athletes from '../data/athletes';
|
||||||
|
|
||||||
|
class AthleteMenu extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<nav className="atheletes-menu">
|
||||||
|
{athletes.map(menuAthlete => {
|
||||||
|
return <Link key={menuAthlete.id} to={`/athlete/${menuAthlete.id}`} activeClassName="active">
|
||||||
|
{menuAthlete.name}
|
||||||
|
</Link>;
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AthleteMenu;
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import NotFoundPage from './NotFoundPage';
|
||||||
|
import AthleteMenu from './AthleteMenu';
|
||||||
|
import Medal from './Medal';
|
||||||
|
import Flag from './Flag';
|
||||||
|
import athletes from '../data/athletes';
|
||||||
|
|
||||||
|
class AthletePage extends React.Component {
|
||||||
|
render() {
|
||||||
|
const id = this.props.params.id;
|
||||||
|
const athlete = athletes.filter(athlete => athlete.id === id)[0];
|
||||||
|
if ( ! athlete) {
|
||||||
|
return <NotFoundPage />
|
||||||
|
}
|
||||||
|
const headerStyle = {
|
||||||
|
backgroundImage: `url(/img/${athlete.cover})`
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="athlete-full">
|
||||||
|
<AthleteMenu />
|
||||||
|
<div className="athlete">
|
||||||
|
<header style={headerStyle} />
|
||||||
|
<div className="picture-container">
|
||||||
|
<img src={`/img/${athlete.image}`} />
|
||||||
|
<h2 className="name">{athlete.name}</h2>
|
||||||
|
</div>
|
||||||
|
<section className="description">
|
||||||
|
Olympic medalist from <strong><Flag code={athlete.country} showName="true" /></strong>,
|
||||||
|
born in {athlete.birth} (Find out more on <a href={athlete.link}>Wikipedia</a>).
|
||||||
|
</section>
|
||||||
|
<section className="medals">
|
||||||
|
<p>Winner of <string>{athlete.medals.length}</string> medals:</p>
|
||||||
|
<ul>{
|
||||||
|
athlete.medals.map((medal, i) => <Medal key={i} {...medal} />)
|
||||||
|
}</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div className="navigateBack">
|
||||||
|
<Link to="/">« Back to the index</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AthletePage;
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
class AthletePreview extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Link to={`/athlete/${this.props.id}`}>
|
||||||
|
<div className="athlete-preview">
|
||||||
|
<img src={`img/${this.props.image}`} />
|
||||||
|
<h2 className="name">{this.props.name}</h2>
|
||||||
|
<span className="medals-count">
|
||||||
|
<img src="/img/medal.png" />
|
||||||
|
{this.props.medals.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AthletePreview;
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
cu: {
|
||||||
|
name: 'Cuba',
|
||||||
|
icon: 'flag-cu.png'
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
name: 'France',
|
||||||
|
icon: 'flag-fr.png'
|
||||||
|
},
|
||||||
|
jp: {
|
||||||
|
name: 'Japan',
|
||||||
|
icon: 'flag-jp.png'
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
name: 'Netherlands',
|
||||||
|
icon: 'flag-nl.png'
|
||||||
|
},
|
||||||
|
uz: {
|
||||||
|
name: 'Uzbekistan',
|
||||||
|
icon: 'flag-uz.png'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Flag extends React.Component {
|
||||||
|
render() {
|
||||||
|
const name = data[this.props.code].name;
|
||||||
|
const icon = data[this.props.code].icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="flag">
|
||||||
|
<img className="icon" title={name} src={`/img/${icon}`} />
|
||||||
|
{this.props.showName && <span className="name">{name}</span>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Flag;
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import AthletePreview from './AthletePreview';
|
||||||
|
import athletes from '../data/athletes';
|
||||||
|
|
||||||
|
class IndexPage extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="home">
|
||||||
|
<div className="athletes-selector">
|
||||||
|
{athletes.map(athleteData => <AthletePreview key={athleteData.id} {...athleteData} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexPage;
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
class Layout extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header>
|
||||||
|
<Link to="/">
|
||||||
|
<img className="logo" src="/img/logo-judo-heroes.png" />
|
||||||
|
</Link>
|
||||||
|
</header>
|
||||||
|
<div className="app-content">{this.props.children}</div>
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
This is a demo app to showcase universal rendering and routing with <strong>React</strong> and <strong>Express</strong>.
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const typeMap = {
|
||||||
|
G: 'Gold',
|
||||||
|
S: 'Silver',
|
||||||
|
B: 'Bronze'
|
||||||
|
};
|
||||||
|
|
||||||
|
class Medal extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<li className="medal">
|
||||||
|
<span className={`symbol symbol-${this.props.type}`} title={typeMap[this.props.type]}>{this.props.type}</span>
|
||||||
|
<span className="year">{this.props.year}</span>
|
||||||
|
<span className="city">{this.props.city}</span>
|
||||||
|
<span className="event">({this.props.event})</span>
|
||||||
|
<span className="category">{this.props.category}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Medal;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
class NotFoundPage extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="not-found">
|
||||||
|
<h1>404</h1>
|
||||||
|
<h2>Page not found!</h2>
|
||||||
|
<p>
|
||||||
|
<Link to="/">Go back to the main page</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotFoundPage;
|
|
@ -0,0 +1,134 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const athletes = [
|
||||||
|
{
|
||||||
|
'id': 'driulis-gonzalez',
|
||||||
|
'name': 'Driulis González',
|
||||||
|
'country': 'cu',
|
||||||
|
'birth': '1973',
|
||||||
|
'image': 'driulis-gonzalez.jpg',
|
||||||
|
'cover': 'driulis-gonzalez-cover.jpg',
|
||||||
|
'link': 'https://en.wikipedia.org/wiki/Driulis_González',
|
||||||
|
'medals': [
|
||||||
|
{ 'year': '1992', 'type': 'B', 'city': 'Barcelona', 'event': 'Olympic Games', 'category': '-57kg' },
|
||||||
|
{ 'year': '1993', 'type': 'B', 'city': 'Hamilton', 'event': 'World Championships', 'category': '-57kg' },
|
||||||
|
{ 'year': '1995', 'type': 'G', 'city': 'Chiba', 'event': 'World Championships', 'category': '-57kg' },
|
||||||
|
{ 'year': '1995', 'type': 'G', 'city': 'Mar del Plata', 'event': 'Pan American Games', 'category': '-57kg' },
|
||||||
|
{ 'year': '1996', 'type': 'G', 'city': 'Atlanta', 'event': 'Olympic Games', 'category': '-57kg' },
|
||||||
|
{ 'year': '1997', 'type': 'S', 'city': 'Osaka', 'event': 'World Championships', 'category': '-57kg' },
|
||||||
|
{ 'year': '1999', 'type': 'G', 'city': 'Birmingham', 'event': 'World Championships', 'category': '-57kg' },
|
||||||
|
{ 'year': '2000', 'type': 'S', 'city': 'Sydney', 'event': 'Olympic Games', 'category': '-57kg' },
|
||||||
|
{ 'year': '2003', 'type': 'G', 'city': 'S Domingo', 'event': 'Pan American Games', 'category': '-63kg' },
|
||||||
|
{ 'year': '2003', 'type': 'S', 'city': 'Osaka', 'event': 'World Championships', 'category': '-63kg' },
|
||||||
|
{ 'year': '2004', 'type': 'B', 'city': 'Athens', 'event': 'Olympic Games', 'category': '-63kg' },
|
||||||
|
{ 'year': '2005', 'type': 'B', 'city': 'Cairo', 'event': 'World Championships', 'category': '-63kg' },
|
||||||
|
{ 'year': '2006', 'type': 'G', 'city': 'Cartagena', 'event': 'Central American and Caribbean Games', 'category': '-63kg' },
|
||||||
|
{ 'year': '2006', 'type': 'G', 'city': 'Cartagena', 'event': 'Central American and Caribbean Games', 'category': 'Tema' },
|
||||||
|
{ 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'Pan American Games', 'category': '-63kg' },
|
||||||
|
{ 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'World Championships', 'category': '-63kg' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'mark-huizinga',
|
||||||
|
'name': 'Mark Huizinga',
|
||||||
|
'country': 'nl',
|
||||||
|
'birth': '1973',
|
||||||
|
'image': 'mark-huizinga.jpg',
|
||||||
|
'cover': 'mark-huizinga-cover.jpg',
|
||||||
|
'link': 'https://en.wikipedia.org/wiki/Mark_Huizinga',
|
||||||
|
'medals': [
|
||||||
|
{ 'year': '1994', 'type': 'B', 'city': 'Gdansk', 'event': 'European Championships', 'category': '-78kg' },
|
||||||
|
{ 'year': '1996', 'type': 'B', 'city': 'Atlanta', 'event': 'Olympic Games', 'category': '-86kg' },
|
||||||
|
{ 'year': '1996', 'type': 'G', 'city': 'The Hague', 'event': 'European Championships', 'category': '-86kg' },
|
||||||
|
{ 'year': '1997', 'type': 'G', 'city': 'Oostende', 'event': 'European Championships', 'category': '-86kg' },
|
||||||
|
{ 'year': '1998', 'type': 'G', 'city': 'Oviedo', 'event': 'European Championships', 'category': '-90kg' },
|
||||||
|
{ 'year': '1999', 'type': 'B', 'city': 'Bratislava', 'event': 'European Championships', 'category': '-90kg' },
|
||||||
|
{ 'year': '2000', 'type': 'G', 'city': 'Sydney', 'event': 'Olympic Games', 'category': '-90kg' },
|
||||||
|
{ 'year': '2000', 'type': 'S', 'city': 'Wroclaw', 'event': 'European Championships', 'category': '-90kg' },
|
||||||
|
{ 'year': '2001', 'type': 'G', 'city': 'Paris', 'event': 'European Championships', 'category': '-90kg' },
|
||||||
|
{ 'year': '2002', 'type': 'B', 'city': 'Maribor', 'event': 'European Championships', 'category': '-90kg' },
|
||||||
|
{ 'year': '2003', 'type': 'B', 'city': 'Düsseldorf', 'event': 'European Championships', 'category': '-90kg' },
|
||||||
|
{ 'year': '2004', 'type': 'B', 'city': 'Athens', 'event': 'Olympic Games', 'category': '-90kg' },
|
||||||
|
{ 'year': '2004', 'type': 'S', 'city': 'Bucharest', 'event': 'European Championships', 'category': '-90kg' },
|
||||||
|
{ 'year': '2005', 'type': 'B', 'city': 'Cairo', 'event': 'World Championships', 'category': '-90kg' },
|
||||||
|
{ 'year': '2005', 'type': 'B', 'city': 'Rotterdam', 'event': 'European Championships', 'category': '-90kg' },
|
||||||
|
{ 'year': '2008', 'type': 'G', 'city': 'Lisbon', 'event': 'European Championships', 'category': '-90kg' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'rishod-sobirov',
|
||||||
|
'name': 'Rishod Sobirov',
|
||||||
|
'country': 'uz',
|
||||||
|
'birth': '1986',
|
||||||
|
'image': 'rishod-sobirov.jpg',
|
||||||
|
'cover': 'rishod-sobirov-cover.jpg',
|
||||||
|
'link': 'https://en.wikipedia.org/wiki/Rishod_Sobirov',
|
||||||
|
'medals': [
|
||||||
|
{ 'year': '2007', 'type': 'S', 'city': 'Kuwait City', 'event': 'Asian Championships', 'category': '-60kg' },
|
||||||
|
{ 'year': '2008', 'type': 'B', 'city': 'Beijing', 'event': 'Olympic Games', 'category': '-60kg' },
|
||||||
|
{ 'year': '2010', 'type': 'G', 'city': 'Tokyo', 'event': 'World Championships', 'category': '-60kg' },
|
||||||
|
{ 'year': '2011', 'type': 'G', 'city': 'Paris', 'event': 'World Championships', 'category': '-60kg' },
|
||||||
|
{ 'year': '2012', 'type': 'B', 'city': 'London', 'event': 'Olympic Games', 'category': '-60kg' },
|
||||||
|
{ 'year': '2015', 'type': 'B', 'city': 'Astana', 'event': 'World Championships', 'category': '-66kg' },
|
||||||
|
{ 'year': '2016', 'type': 'B', 'city': 'Rio de Janeiro', 'event': 'Olympic Games', 'category': '-66kg' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'ryoko-tani',
|
||||||
|
'name': 'Ryoko Tani',
|
||||||
|
'country': 'jp',
|
||||||
|
'birth': '1975',
|
||||||
|
'image': 'ryoko-tani.jpg',
|
||||||
|
'cover': 'ryoko-tani-cover.jpg',
|
||||||
|
'link': 'https://en.wikipedia.org/wiki/Ryoko_Tani',
|
||||||
|
'medals': [
|
||||||
|
{ 'year': '1991', 'type': 'B', 'city': 'Barcelona', 'event': 'World Championships', 'category': '-48kg' },
|
||||||
|
{ 'year': '1991', 'type': 'B', 'city': 'Osaka', 'event': 'Asian Championships', 'category': '-48kg' },
|
||||||
|
{ 'year': '1992', 'type': 'S', 'city': 'Barcelona', 'event': 'Olympic Games', 'category': '-48kg' },
|
||||||
|
{ 'year': '1993', 'type': 'G', 'city': 'Hamilton', 'event': 'World Championships', 'category': '-48kg' },
|
||||||
|
{ 'year': '1994', 'type': 'G', 'city': 'Hiroshima', 'event': 'Asian Games', 'category': '-48kg' },
|
||||||
|
{ 'year': '1995', 'type': 'G', 'city': 'Chiba', 'event': 'World Championships', 'category': '-48kg' },
|
||||||
|
{ 'year': '1995', 'type': 'G', 'city': 'Fukuoka', 'event': 'Universiade', 'category': '-48kg' },
|
||||||
|
{ 'year': '1996', 'type': 'S', 'city': 'Atlanta', 'event': 'Olympic Games', 'category': '-48kg' },
|
||||||
|
{ 'year': '1997', 'type': 'G', 'city': 'Paris', 'event': 'World Championships', 'category': '-48kg' },
|
||||||
|
{ 'year': '1999', 'type': 'G', 'city': 'Birmingham', 'event': 'World Championships', 'category': '-48kg' },
|
||||||
|
{ 'year': '2000', 'type': 'G', 'city': 'Sydney', 'event': 'Olympic Games', 'category': '-48kg' },
|
||||||
|
{ 'year': '2001', 'type': 'G', 'city': 'Munich', 'event': 'World Championships', 'category': '-48kg' },
|
||||||
|
{ 'year': '2003', 'type': 'G', 'city': 'Osaka', 'event': 'World Championships', 'category': '-48kg' },
|
||||||
|
{ 'year': '2004', 'type': 'G', 'city': 'Athens', 'event': 'Olympic Games', 'category': '-48kg' },
|
||||||
|
{ 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'World Championships', 'category': '-48kg' },
|
||||||
|
{ 'year': '2008', 'type': 'B', 'city': 'Beijing', 'event': 'Olympic Games', 'category': '-48kg' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'teddy-riner',
|
||||||
|
'name': 'Teddy Riner',
|
||||||
|
'country': 'fr',
|
||||||
|
'birth': '1989',
|
||||||
|
'image': 'teddy-riner.jpg',
|
||||||
|
'cover': 'teddy-riner-cover.jpg',
|
||||||
|
'link': 'https://en.wikipedia.org/wiki/Teddy_Riner',
|
||||||
|
'medals': [
|
||||||
|
{ 'year': '2007', 'type': 'G', 'city': 'Belgrade', 'event': 'European Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'World Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2008', 'type': 'B', 'city': 'Beijing', 'event': 'Olympic Games', 'category': '+100kg' },
|
||||||
|
{ 'year': '2008', 'type': 'G', 'city': 'Levallois-Perret', 'event': 'World Openweight Championships', 'category': 'Open' },
|
||||||
|
{ 'year': '2009', 'type': 'G', 'city': 'Pescara', 'event': 'Mediterranean Games', 'category': '+100kg' },
|
||||||
|
{ 'year': '2009', 'type': 'G', 'city': 'Rotterdam', 'event': 'World Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2010', 'type': 'G', 'city': 'Tokyo', 'event': 'World Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2010', 'type': 'S', 'city': 'Tokyo', 'event': 'World Championships', 'category': 'Open' },
|
||||||
|
{ 'year': '2011', 'type': 'G', 'city': 'Istanbul', 'event': 'European Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2011', 'type': 'G', 'city': 'Paris', 'event': 'World Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2012', 'type': 'G', 'city': 'London', 'event': 'Olympic Games', 'category': '+100kg' },
|
||||||
|
{ 'year': '2013', 'type': 'G', 'city': 'Budapest', 'event': 'European Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2013', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'World Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2014', 'type': 'G', 'city': 'Chelyabinsk', 'event': 'World Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2014', 'type': 'G', 'city': 'Montpellier', 'event': 'European Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2015', 'type': 'G', 'city': 'Astana', 'event': 'World Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2016', 'type': 'G', 'city': 'Kazan', 'event': 'European Championships', 'category': '+100kg' },
|
||||||
|
{ 'year': '2016', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'Olympic Games', 'category': '+100kg' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default athletes;
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Route, IndexRoute } from 'react-router';
|
||||||
|
import Layout from './components/Layout';
|
||||||
|
import IndexPage from './components/IndexPage';
|
||||||
|
import AthletePage from './components/AthletePage';
|
||||||
|
import NotFoundPage from './components/NotFoundPage';
|
||||||
|
|
||||||
|
const routes = (
|
||||||
|
<Route path="/" component={Layout}>
|
||||||
|
<IndexRoute component={IndexPage} />
|
||||||
|
<Route path="athlete/:id" component={AthletePage} />
|
||||||
|
<Route path="*" component={NotFoundPage} />
|
||||||
|
</Route>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default routes;
|
|
@ -0,0 +1,56 @@
|
||||||
|
import path from 'path';
|
||||||
|
import { Server } from 'http';
|
||||||
|
import Express from 'express';
|
||||||
|
import React from 'react';
|
||||||
|
import { renderToString } from 'react-dom/server';
|
||||||
|
import { match, RouterContext } from 'react-router';
|
||||||
|
import routes from './routes';
|
||||||
|
import NotFoundPage from './components/NotFoundPage';
|
||||||
|
|
||||||
|
// initialize the server and configure support for ejs templates
|
||||||
|
const app = new Express();
|
||||||
|
const server = new Server(app);
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, '..', 'views'));
|
||||||
|
|
||||||
|
// define the folder that will be used for public assets
|
||||||
|
app.use(Express.static(path.join(__dirname, '..', 'public')));
|
||||||
|
|
||||||
|
// universal routing and rendering
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
match({routes, location: req.url}, (err, redirectLocation, renderProps) => {
|
||||||
|
// Directly send errors
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).send(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate redirects to the browser
|
||||||
|
if (redirectLocation) {
|
||||||
|
return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate the React markup for the current route
|
||||||
|
let markup;
|
||||||
|
if (renderProps) {
|
||||||
|
// if the current route matched, we have renderProps
|
||||||
|
markup = renderToString(<RouterContext {...renderProps} />);
|
||||||
|
} else {
|
||||||
|
markup = renderToString(<NotFoundPage />);
|
||||||
|
res.status(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// render the index template with the embedded React markup
|
||||||
|
return res.render('index', { markup:markup });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// start the server
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
const env = process.env.NODE_ENV || 'production';
|
||||||
|
|
||||||
|
server.listen(port, err => {
|
||||||
|
if (err) {
|
||||||
|
return console.error(err);
|
||||||
|
}
|
||||||
|
console.info(`Server running on http://localhost:${port} [${env}]`);
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Judo Heroes - A Universal Javascript demo application with React</title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>JavaScript is disabled.</noscript>
|
||||||
|
<div id="main"><%- markup -%></div>
|
||||||
|
<script src="/js/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,36 @@
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: path.join(__dirname, 'src', 'app-client.js'),
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'public', 'js'),
|
||||||
|
filename: 'bundle.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [{
|
||||||
|
test: path.join(__dirname, 'src'),
|
||||||
|
loader: ['babel-loader'],
|
||||||
|
query: {
|
||||||
|
cacheDirectory: 'babel_cache',
|
||||||
|
presets: ['react', 'es2015']
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
|
||||||
|
}),
|
||||||
|
new webpack.optimize.DedupePlugin(),
|
||||||
|
new webpack.optimize.OccurrenceOrderPlugin(),
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
compress: {
|
||||||
|
warnings: false
|
||||||
|
},
|
||||||
|
mangle: true,
|
||||||
|
sourcemap: false,
|
||||||
|
beautify: false,
|
||||||
|
dead_code: true
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
};
|