Permalink
Browse files

Initial commit of Node frontend server

  • Loading branch information...
dittos committed Oct 29, 2015
1 parent 7b42bfd commit e0c3ed7a16ccdac21727ebbe70fa27a687118949
@@ -13,3 +13,5 @@ animeta/assets.py
htmlcov
.env
*.db
frontend/assets.json
frontend/config.json
@@ -0,0 +1,4 @@
{
"apiEndpoint": "http://127.0.0.1:8000/api/v2",
"daumAPIKey": ""
}
@@ -0,0 +1,140 @@
import querystring from 'querystring';
import Hapi from 'hapi';
import ejs from 'ejs';
import renderers from './server/renderers';
import Backend, {HttpNotFound} from './server/backend';
import assetFilenames from './assets.json';
import config from './config.json';

const DEBUG = process.env.NODE_ENV !== 'production';

const server = new Hapi.Server();
server.connection({ port: process.env.PORT || 3000 });

server.ext('onPreResponse', (request, reply) => {
const response = request.response;

if (response.isBoom &&
response.output.statusCode === 404 &&
!request.path.match(/\/$/)) {
var url = request.path + '/';
if (request.query) {
url += '?' + querystring.stringify(request.query);
}
return reply().redirect(url);
}
return reply.continue();
});

const backend = new Backend(config.apiEndpoint);

server.start(() => {
console.log('Server running at:', server.info.uri);
});

function wrapHandler(handler) {
return (request, reply) => {
handler.call(null, request, reply).catch(e => {
if (e === HttpNotFound) {
const response = reply('Not found.');
response.statusCode = 404;
return;
}
if (!(e instanceof Error)) {
e = new Error(e);
}
reply(e);
});
};
}

server.route({
method: 'GET',
path: '/works/{title}/',
handler: wrapHandler(async(request, reply) => {
const {title} = request.params;
const [currentUser, work, chart] = await Promise.all([
backend.getCurrentUser(request),
backend.call(request, '/works/_/' + encodeURIComponent(title)),
backend.call(request, '/charts/works/weekly', {limit: 5}),
]);
const preloadData = {
current_user: currentUser,
title,
work,
chart,
daum_api_key: config.daumAPIKey,
};
const html = await renderers.work('/', preloadData);
reply.view('template', {
html,
preloadData,
title,
meta: {
og_url: `/works/${encodeURIComponent(title)}/`,
og_type: 'tv_show',
og_image: work.metadata && work.metadata.image_url,
tw_image: work.metadata && work.metadata.image_url,
},
stylesheets: [`build/${assetFilenames.work.css}`],
scripts: [`build/${assetFilenames.work.js}`],
});
})
});

server.register(require('vision'), err => {
if (err)
throw err;

server.views({
engines: {
html: require('ejs')
},
relativeTo: __dirname,
path: '.',
context: {
DEBUG,
STATIC_URL: '/static/',
assetFilenames,
title: '',
meta: {},
stylesheets: [],
scripts: [],
},
isCached: !DEBUG,
});
});

if (DEBUG) {
server.register(require('inert'), err => {
if (err)
throw err;

server.route({
method: 'GET',
path: '/static/{param*}',
handler: {
directory: {
path: __dirname + '/../animeta/static'
}
}
});
});

server.register(require('h2o2'), err => {
if (err)
throw err;

server.route({
method: '*',
path: '/api/{path*}',
handler: {
proxy: {
host: '127.0.0.1',
port: 8000,
passThrough: true
}
}
});
});
}
@@ -0,0 +1,45 @@
import request from 'request';

export const HttpNotFound = {};

export default class {
constructor(endpoint) {
this.endpoint = endpoint;
}

async call(req, path, params) {
const {response, body} = await this._call(req, path, params);
if (response.statusCode === 404) {
throw HttpNotFound;
}
return JSON.parse(body);
}

async getCurrentUser(req) {
const {response, body} = await this._call(req, '/me');
if (response.statusCode !== 200) {
return null;
}
return JSON.parse(body);
}

_call(req, path, params) {
return new Promise((resolve, reject) => {
request({
baseUrl: this.endpoint,
url: path,
qs: params,
headers: {
'Cookie': req.headers.cookie,
'Host': req.headers.host,
},
}, (err, response, body) => {
if (!err) {
resolve({response, body});
} else {
reject(err);
}
});
});
}
}
@@ -1,50 +1,5 @@
require('moment').locale('ko');

var React = require('react');
var ReactDOMServer = require('react-dom/server');
var createLocation = require('history/lib/createLocation');
var Router = require('react-router');
var workRoutes = require('../js/work.react.js');
var indexRoutes = require('../js/index.react.js');
var PostApp = require('../js/post.react.js');
var http = require('http');

var renderers = {
'/work': createRoutesRenderer(workRoutes, true),
'/index': createRoutesRenderer(indexRoutes),
'/post': createSimpleRenderer(PostApp),
};

function createRoutesRenderer(routes, hashPatch) {
return (path, preloadData) => new Promise((resolve, reject) => {
var location = createLocation(path);

Router.match({routes, location}, (error, redirectLocation, renderProps) => {
if (hashPatch) {
// Monkey patch makeHref to support hash-prefixed link
var createHref = renderProps.history.createHref;
renderProps.history.createHref = function(to, query) {
return '#' + createHref.call(this, to, query);
};
}

global.PreloadData = preloadData;
var markup;
try {
markup = ReactDOMServer.renderToString(<Router.RoutingContext {...renderProps} />);
} finally {
delete global.PreloadData;
}
resolve(markup);
});
});
}

function createSimpleRenderer(Component) {
return (path, preloadData) => Promise.resolve(
ReactDOMServer.renderToString(<Component PreloadData={preloadData} />)
);
}
var renderers = require('./renderers');

module.exports = function() {
return http.createServer((req, res) => {
@@ -55,7 +10,7 @@ module.exports = function() {
req.on('end', async () => {
var view = req.url;
var preloadData = JSON.parse(buf);
var html = await renderers[view]('/', preloadData);
var html = await renderers[view.substring(1)]('/', preloadData);
res.writeHead(200, {'Content-Type': 'text/html'});
res.write(html);
res.end();
@@ -0,0 +1,46 @@
require('moment').locale('ko');

var React = require('react');
var ReactDOMServer = require('react-dom/server');
var createLocation = require('history/lib/createLocation');
var Router = require('react-router');
var workRoutes = require('../js/work.react.js');
var indexRoutes = require('../js/index.react.js');
var PostApp = require('../js/post.react.js');

export default {
work: createRoutesRenderer(workRoutes, true),
index: createRoutesRenderer(indexRoutes),
post: createSimpleRenderer(PostApp),
};

function createRoutesRenderer(routes, hashPatch) {
return (path, preloadData) => new Promise((resolve, reject) => {
var location = createLocation(path);

Router.match({routes, location}, (error, redirectLocation, renderProps) => {
if (hashPatch) {
// Monkey patch makeHref to support hash-prefixed link
var createHref = renderProps.history.createHref;
renderProps.history.createHref = function(to, query) {
return '#' + createHref.call(this, to, query);
};
}

global.PreloadData = preloadData;
var markup;
try {
markup = ReactDOMServer.renderToString(<Router.RoutingContext {...renderProps} />);
} finally {
delete global.PreloadData;
}
resolve(markup);
});
});
}

function createSimpleRenderer(Component) {
return (path, preloadData) => Promise.resolve(
ReactDOMServer.renderToString(<Component PreloadData={preloadData} />)
);
}
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title><%= title %><% if (title) { %> - <% } %>애니메타</title>
<meta name="title" content="<%= title || '애니메타' %>">
<meta name="description"
content="<%= meta.description || '애니메타는 애니메이션, 만화, 드라마 등의 순위를 확인하고 감상 기록을 관리할 수 있는 서비스입니다.' %>">
<meta name="keywords"
content="<%= meta.keywords || '애니메타, 애니메이션, 만화, 드라마, 순위, 감상' %>">

<meta property="og:site_name" content="animeta">
<meta property="og:title" content="<%= title || '애니메타' %>">
<meta property="og:type" content="<%= meta.og_type || 'website' %>">
<meta property="og:url" content="http://animeta.net<%= meta.og_url || '/' %>">
<meta property="og:description" content="<%= meta.description || '애니메타는 애니메이션, 만화, 드라마 등의 순위를 확인하고 감상 기록을 관리할 수 있는 서비스입니다.' %>">
<meta property="og:image"
content="<%= meta.og_image || (STATIC_URL + 'apple-touch-icon-180x.png') %>">
<meta property="fb:app_id" content="153413084690553">

<meta name="twitter:card" content="summary">
<meta name="twitter:url" content="http://animeta.net<%= meta.tw_url || '/' %>">
<meta name="twitter:title" content="<%= title || '애니메타' %>">
<meta name="twitter:description" content="<%= meta.description || '애니메타는 애니메이션, 만화, 드라마 등의 순위를 확인하고 감상 기록을 관리할 수 있는 서비스입니다.' %>">
<meta name="twitter:image:src"
content="<%= meta.tw_image || (STATIC_URL + 'apple-touch-icon-180x.png') %>">

<link rel="apple-touch-icon-precomposed" sizes="120x120" href="<%= STATIC_URL %>apple-touch-icon-120x.png"><!-- iPhone @2x -->
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="<%= STATIC_URL %>apple-touch-icon-152x.png"><!-- iPad @2x -->
<link rel="apple-touch-icon-precomposed" sizes="180x180" href="<%= STATIC_URL %>apple-touch-icon-180x.png"><!-- iPhone @3x -->
<link rel="icon" sizes="196x196" href="<%= STATIC_URL %>touch-icon-192x.png"><!-- Android -->
<link rel="shortcut icon" href="<%= STATIC_URL %>favicon.ico">

<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, target-densitydpi=medium-dpi">
<meta name="apple-mobile-web-app-capable" content="no">
<meta name="google-site-verification" content="dhlhxHlQ_wDcoxu7hcyfWoXOyD79pPryal6fVLUxLeg">
<script type="text/javascript">
var _gaq = [['_setAccount', 'UA-7500828-2'], ['_trackPageview']];
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
<% stylesheets.forEach(path => { %>
<link href="<%= STATIC_URL %><%= path %>" rel="stylesheet">
<% }) %>
</head>
<body>
<script>
if (!document.createElementNS || !document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect)document.body.className+=' no-svg';
</script>
<div id="app"><%- html %></div>
<script src="//cdn.ravenjs.com/1.1.20/raven.min.js"></script>
<% if (!DEBUG) { %>
<script>
Raven.config('http://fb3fd1fc77c94c4799d9fc7228260818@sentry.sapzil.org/3', {
ignoreErrors: ['hideGuidePopup']
}).install();
</script>
<% } %>
<script>
<% if (preloadData) { %>
var PreloadData = <%- JSON.stringify(preloadData) %>;
<% } else { %>
var PreloadData = {};
<% } %>
</script>
<% if (assetFilenames.common) { %>
<script type="text/javascript" src="<%= STATIC_URL %>build/<%= assetFilenames.common.js %>"></script>
<% } %>
<% scripts.forEach(path => { %>
<script type="text/javascript" src="<%= STATIC_URL %><%= path %>"></script>
<% }) %>
</body>
</html>
Oops, something went wrong.

0 comments on commit e0c3ed7

Please sign in to comment.