Merge pull request #360 from lbryio/350-open-graph-react
350 open graph react
This commit is contained in:
commit
cfb94c0add
65 changed files with 873 additions and 25733 deletions
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"extends": "standard",
|
"extends": ["standard", "standard-jsx"],
|
||||||
"env": {
|
"env": {
|
||||||
"es6": true,
|
"es6": true,
|
||||||
"jest": true,
|
"jest": true,
|
||||||
"node": true
|
"node": true,
|
||||||
|
"browser": true
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"GENTLY": true
|
"GENTLY": true
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,3 +2,6 @@ node_modules
|
||||||
.idea
|
.idea
|
||||||
config/sequelizeCliConfig.js
|
config/sequelizeCliConfig.js
|
||||||
config/speechConfig.js
|
config/speechConfig.js
|
||||||
|
public/bundle
|
||||||
|
server.js
|
||||||
|
webpack.config.js
|
||||||
|
|
10
README.md
10
README.md
|
@ -1,5 +1,5 @@
|
||||||
# spee.ch
|
# Spee.ch
|
||||||
spee.ch is a single-serving site that reads and publishes images and videos to and from the [LBRY](https://lbry.io/) blockchain.
|
Spee.ch is a web app that reads and publishes images and videos to and from the [LBRY](https://lbry.io/) blockchain.
|
||||||
|
|
||||||
## how to run this repository locally
|
## how to run this repository locally
|
||||||
* start mysql
|
* start mysql
|
||||||
|
@ -16,9 +16,9 @@ spee.ch is a single-serving site that reads and publishes images and videos to a
|
||||||
* run `npm install`
|
* run `npm install`
|
||||||
* create your `speechConfig.js` file
|
* create your `speechConfig.js` file
|
||||||
* copy `speechConfig.js.example` and name it `speechConfig.js`
|
* copy `speechConfig.js.example` and name it `speechConfig.js`
|
||||||
* replace the `null` values in the config file with the appropriate values for your environement
|
* replace the `null` values in the config file with the appropriate values for your environment
|
||||||
* to start the server, from your command line run `node speech.js`
|
* build the app by running `npm run build-prod`
|
||||||
* To run hot, use `nodemon` instead of `node`
|
* to start the server, run `npm run start`
|
||||||
* visit [localhost:3000](http://localhost:3000)
|
* visit [localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
|
@ -23,8 +23,10 @@ module.exports = {
|
||||||
uploadDirectory: null, // enter file path to where uploads/publishes should be stored
|
uploadDirectory: null, // enter file path to where uploads/publishes should be stored
|
||||||
},
|
},
|
||||||
site: {
|
site: {
|
||||||
|
title: 'Spee.ch',
|
||||||
name : 'Spee.ch',
|
name : 'Spee.ch',
|
||||||
host : 'https://spee.ch',
|
host : 'https://spee.ch',
|
||||||
|
description: 'Open-source, decentralized image and video sharing.'
|
||||||
},
|
},
|
||||||
claim: {
|
claim: {
|
||||||
defaultTitle : 'Spee.ch',
|
defaultTitle : 'Spee.ch',
|
||||||
|
|
45
helpers/handlePageRender.jsx
Normal file
45
helpers/handlePageRender.jsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { renderToString } from 'react-dom/server';
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
import Reducer from '../react/reducers';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { StaticRouter } from 'react-router-dom';
|
||||||
|
import GAListener from '../react/components/GAListener';
|
||||||
|
import App from '../react/app';
|
||||||
|
import renderFullPage from './renderFullPage.js';
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
|
module.exports = (req, res) => {
|
||||||
|
let context = {};
|
||||||
|
|
||||||
|
// create a new Redux store instance
|
||||||
|
const store = createStore(Reducer);
|
||||||
|
|
||||||
|
// render component to a string
|
||||||
|
const html = renderToString(
|
||||||
|
<Provider store={store}>
|
||||||
|
<StaticRouter location={req.url} context={context}>
|
||||||
|
<GAListener>
|
||||||
|
<App />
|
||||||
|
</GAListener>
|
||||||
|
</StaticRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// get head tags from helmet
|
||||||
|
const helmet = Helmet.renderStatic();
|
||||||
|
|
||||||
|
// check for a redirect
|
||||||
|
if (context.url) {
|
||||||
|
// Somewhere a `<Redirect>` was rendered
|
||||||
|
return res.redirect(301, context.url);
|
||||||
|
} else {
|
||||||
|
// we're good, send the response
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the initial state from our Redux store
|
||||||
|
const preloadedState = store.getState();
|
||||||
|
|
||||||
|
// send the rendered page back to the client
|
||||||
|
res.send(renderFullPage(helmet, html, preloadedState));
|
||||||
|
};
|
71
helpers/handleShowRender.jsx
Normal file
71
helpers/handleShowRender.jsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { renderToString } from 'react-dom/server';
|
||||||
|
import { createStore, applyMiddleware } from 'redux';
|
||||||
|
import Reducer from '../react/reducers';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { StaticRouter } from 'react-router-dom';
|
||||||
|
import GAListener from '../react/components/GAListener';
|
||||||
|
import App from '../react/app';
|
||||||
|
import renderFullPage from './renderFullPage';
|
||||||
|
import createSagaMiddleware from 'redux-saga';
|
||||||
|
import { call } from 'redux-saga/effects';
|
||||||
|
import { handleShowPageUri } from '../react/sagas/show_uri';
|
||||||
|
import { onHandleShowPageUri } from '../react/actions/show';
|
||||||
|
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
|
const returnSagaWithParams = (saga, params) => {
|
||||||
|
return function * () {
|
||||||
|
yield call(saga, params);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (req, res) => {
|
||||||
|
let context = {};
|
||||||
|
|
||||||
|
// create and apply middleware
|
||||||
|
const sagaMiddleware = createSagaMiddleware();
|
||||||
|
const middleware = applyMiddleware(sagaMiddleware);
|
||||||
|
|
||||||
|
// create a new Redux store instance
|
||||||
|
const store = createStore(Reducer, middleware);
|
||||||
|
|
||||||
|
// create saga
|
||||||
|
const action = onHandleShowPageUri(req.params);
|
||||||
|
const saga = returnSagaWithParams(handleShowPageUri, action);
|
||||||
|
|
||||||
|
// run the saga middleware
|
||||||
|
sagaMiddleware
|
||||||
|
.run(saga)
|
||||||
|
.done
|
||||||
|
.then(() => {
|
||||||
|
console.log('preload sagas are done');
|
||||||
|
// render component to a string
|
||||||
|
const html = renderToString(
|
||||||
|
<Provider store={store}>
|
||||||
|
<StaticRouter location={req.url} context={context}>
|
||||||
|
<GAListener>
|
||||||
|
<App />
|
||||||
|
</GAListener>
|
||||||
|
</StaticRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// get head tags from helmet
|
||||||
|
const helmet = Helmet.renderStatic();
|
||||||
|
|
||||||
|
// check for a redirect
|
||||||
|
if (context.url) {
|
||||||
|
console.log('REDIRECTING:', context.url);
|
||||||
|
return res.redirect(301, context.url);
|
||||||
|
} else {
|
||||||
|
console.log(`we're good, send the response`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the initial state from our Redux store
|
||||||
|
const preloadedState = store.getState();
|
||||||
|
|
||||||
|
// send the rendered page back to the client
|
||||||
|
res.send(renderFullPage(helmet, html, preloadedState));
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,113 +0,0 @@
|
||||||
const Handlebars = require('handlebars');
|
|
||||||
const { site, claim: claimDefaults } = require('../config/speechConfig.js');
|
|
||||||
|
|
||||||
function determineOgTitle (storedTitle, defaultTitle) {
|
|
||||||
return ifEmptyReturnOther(storedTitle, defaultTitle);
|
|
||||||
};
|
|
||||||
|
|
||||||
function determineOgDescription (storedDescription, defaultDescription) {
|
|
||||||
const length = 200;
|
|
||||||
let description = ifEmptyReturnOther(storedDescription, defaultDescription);
|
|
||||||
if (description.length >= length) {
|
|
||||||
description = `${description.substring(0, length)}...`;
|
|
||||||
};
|
|
||||||
return description;
|
|
||||||
};
|
|
||||||
|
|
||||||
function ifEmptyReturnOther (value, replacement) {
|
|
||||||
if (value === '') {
|
|
||||||
return replacement;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function determineContentTypeFromFileExtension (fileExtension) {
|
|
||||||
switch (fileExtension) {
|
|
||||||
case 'jpeg':
|
|
||||||
case 'jpg':
|
|
||||||
return 'image/jpeg';
|
|
||||||
case 'png':
|
|
||||||
return 'image/png';
|
|
||||||
case 'gif':
|
|
||||||
return 'image/gif';
|
|
||||||
case 'mp4':
|
|
||||||
return 'video/mp4';
|
|
||||||
default:
|
|
||||||
return 'image/jpeg';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function determineOgThumbnailContentType (thumbnail) {
|
|
||||||
if (thumbnail) {
|
|
||||||
if (thumbnail.lastIndexOf('.') !== -1) {
|
|
||||||
return determineContentTypeFromFileExtension(thumbnail.substring(thumbnail.lastIndexOf('.')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOpenGraphDataFromClaim (claim, defaultTitle, defaultDescription) {
|
|
||||||
let openGraphData = {};
|
|
||||||
openGraphData['embedUrl'] = `${site.host}/${claim.claimId}/${claim.name}`;
|
|
||||||
openGraphData['showUrl'] = `${site.host}/${claim.claimId}/${claim.name}`;
|
|
||||||
openGraphData['source'] = `${site.host}/${claim.claimId}/${claim.name}.${claim.fileExt}`;
|
|
||||||
openGraphData['directFileUrl'] = `${site.host}/${claim.claimId}/${claim.name}.${claim.fileExt}`;
|
|
||||||
openGraphData['ogTitle'] = determineOgTitle(claim.title, defaultTitle);
|
|
||||||
openGraphData['ogDescription'] = determineOgDescription(claim.description, defaultDescription);
|
|
||||||
openGraphData['ogThumbnailContentType'] = determineOgThumbnailContentType(claim.thumbnail);
|
|
||||||
return openGraphData;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
placeCommonHeaderTags () {
|
|
||||||
const headerBoilerplate = `<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>${site.title}</title><link rel="stylesheet" href="/assets/css/reset.css" type="text/css"><link rel="stylesheet" href="/assets/css/general.css" type="text/css"><link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">`;
|
|
||||||
return new Handlebars.SafeString(headerBoilerplate);
|
|
||||||
},
|
|
||||||
addOpenGraph (claim) {
|
|
||||||
const { ogTitle, ogDescription, showUrl, source, ogThumbnailContentType } = createOpenGraphDataFromClaim(claim, claimDefaults.defaultTitle, claimDefaults.defaultDescription);
|
|
||||||
const thumbnail = claim.thumbnail;
|
|
||||||
const contentType = claim.contentType;
|
|
||||||
const ogTitleTag = `<meta property="og:title" content="${ogTitle}" />`;
|
|
||||||
const ogUrlTag = `<meta property="og:url" content="${showUrl}" />`;
|
|
||||||
const ogSiteNameTag = `<meta property="og:site_name" content="${site.title}" />`;
|
|
||||||
const ogDescriptionTag = `<meta property="og:description" content="${ogDescription}" />`;
|
|
||||||
const ogImageWidthTag = '<meta property="og:image:width" content="600" />';
|
|
||||||
const ogImageHeightTag = '<meta property="og:image:height" content="315" />';
|
|
||||||
const basicTags = `${ogTitleTag} ${ogUrlTag} ${ogSiteNameTag} ${ogDescriptionTag} ${ogImageWidthTag} ${ogImageHeightTag}`;
|
|
||||||
let ogImageTag = `<meta property="og:image" content="${source}" />`;
|
|
||||||
let ogImageTypeTag = `<meta property="og:image:type" content="${contentType}" />`;
|
|
||||||
let ogTypeTag = `<meta property="og:type" content="article" />`;
|
|
||||||
if (contentType === 'video/mp4') {
|
|
||||||
const ogVideoTag = `<meta property="og:video" content="${source}" />`;
|
|
||||||
const ogVideoSecureUrlTag = `<meta property="og:video:secure_url" content="${source}" />`;
|
|
||||||
const ogVideoTypeTag = `<meta property="og:video:type" content="${contentType}" />`;
|
|
||||||
ogImageTag = `<meta property="og:image" content="${thumbnail}" />`;
|
|
||||||
ogImageTypeTag = `<meta property="og:image:type" content="${ogThumbnailContentType}" />`;
|
|
||||||
ogTypeTag = `<meta property="og:type" content="video" />`;
|
|
||||||
return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag} ${ogVideoTag} ${ogVideoSecureUrlTag} ${ogVideoTypeTag}`);
|
|
||||||
} else {
|
|
||||||
if (contentType === 'image/gif') {
|
|
||||||
ogTypeTag = `<meta property="og:type" content="video.other" />`;
|
|
||||||
};
|
|
||||||
return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addTwitterCard (claim) {
|
|
||||||
const { embedUrl, directFileUrl } = createOpenGraphDataFromClaim(claim, claimDefaults.defaultTitle, claimDefaults.defaultDescription);
|
|
||||||
const basicTwitterTags = `<meta name="twitter:site" content="@spee_ch" >`;
|
|
||||||
const contentType = claim.contentType;
|
|
||||||
if (contentType === 'video/mp4') {
|
|
||||||
const twitterName = '<meta name="twitter:card" content="player" >';
|
|
||||||
const twitterPlayer = `<meta name="twitter:player" content="${embedUrl}" >`;
|
|
||||||
const twitterPlayerWidth = '<meta name="twitter:player:width" content="600" >';
|
|
||||||
const twitterTextPlayerWidth = '<meta name="twitter:text:player_width" content="600" >';
|
|
||||||
const twitterPlayerHeight = '<meta name="twitter:player:height" content="337" >';
|
|
||||||
const twitterPlayerStream = `<meta name="twitter:player:stream" content="${directFileUrl}" >`;
|
|
||||||
const twitterPlayerStreamContentType = '<meta name="twitter:player:stream:content_type" content="video/mp4" >';
|
|
||||||
return new Handlebars.SafeString(`${basicTwitterTags} ${twitterName} ${twitterPlayer} ${twitterPlayerWidth} ${twitterTextPlayerWidth} ${twitterPlayerHeight} ${twitterPlayerStream} ${twitterPlayerStreamContentType}`);
|
|
||||||
} else {
|
|
||||||
const twitterCard = '<meta name="twitter:card" content="summary_large_image" >';
|
|
||||||
return new Handlebars.SafeString(`${basicTwitterTags} ${twitterCard}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
32
helpers/renderFullPage.js
Normal file
32
helpers/renderFullPage.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
module.exports = (helmet, html, preloadedState) => {
|
||||||
|
// take the html and preloadedState and return the full page
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<!--helmet-->
|
||||||
|
${helmet.title.toString()}
|
||||||
|
${helmet.meta.toString()}
|
||||||
|
${helmet.link.toString()}
|
||||||
|
<!--style sheets-->
|
||||||
|
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
|
||||||
|
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
|
||||||
|
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
|
||||||
|
<!--google font-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body id="main-body">
|
||||||
|
<div class="row row--tall flex-container--column">
|
||||||
|
<div id="react-app" class="row row--tall flex-container--column">${html}</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\\u003c')}
|
||||||
|
</script>
|
||||||
|
<script src="/bundle/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
};
|
|
@ -3,11 +3,9 @@ const express = require('express');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const expressHandlebars = require('express-handlebars');
|
const expressHandlebars = require('express-handlebars');
|
||||||
const Handlebars = require('handlebars');
|
const Handlebars = require('handlebars');
|
||||||
const handlebarsHelpers = require('./helpers/handlebarsHelpers.js');
|
|
||||||
const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js');
|
const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js');
|
||||||
const config = require('./config/speechConfig.js');
|
const config = require('./config/speechConfig.js');
|
||||||
const logger = require('winston');
|
const logger = require('winston');
|
||||||
const { getDownloadDirectory } = require('./helpers/lbryApi');
|
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
const PORT = 3000; // set port
|
const PORT = 3000; // set port
|
||||||
const app = express(); // create an Express application
|
const app = express(); // create an Express application
|
||||||
|
@ -54,9 +52,8 @@ app.use(passport.session());
|
||||||
|
|
||||||
// configure handlebars & register it with express app
|
// configure handlebars & register it with express app
|
||||||
const hbs = expressHandlebars.create({
|
const hbs = expressHandlebars.create({
|
||||||
defaultLayout: 'main', // sets the default layout
|
defaultLayout: 'embed', // sets the default layout
|
||||||
handlebars : Handlebars, // includes basic handlebars for access to that library
|
handlebars : Handlebars, // includes basic handlebars for access to that library
|
||||||
helpers : handlebarsHelpers, // custom defined helpers
|
|
||||||
});
|
});
|
||||||
app.engine('handlebars', hbs.engine);
|
app.engine('handlebars', hbs.engine);
|
||||||
app.set('view engine', 'handlebars');
|
app.set('view engine', 'handlebars');
|
||||||
|
@ -67,19 +64,12 @@ app.use(populateLocalsDotUser);
|
||||||
// start the server
|
// start the server
|
||||||
db.sequelize
|
db.sequelize
|
||||||
.sync() // sync sequelize
|
.sync() // sync sequelize
|
||||||
.then(() => { // get the download directory from the daemon
|
.then(() => { // require routes
|
||||||
logger.info('Retrieving daemon download directory...');
|
|
||||||
return getDownloadDirectory();
|
|
||||||
})
|
|
||||||
.then(hostedContentPath => {
|
|
||||||
// add the hosted content folder at a static path
|
|
||||||
app.use('/media', express.static(hostedContentPath));
|
|
||||||
// require routes
|
|
||||||
require('./routes/auth-routes.js')(app);
|
require('./routes/auth-routes.js')(app);
|
||||||
require('./routes/api-routes.js')(app);
|
require('./routes/api-routes.js')(app);
|
||||||
require('./routes/page-routes.js')(app);
|
require('./routes/page-routes.js')(app);
|
||||||
require('./routes/serve-routes.js')(app);
|
require('./routes/serve-routes.js')(app);
|
||||||
require('./routes/home-routes.js')(app);
|
require('./routes/fallback-routes.js')(app);
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
return http.Server(app);
|
return http.Server(app);
|
||||||
})
|
})
|
|
@ -1,7 +1,7 @@
|
||||||
const fs = require('fs');
|
// const fs = require('fs');
|
||||||
const path = require('path');
|
// const path = require('path');
|
||||||
const Sequelize = require('sequelize');
|
const Sequelize = require('sequelize');
|
||||||
const basename = path.basename(module.filename);
|
// const basename = path.basename(module.filename);
|
||||||
const logger = require('winston');
|
const logger = require('winston');
|
||||||
const config = require('../config/speechConfig.js');
|
const config = require('../config/speechConfig.js');
|
||||||
const { database, username, password } = config.sql;
|
const { database, username, password } = config.sql;
|
||||||
|
@ -30,16 +30,19 @@ sequelize
|
||||||
logger.error('Sequelize was unable to connect to the database:', err);
|
logger.error('Sequelize was unable to connect to the database:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// add each model to the db object
|
// manually add each model to the db
|
||||||
fs
|
const Certificate = require('./certificate.js');
|
||||||
.readdirSync(__dirname)
|
const Channel = require('./channel.js');
|
||||||
.filter(file => {
|
const Claim = require('./claim.js');
|
||||||
return (file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js');
|
const File = require('./file.js');
|
||||||
})
|
const Request = require('./request.js');
|
||||||
.forEach(file => {
|
const User = require('./user.js');
|
||||||
const model = sequelize['import'](path.join(__dirname, file));
|
db['Certificate'] = sequelize.import('Certificate', Certificate);
|
||||||
db[model.name] = model;
|
db['Channel'] = sequelize.import('Channel', Channel);
|
||||||
});
|
db['Claim'] = sequelize.import('Claim', Claim);
|
||||||
|
db['File'] = sequelize.import('File', File);
|
||||||
|
db['Request'] = sequelize.import('Request', Request);
|
||||||
|
db['User'] = sequelize.import('User', User);
|
||||||
|
|
||||||
// run model.association for each model in the db object that has an association
|
// run model.association for each model in the db object that has an association
|
||||||
Object.keys(db).forEach(modelName => {
|
Object.keys(db).forEach(modelName => {
|
||||||
|
|
34
package.json
34
package.json
|
@ -6,12 +6,14 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --recursive",
|
"test": "mocha --recursive",
|
||||||
"test-all": "mocha --recursive",
|
"test-all": "mocha --recursive",
|
||||||
"start": "node speech.js",
|
"start": "node server.js",
|
||||||
|
"start-dev": "nodemon server.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"fix": "eslint . --fix",
|
"fix": "eslint . --fix",
|
||||||
"precommit": "eslint .",
|
"precommit": "eslint .",
|
||||||
"babel": "babel",
|
"babel": "babel",
|
||||||
"webpack": "webpack"
|
"build-dev": "webpack --config webpack.dev.js",
|
||||||
|
"build-prod": "webpack --config webpack.prod.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -35,18 +37,20 @@
|
||||||
"config": "^1.26.1",
|
"config": "^1.26.1",
|
||||||
"connect-multiparty": "^2.0.0",
|
"connect-multiparty": "^2.0.0",
|
||||||
"cookie-session": "^2.0.0-beta.3",
|
"cookie-session": "^2.0.0-beta.3",
|
||||||
|
"cross-fetch": "^1.1.1",
|
||||||
"express": "^4.15.2",
|
"express": "^4.15.2",
|
||||||
"express-handlebars": "^3.0.0",
|
"express-handlebars": "^3.0.0",
|
||||||
"form-data": "^2.3.1",
|
"form-data": "^2.3.1",
|
||||||
"helmet": "^3.8.1",
|
"helmet": "^3.8.1",
|
||||||
"mysql2": "^1.3.5",
|
"mysql2": "^1.3.5",
|
||||||
"nodemon": "^1.11.0",
|
"node-fetch": "^2.0.0",
|
||||||
"passport": "^0.4.0",
|
"passport": "^0.4.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"prop-types": "^15.6.0",
|
"prop-types": "^15.6.0",
|
||||||
"react": "^16.2.0",
|
"react": "^16.2.0",
|
||||||
"react-dom": "^16.2.0",
|
"react-dom": "^16.2.0",
|
||||||
"react-ga": "^2.4.1",
|
"react-ga": "^2.4.1",
|
||||||
|
"react-helmet": "^5.2.0",
|
||||||
"react-redux": "^5.0.6",
|
"react-redux": "^5.0.6",
|
||||||
"react-router-dom": "^4.2.2",
|
"react-router-dom": "^4.2.2",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
|
@ -57,6 +61,7 @@
|
||||||
"sequelize-cli": "^3.0.0-3",
|
"sequelize-cli": "^3.0.0-3",
|
||||||
"sleep": "^5.1.1",
|
"sleep": "^5.1.1",
|
||||||
"universal-analytics": "^0.4.13",
|
"universal-analytics": "^0.4.13",
|
||||||
|
"webpack-node-externals": "^1.6.0",
|
||||||
"whatwg-fetch": "^2.0.3",
|
"whatwg-fetch": "^2.0.3",
|
||||||
"winston": "^2.3.1",
|
"winston": "^2.3.1",
|
||||||
"winston-slack-webhook": "billbitt/winston-slack-webhook"
|
"winston-slack-webhook": "billbitt/winston-slack-webhook"
|
||||||
|
@ -64,22 +69,29 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.0",
|
"babel-core": "^6.26.0",
|
||||||
"babel-loader": "^7.1.2",
|
"babel-loader": "^7.1.2",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
"babel-preset-stage-2": "^6.24.1",
|
"babel-preset-stage-2": "^6.24.1",
|
||||||
|
"babel-register": "^6.26.0",
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"chai-http": "^3.0.0",
|
"chai-http": "^3.0.0",
|
||||||
"eslint": "3.19.0",
|
"css-loader": "^0.28.9",
|
||||||
"eslint-config-standard": "10.2.1",
|
"eslint": "4.18.0",
|
||||||
"eslint-plugin-import": "^2.2.0",
|
"eslint-config-standard": "^10.2.1",
|
||||||
"eslint-plugin-node": "^4.2.2",
|
"eslint-config-standard-jsx": "^5.0.0",
|
||||||
"eslint-plugin-promise": "3.5.0",
|
"eslint-plugin-import": "^2.8.0",
|
||||||
"eslint-plugin-react": "6.10.3",
|
"eslint-plugin-node": "^4.2.3",
|
||||||
"eslint-plugin-standard": "3.0.1",
|
"eslint-plugin-promise": "^3.5.0",
|
||||||
|
"eslint-plugin-react": "^7.6.1",
|
||||||
|
"eslint-plugin-standard": "^3.0.1",
|
||||||
"husky": "^0.13.4",
|
"husky": "^0.13.4",
|
||||||
"mocha": "^4.0.1",
|
"mocha": "^4.0.1",
|
||||||
|
"nodemon": "^1.15.1",
|
||||||
"redux-devtools": "^3.4.1",
|
"redux-devtools": "^3.4.1",
|
||||||
"webpack": "^3.10.0"
|
"regenerator-transform": "^0.12.3",
|
||||||
|
"webpack": "^3.10.0",
|
||||||
|
"webpack-merge": "^4.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
25176
public/bundle/bundle.js
25176
public/bundle/bundle.js
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@ import * as actions from 'constants/show_action_types';
|
||||||
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
|
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
|
||||||
|
|
||||||
// basic request parsing
|
// basic request parsing
|
||||||
export function handleShowPageUri (params) {
|
export function onHandleShowPageUri (params) {
|
||||||
return {
|
return {
|
||||||
type: actions.HANDLE_SHOW_URI,
|
type: actions.HANDLE_SHOW_URI,
|
||||||
data: params,
|
data: params,
|
||||||
|
@ -12,7 +12,7 @@ export function handleShowPageUri (params) {
|
||||||
|
|
||||||
export function onRequestError (error) {
|
export function onRequestError (error) {
|
||||||
return {
|
return {
|
||||||
type: actions.REQUEST_UPDATE_ERROR,
|
type: actions.REQUEST_ERROR,
|
||||||
data: error,
|
data: error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -46,6 +46,16 @@ export function onNewAssetRequest (name, id, channelName, channelId, extension)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function onRequestUpdate (requestType, requestId) {
|
||||||
|
return {
|
||||||
|
type: actions.REQUEST_UPDATE,
|
||||||
|
data: {
|
||||||
|
requestType,
|
||||||
|
requestId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function addRequestToRequestList (id, error, key) {
|
export function addRequestToRequestList (id, error, key) {
|
||||||
return {
|
return {
|
||||||
type: actions.REQUEST_LIST_ADD,
|
type: actions.REQUEST_LIST_ADD,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Request from 'utils/request';
|
import Request from 'utils/request';
|
||||||
|
const { site: { host } } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
export function getLongClaimId (name, modifier) {
|
export function getLongClaimId (name, modifier) {
|
||||||
// console.log('getting long claim id for asset:', name, modifier);
|
// console.log('getting long claim id for asset:', name, modifier);
|
||||||
|
@ -15,25 +16,23 @@ export function getLongClaimId (name, modifier) {
|
||||||
body['claimName'] = name;
|
body['claimName'] = name;
|
||||||
const params = {
|
const params = {
|
||||||
method : 'POST',
|
method : 'POST',
|
||||||
headers: new Headers({
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}),
|
|
||||||
body : JSON.stringify(body),
|
body : JSON.stringify(body),
|
||||||
}
|
};
|
||||||
// create url
|
// create url
|
||||||
const url = `/api/claim/long-id`;
|
const url = `${host}/api/claim/long-id`;
|
||||||
// return the request promise
|
// return the request promise
|
||||||
return Request(url, params);
|
return Request(url, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getShortId (name, claimId) {
|
export function getShortId (name, claimId) {
|
||||||
// console.log('getting short id for asset:', name, claimId);
|
// console.log('getting short id for asset:', name, claimId);
|
||||||
const url = `/api/claim/short-id/${claimId}/${name}`;
|
const url = `${host}/api/claim/short-id/${claimId}/${name}`;
|
||||||
return Request(url);
|
return Request(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getClaimData (name, claimId) {
|
export function getClaimData (name, claimId) {
|
||||||
// console.log('getting claim data for asset:', name, claimId);
|
// console.log('getting claim data for asset:', name, claimId);
|
||||||
const url = `/api/claim/data/${name}/${claimId}`;
|
const url = `${host}/api/claim/data/${name}/${claimId}`;
|
||||||
return Request(url);
|
return Request(url);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import Request from 'utils/request';
|
import Request from 'utils/request';
|
||||||
import request from '../utils/request';
|
const { site: { host } } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
export function getChannelData (name, id) {
|
export function getChannelData (name, id) {
|
||||||
console.log('getting channel data for channel:', name, id);
|
console.log('getting channel data for channel:', name, id);
|
||||||
if (!id) id = 'none';
|
if (!id) id = 'none';
|
||||||
const url = `/api/channel/data/${name}/${id}`;
|
const url = `${host}/api/channel/data/${name}/${id}`;
|
||||||
return request(url);
|
return Request(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getChannelClaims (name, longId, page) {
|
export function getChannelClaims (name, longId, page) {
|
||||||
console.log('getting channel claims for channel:', name, longId);
|
console.log('getting channel claims for channel:', name, longId);
|
||||||
if (!page) page = 1;
|
if (!page) page = 1;
|
||||||
const url = `/api/channel/claims/${name}/${longId}/${page}`;
|
const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;
|
||||||
return Request(url);
|
return Request(url);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import Request from 'utils/request';
|
import Request from 'utils/request';
|
||||||
|
const { site: { host } } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
export function checkFileAvailability (name, claimId) {
|
export function checkFileAvailability (name, claimId) {
|
||||||
const url = `/api/file/availability/${name}/${claimId}`;
|
const url = `${host}/api/file/availability/${name}/${claimId}`;
|
||||||
return Request(url);
|
return Request(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function triggerClaimGet (name, claimId) {
|
export function triggerClaimGet (name, claimId) {
|
||||||
const url = `/api/claim/get/${name}/${claimId}`;
|
const url = `${host}/api/claim/get/${name}/${claimId}`;
|
||||||
return Request(url);
|
return Request(url);
|
||||||
}
|
}
|
||||||
|
|
22
react/app.js
Normal file
22
react/app.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
import HomePage from 'components/HomePage';
|
||||||
|
import AboutPage from 'components/AboutPage';
|
||||||
|
import LoginPage from 'containers/LoginPage';
|
||||||
|
import ShowPage from 'containers/ShowPage';
|
||||||
|
import FourOhFourPage from 'components/FourOhFourPage';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route exact path='/' component={HomePage} />
|
||||||
|
<Route exact path='/about' component={AboutPage} />
|
||||||
|
<Route exact path='/login' component={LoginPage} />
|
||||||
|
<Route exact path='/:identifier/:claim' component={ShowPage} />
|
||||||
|
<Route exact path='/:claim' component={ShowPage} />
|
||||||
|
<Route component={FourOhFourPage} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
45
react/client.js
Normal file
45
react/client.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { hydrate } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import Reducer from 'reducers';
|
||||||
|
import createSagaMiddleware from 'redux-saga';
|
||||||
|
import rootSaga from 'sagas';
|
||||||
|
|
||||||
|
import GAListener from 'components/GAListener';
|
||||||
|
import App from './app';
|
||||||
|
|
||||||
|
// get the state from a global variable injected into the server-generated HTML
|
||||||
|
const preloadedState = window.__PRELOADED_STATE__ || null;
|
||||||
|
|
||||||
|
// Allow the passed state to be garbage-collected
|
||||||
|
delete window.__PRELOADED_STATE__;
|
||||||
|
|
||||||
|
// create and apply middleware
|
||||||
|
const sagaMiddleware = createSagaMiddleware();
|
||||||
|
const middleware = applyMiddleware(sagaMiddleware);
|
||||||
|
const reduxMiddleware = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose(middleware, window.__REDUX_DEVTOOLS_EXTENSION__()) : middleware;
|
||||||
|
|
||||||
|
// create teh store
|
||||||
|
let store;
|
||||||
|
if (preloadedState) {
|
||||||
|
store = createStore(Reducer, preloadedState, reduxMiddleware);
|
||||||
|
} else {
|
||||||
|
store = createStore(Reducer, reduxMiddleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the saga middlweare
|
||||||
|
sagaMiddleware.run(rootSaga);
|
||||||
|
|
||||||
|
// render the app
|
||||||
|
hydrate(
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<GAListener>
|
||||||
|
<App />
|
||||||
|
</GAListener>
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('react-app')
|
||||||
|
);
|
|
@ -1,27 +1,29 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import NavBar from 'containers/NavBar';
|
import NavBar from 'containers/NavBar';
|
||||||
|
import SEO from 'components/SEO';
|
||||||
|
|
||||||
class AboutPage extends React.Component {
|
class AboutPage extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEO pageTitle={'About'} pageUri={'about'} />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="row row--padded">
|
<div className='row row--padded'>
|
||||||
<div className="column column--5 column--med-10 align-content-top">
|
<div className='column column--5 column--med-10 align-content-top'>
|
||||||
<div className="column column--8 column--med-10">
|
<div className='column column--8 column--med-10'>
|
||||||
<p className="pull-quote">Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.</p>
|
<p className='pull-quote'>Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.</p>
|
||||||
<p><a className="link--primary" target="_blank" href="https://twitter.com/spee_ch">TWITTER</a></p>
|
<p><a className='link--primary' target='_blank' href='https://twitter.com/spee_ch'>TWITTER</a></p>
|
||||||
<p><a className="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch">GITHUB</a></p>
|
<p><a className='link--primary' target='_blank' href='https://github.com/lbryio/spee.ch'>GITHUB</a></p>
|
||||||
<p><a className="link--primary" target="_blank" href="https://discord.gg/YjYbwhS">DISCORD CHANNEL</a></p>
|
<p><a className='link--primary' target='_blank' href='https://discord.gg/YjYbwhS'>DISCORD CHANNEL</a></p>
|
||||||
<p><a className="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch/blob/master/README.md">DOCUMENTATION</a></p>
|
<p><a className='link--primary' target='_blank' href='https://github.com/lbryio/spee.ch/blob/master/README.md'>DOCUMENTATION</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div><div className="column column--5 column--med-10 align-content-top">
|
</div><div className='column column--5 column--med-10 align-content-top'>
|
||||||
<div className="column column--8 column--med-10">
|
<div className='column column--8 column--med-10'>
|
||||||
<p>Spee.ch is a media-hosting site that reads from and publishes content to the <a className="link--primary" href="https://lbry.io">LBRY</a> blockchain.</p>
|
<p>Spee.ch is a media-hosting site that reads from and publishes content to the <a className='link--primary' href='https://lbry.io'>LBRY</a> blockchain.</p>
|
||||||
<p>Spee.ch is a hosting service, but with the added benefit that it stores your content on a decentralized network of computers -- the LBRY network. This means that your images are stored in multiple locations without a single point of failure.</p>
|
<p>Spee.ch is a hosting service, but with the added benefit that it stores your content on a decentralized network of computers -- the LBRY network. This means that your images are stored in multiple locations without a single point of failure.</p>
|
||||||
<h3>Contribute</h3>
|
<h3>Contribute</h3>
|
||||||
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a className="link--primary" href="https://github.com/lbryio/spee.ch">github repo</a> and go to town!</p>
|
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a className='link--primary' href='https://github.com/lbryio/spee.ch'>github repo</a> and go to town!</p>
|
||||||
<p>If you want to improve spee.ch, join our <a className="link--primary" href="https://discord.gg/YjYbwhS">discord channel</a> or solve one of our <a className="link--primary" href="https://github.com/lbryio/spee.ch/issues">github issues</a>.</p>
|
<p>If you want to improve spee.ch, join our <a className='link--primary' href='https://discord.gg/YjYbwhS'>discord channel</a> or solve one of our <a className='link--primary' href='https://github.com/lbryio/spee.ch/issues'>github issues</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,6 @@ class AssetDisplay extends React.Component {
|
||||||
this.props.onFileRequest(name, claimId);
|
this.props.onFileRequest(name, claimId);
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
console.log('rendering assetdisplay', this.props);
|
|
||||||
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
|
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
|
||||||
return (
|
return (
|
||||||
<div id="asset-display-component">
|
<div id="asset-display-component">
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
|
const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
|
||||||
const directSourceLink = `${claimId}/${name}.${fileExt}`;
|
const directSourceLink = `${claimId}/${name}.${fileExt}`;
|
||||||
const showUrlLink = `${claimId}/${name}`;
|
const showUrlLink = `/${claimId}/${name}`;
|
||||||
return (
|
return (
|
||||||
<div className="asset-holder">
|
<div className='asset-holder'>
|
||||||
<Link to={showUrlLink} >
|
<Link to={showUrlLink} >
|
||||||
{(() => {
|
{(() => {
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ class ErrorPage extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="row row--padded">
|
<div className='row row--padded'>
|
||||||
<p>{error}</p>
|
<p>{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,6 @@ class ErrorPage extends React.Component {
|
||||||
|
|
||||||
ErrorPage.propTypes = {
|
ErrorPage.propTypes = {
|
||||||
error: PropTypes.string.isRequired,
|
error: PropTypes.string.isRequired,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ErrorPage;
|
export default ErrorPage;
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import NavBar from 'containers/NavBar';
|
import NavBar from 'containers/NavBar';
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
const { site: { title, host } } = require('../../../config/speechConfig.js');
|
||||||
|
|
||||||
class FourOhForPage extends React.Component {
|
class FourOhForPage extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Helmet>
|
||||||
|
<title>{title} - 404</title>
|
||||||
|
<link rel='canonical' href={`${host}/404`} />
|
||||||
|
</Helmet>
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="row row--padded">
|
<div className='row row--padded'>
|
||||||
<h2>404</h2>
|
<h2>404</h2>
|
||||||
<p>That page does not exist</p>
|
<p>That page does not exist</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import SEO from 'components/SEO';
|
||||||
import NavBar from 'containers/NavBar';
|
import NavBar from 'containers/NavBar';
|
||||||
import PublishTool from 'containers/PublishTool';
|
import PublishTool from 'containers/PublishTool';
|
||||||
|
|
||||||
class PublishPage extends React.Component {
|
class HomePage extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className={'row row--tall flex-container--column'}>
|
<div className={'row row--tall flex-container--column'}>
|
||||||
|
<SEO />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className={'row row--tall row--padded flex-container--column'}>
|
<div className={'row row--tall row--padded flex-container--column'}>
|
||||||
<PublishTool />
|
<PublishTool />
|
||||||
|
@ -15,4 +17,4 @@ class PublishPage extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PublishPage;
|
export default HomePage;
|
32
react/components/SEO/index.jsx
Normal file
32
react/components/SEO/index.jsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { createPageTitle } from 'utils/pageTitle';
|
||||||
|
import { createMetaTags } from 'utils/metaTags';
|
||||||
|
import { createCanonicalLink } from 'utils/canonicalLink';
|
||||||
|
|
||||||
|
class SEO extends React.Component {
|
||||||
|
render () {
|
||||||
|
let { pageTitle, asset, channel, pageUri } = this.props;
|
||||||
|
pageTitle = createPageTitle(pageTitle);
|
||||||
|
const metaTags = createMetaTags(asset, channel);
|
||||||
|
const canonicalLink = createCanonicalLink(asset, channel, pageUri);
|
||||||
|
return (
|
||||||
|
<Helmet
|
||||||
|
title={pageTitle}
|
||||||
|
meta={metaTags}
|
||||||
|
link={[{rel: 'canonical', href: canonicalLink}]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SEO.propTypes = {
|
||||||
|
pageTitle: PropTypes.string,
|
||||||
|
pageUri : PropTypes.string,
|
||||||
|
channel : PropTypes.object,
|
||||||
|
asset : PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SEO;
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import SEO from 'components/SEO';
|
||||||
import NavBar from 'containers/NavBar';
|
import NavBar from 'containers/NavBar';
|
||||||
import ErrorPage from 'components/ErrorPage';
|
import ErrorPage from 'components/ErrorPage';
|
||||||
import AssetTitle from 'components/AssetTitle';
|
import AssetTitle from 'components/AssetTitle';
|
||||||
|
@ -9,19 +10,21 @@ class ShowAssetDetails extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const { asset } = this.props;
|
const { asset } = this.props;
|
||||||
if (asset) {
|
if (asset) {
|
||||||
|
const { name } = asset.claimData;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEO pageTitle={`${name} - details`} asset={asset} />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="row row--tall row--padded">
|
<div className='row row--tall row--padded'>
|
||||||
<div className="column column--10">
|
<div className='column column--10'>
|
||||||
<AssetTitle />
|
<AssetTitle />
|
||||||
</div>
|
</div>
|
||||||
<div className="column column--5 column--sml-10 align-content-top">
|
<div className='column column--5 column--sml-10 align-content-top'>
|
||||||
<div className="row row--padded">
|
<div className='row row--padded'>
|
||||||
<AssetDisplay />
|
<AssetDisplay />
|
||||||
</div>
|
</div>
|
||||||
</div><div className="column column--5 column--sml-10 align-content-top">
|
</div><div className='column column--5 column--sml-10 align-content-top'>
|
||||||
<div className="row row--padded">
|
<div className='row row--padded'>
|
||||||
<AssetInfo />
|
<AssetInfo />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import SEO from 'components/SEO';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import AssetDisplay from 'components/AssetDisplay';
|
import AssetDisplay from 'components/AssetDisplay';
|
||||||
|
|
||||||
class ShowLite extends React.Component {
|
class ShowLite extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const { asset } = this.props;
|
const { asset } = this.props;
|
||||||
|
if (asset) {
|
||||||
|
const { name, claimId } = asset.claimData;
|
||||||
return (
|
return (
|
||||||
<div className="row row--tall flex-container--column flex-container--center-center">
|
<div className='row row--tall flex-container--column flex-container--center-center'>
|
||||||
{ (asset) &&
|
<SEO pageTitle={name} asset={asset} />
|
||||||
<div>
|
<div>
|
||||||
<AssetDisplay />
|
<AssetDisplay />
|
||||||
<Link id="asset-boilerpate" className="link--primary fine-print" to={`/${asset.claimId}/${asset.name}`}>hosted via Spee.ch</Link>
|
<Link id='asset-boilerpate' className='link--primary fine-print' to={`/${claimId}/${name}`}>hosted
|
||||||
|
via Spee.ch</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</div>
|
return (
|
||||||
|
<p>loading asset data...</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import SEO from 'components/SEO';
|
||||||
import ErrorPage from 'components/ErrorPage';
|
import ErrorPage from 'components/ErrorPage';
|
||||||
import NavBar from 'containers/NavBar';
|
import NavBar from 'containers/NavBar';
|
||||||
import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay';
|
import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay';
|
||||||
|
@ -10,14 +11,15 @@ class ShowChannel extends React.Component {
|
||||||
const { name, longId, shortId } = channel;
|
const { name, longId, shortId } = channel;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEO pageTitle={name} channel={channel} />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="row row--tall row--padded">
|
<div className='row row--tall row--padded'>
|
||||||
<div className="column column--10">
|
<div className='column column--10'>
|
||||||
<h2>channel name: {name || 'loading...'}</h2>
|
<h2>channel name: {name}</h2>
|
||||||
<p className={'fine-print'}>full channel id: {longId || 'loading...'}</p>
|
<p className={'fine-print'}>full channel id: {longId}</p>
|
||||||
<p className={'fine-print'}>short channel id: {shortId || 'loading...'}</p>
|
<p className={'fine-print'}>short channel id: {shortId}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="column column--10">
|
<div className='column column--10'>
|
||||||
<ChannelClaimsDisplay />
|
<ChannelClaimsDisplay />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// request actions
|
// request actions
|
||||||
export const HANDLE_SHOW_URI = 'HANDLE_SHOW_URI';
|
export const HANDLE_SHOW_URI = 'HANDLE_SHOW_URI';
|
||||||
export const REQUEST_UPDATE_ERROR = 'REQUEST_UPDATE_ERROR';
|
export const REQUEST_ERROR = 'REQUEST_ERROR';
|
||||||
|
export const REQUEST_UPDATE = 'REQUEST_UPDATE';
|
||||||
export const ASSET_REQUEST_NEW = 'ASSET_REQUEST_NEW';
|
export const ASSET_REQUEST_NEW = 'ASSET_REQUEST_NEW';
|
||||||
export const CHANNEL_REQUEST_NEW = 'CHANNEL_REQUEST_NEW';
|
export const CHANNEL_REQUEST_NEW = 'CHANNEL_REQUEST_NEW';
|
||||||
export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';
|
export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';
|
||||||
|
|
|
@ -22,6 +22,6 @@ const mapDispatchToProps = dispatch => {
|
||||||
dispatch(updateSelectedChannel(value));
|
dispatch(updateSelectedChannel(value));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import SEO from 'components/SEO';
|
||||||
import NavBar from 'containers/NavBar';
|
import NavBar from 'containers/NavBar';
|
||||||
import ChannelLoginForm from 'containers/ChannelLoginForm';
|
import ChannelLoginForm from 'containers/ChannelLoginForm';
|
||||||
import ChannelCreateForm from 'containers/ChannelCreateForm';
|
import ChannelCreateForm from 'containers/ChannelCreateForm';
|
||||||
|
|
||||||
class PublishPage extends React.Component {
|
class LoginPage extends React.Component {
|
||||||
componentWillReceiveProps (newProps) {
|
componentWillReceiveProps (newProps) {
|
||||||
// re-route the user to the homepage if the user is logged in
|
// re-route the user to the homepage if the user is logged in
|
||||||
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
|
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
|
||||||
|
@ -15,17 +16,18 @@ class PublishPage extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEO pageTitle={'Login'} pageUri={'login'} />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="row row--padded">
|
<div className='row row--padded'>
|
||||||
<div className="column column--5 column--med-10 align-content-top">
|
<div className='column column--5 column--med-10 align-content-top'>
|
||||||
<div className="column column--8 column--med-10">
|
<div className='column column--8 column--med-10'>
|
||||||
<p>Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're <a className="link--primary" target="_blank" href="/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8">documenting important events</a>, or making a public repository for <a className="link--primary" target="_blank" href="/@catGifs">cat gifs</a> (password: '1234'), try creating a channel for it!</p>
|
<p>Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're <a className='link--primary' target='_blank' href='/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8'>documenting important events</a>, or making a public repository for <a className='link--primary' target='_blank' href='/@catGifs'>cat gifs</a> (password: '1234'), try creating a channel for it!</p>
|
||||||
</div>
|
</div>
|
||||||
</div><div className="column column--5 column--med-10 align-content-top">
|
</div><div className='column column--5 column--med-10 align-content-top'>
|
||||||
<div className="column column--8 column--med-10">
|
<div className='column column--8 column--med-10'>
|
||||||
<h3 className="h3--no-bottom">Log in to an existing channel:</h3>
|
<h3 className='h3--no-bottom'>Log in to an existing channel:</h3>
|
||||||
<ChannelLoginForm />
|
<ChannelLoginForm />
|
||||||
<h3 className="h3--no-bottom">Create a brand new channel:</h3>
|
<h3 className='h3--no-bottom'>Create a brand new channel:</h3>
|
||||||
<ChannelCreateForm />
|
<ChannelCreateForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,4 +37,4 @@ class PublishPage extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(PublishPage);
|
export default withRouter(LoginPage);
|
||||||
|
|
|
@ -14,6 +14,6 @@ const mapDispatchToProps = dispatch => {
|
||||||
dispatch(updateMetadata(name, value));
|
dispatch(updateMetadata(name, value));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { handleShowPageUri } from 'actions/show';
|
import { onHandleShowPageUri } from 'actions/show';
|
||||||
import View from './view';
|
import View from './view';
|
||||||
|
|
||||||
const mapStateToProps = ({ show }) => {
|
const mapStateToProps = ({ show }) => {
|
||||||
|
@ -10,7 +10,7 @@ const mapStateToProps = ({ show }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
handleShowPageUri,
|
onHandleShowPageUri,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||||
|
|
|
@ -8,11 +8,11 @@ import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types
|
||||||
|
|
||||||
class ShowPage extends React.Component {
|
class ShowPage extends React.Component {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.props.handleShowPageUri(this.props.match.params);
|
this.props.onHandleShowPageUri(this.props.match.params);
|
||||||
}
|
}
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (nextProps.match.params !== this.props.match.params) {
|
if (nextProps.match.params !== this.props.match.params) {
|
||||||
this.props.handleShowPageUri(nextProps.match.params);
|
this.props.onHandleShowPageUri(nextProps.match.params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
|
||||||
import Reducer from 'reducers';
|
|
||||||
import createSagaMiddleware from 'redux-saga';
|
|
||||||
import rootSaga from 'sagas';
|
|
||||||
import Root from './root';
|
|
||||||
|
|
||||||
const sagaMiddleware = createSagaMiddleware();
|
|
||||||
const middleware = applyMiddleware(sagaMiddleware);
|
|
||||||
|
|
||||||
const enhancer = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose(middleware, window.__REDUX_DEVTOOLS_EXTENSION__()) : middleware;
|
|
||||||
|
|
||||||
let store = createStore(
|
|
||||||
Reducer,
|
|
||||||
enhancer,
|
|
||||||
);
|
|
||||||
|
|
||||||
sagaMiddleware.run(rootSaga);
|
|
||||||
|
|
||||||
render(
|
|
||||||
<Root store={store} />,
|
|
||||||
document.getElementById('react-app')
|
|
||||||
);
|
|
|
@ -19,14 +19,13 @@ const initialState = {
|
||||||
export default function (state = initialState, action) {
|
export default function (state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
// handle request
|
// handle request
|
||||||
case actions.REQUEST_UPDATE_ERROR:
|
case actions.REQUEST_ERROR:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
request: Object.assign({}, state.request, {
|
request: Object.assign({}, state.request, {
|
||||||
error: action.data,
|
error: action.data,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
case actions.CHANNEL_REQUEST_NEW:
|
case actions.REQUEST_UPDATE:
|
||||||
case actions.ASSET_REQUEST_NEW:
|
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
request: Object.assign({}, state.request, {
|
request: Object.assign({}, state.request, {
|
||||||
type: action.data.requestType,
|
type: action.data.requestType,
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import { BrowserRouter, Route, Switch } from 'react-router-dom';
|
|
||||||
|
|
||||||
import GAListener from 'components/GAListener';
|
|
||||||
import PublishPage from 'components/PublishPage';
|
|
||||||
import AboutPage from 'components/AboutPage';
|
|
||||||
import LoginPage from 'containers/LoginPage';
|
|
||||||
import ShowPage from 'containers/ShowPage';
|
|
||||||
import FourOhFourPage from 'components/FourOhFourPage';
|
|
||||||
|
|
||||||
const Root = ({ store }) => (
|
|
||||||
<Provider store={store}>
|
|
||||||
<BrowserRouter>
|
|
||||||
<GAListener>
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/" component={PublishPage} />
|
|
||||||
<Route exact path="/about" component={AboutPage} />
|
|
||||||
<Route exact path="/login" component={LoginPage} />
|
|
||||||
<Route exact path="/:identifier/:claim" component={ShowPage} />
|
|
||||||
<Route exact path="/:claim" component={ShowPage} />
|
|
||||||
<Route component={FourOhFourPage} />
|
|
||||||
</Switch>
|
|
||||||
</GAListener>
|
|
||||||
</BrowserRouter>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
Root.propTypes = {
|
|
||||||
store: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Root;
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
||||||
import * as actions from 'constants/show_action_types';
|
import * as actions from 'constants/show_action_types';
|
||||||
import { addRequestToRequestList, onRequestError, addAssetToAssetList } from 'actions/show';
|
import { addRequestToRequestList, onRequestError, onRequestUpdate, addAssetToAssetList } from 'actions/show';
|
||||||
import { getLongClaimId, getShortId, getClaimData } from 'api/assetApi';
|
import { getLongClaimId, getShortId, getClaimData } from 'api/assetApi';
|
||||||
import { selectShowState } from 'selectors/show';
|
import { selectShowState } from 'selectors/show';
|
||||||
|
|
||||||
function* newAssetRequest (action) {
|
export function * newAssetRequest (action) {
|
||||||
const { requestId, name, modifier } = action.data;
|
const { requestType, requestId, name, modifier } = action.data;
|
||||||
const state = yield select(selectShowState);
|
// put an action to update the request in redux
|
||||||
|
yield put(onRequestUpdate(requestType, requestId));
|
||||||
// is this an existing request?
|
// is this an existing request?
|
||||||
// If this uri is in the request list, it's already been fetched
|
// If this uri is in the request list, it's already been fetched
|
||||||
|
const state = yield select(selectShowState);
|
||||||
if (state.requestList[requestId]) {
|
if (state.requestList[requestId]) {
|
||||||
console.log('that request already exists in the request list!');
|
console.log('that request already exists in the request list!');
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import {call, put, select, takeLatest} from 'redux-saga/effects';
|
import {call, put, select, takeLatest} from 'redux-saga/effects';
|
||||||
import * as actions from 'constants/show_action_types';
|
import * as actions from 'constants/show_action_types';
|
||||||
import { addNewChannelToChannelList, addRequestToRequestList, onRequestError, updateChannelClaims } from 'actions/show';
|
import { addNewChannelToChannelList, addRequestToRequestList, onRequestError, onRequestUpdate, updateChannelClaims } from 'actions/show';
|
||||||
import { getChannelClaims, getChannelData } from 'api/channelApi';
|
import { getChannelClaims, getChannelData } from 'api/channelApi';
|
||||||
import { selectShowState } from 'selectors/show';
|
import { selectShowState } from 'selectors/show';
|
||||||
|
|
||||||
function* getNewChannelAndUpdateChannelList (action) {
|
export function * newChannelRequest (action) {
|
||||||
const { requestId, channelName, channelId } = action.data;
|
const { requestType, requestId, channelName, channelId } = action.data;
|
||||||
const state = yield select(selectShowState);
|
// put an action to update the request in redux
|
||||||
|
yield put(onRequestUpdate(requestType, requestId));
|
||||||
// is this an existing request?
|
// is this an existing request?
|
||||||
// If this uri is in the request list, it's already been fetched
|
// If this uri is in the request list, it's already been fetched
|
||||||
|
const state = yield select(selectShowState);
|
||||||
if (state.requestList[requestId]) {
|
if (state.requestList[requestId]) {
|
||||||
console.log('that request already exists in the request list!');
|
console.log('that request already exists in the request list!');
|
||||||
return null;
|
return null;
|
||||||
|
@ -45,7 +47,7 @@ function* getNewChannelAndUpdateChannelList (action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function * watchNewChannelRequest () {
|
export function * watchNewChannelRequest () {
|
||||||
yield takeLatest(actions.CHANNEL_REQUEST_NEW, getNewChannelAndUpdateChannelList);
|
yield takeLatest(actions.CHANNEL_REQUEST_NEW, newChannelRequest);
|
||||||
};
|
};
|
||||||
|
|
||||||
function * getNewClaimsAndUpdateChannel (action) {
|
function * getNewClaimsAndUpdateChannel (action) {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { call, put, takeLatest } from 'redux-saga/effects';
|
import { call, put, takeLatest } from 'redux-saga/effects';
|
||||||
import * as actions from 'constants/show_action_types';
|
import * as actions from 'constants/show_action_types';
|
||||||
import { onRequestError, onNewChannelRequest, onNewAssetRequest } from 'actions/show';
|
import { onRequestError, onNewChannelRequest, onNewAssetRequest } from 'actions/show';
|
||||||
|
import { newAssetRequest } from 'sagas/show_asset';
|
||||||
|
import { newChannelRequest } from 'sagas/show_channel';
|
||||||
import lbryUri from 'utils/lbryUri';
|
import lbryUri from 'utils/lbryUri';
|
||||||
|
|
||||||
function * parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
function * parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
||||||
|
@ -17,9 +19,9 @@ function* parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
||||||
}
|
}
|
||||||
// trigger an new action to update the store
|
// trigger an new action to update the store
|
||||||
if (isChannel) {
|
if (isChannel) {
|
||||||
return yield put(onNewAssetRequest(claimName, null, channelName, channelClaimId, extension));
|
return yield call(newAssetRequest, onNewAssetRequest(claimName, null, channelName, channelClaimId, extension));
|
||||||
};
|
};
|
||||||
yield put(onNewAssetRequest(claimName, claimId, null, null, extension));
|
yield call(newAssetRequest, onNewAssetRequest(claimName, claimId, null, null, extension));
|
||||||
}
|
}
|
||||||
function * parseAndUpdateClaimOnly (claim) {
|
function * parseAndUpdateClaimOnly (claim) {
|
||||||
console.log('parseAndUpdateIdentifierAndClaim');
|
console.log('parseAndUpdateIdentifierAndClaim');
|
||||||
|
@ -34,7 +36,7 @@ function* parseAndUpdateClaimOnly (claim) {
|
||||||
// trigger an new action to update the store
|
// trigger an new action to update the store
|
||||||
// return early if this request is for a channel
|
// return early if this request is for a channel
|
||||||
if (isChannel) {
|
if (isChannel) {
|
||||||
return yield put(onNewChannelRequest(channelName, channelClaimId));
|
return yield call(newChannelRequest, onNewChannelRequest(channelName, channelClaimId));
|
||||||
}
|
}
|
||||||
// if not for a channel, parse the claim request
|
// if not for a channel, parse the claim request
|
||||||
let claimName, extension;
|
let claimName, extension;
|
||||||
|
@ -43,10 +45,10 @@ function* parseAndUpdateClaimOnly (claim) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return yield put(onRequestError(error.message));
|
return yield put(onRequestError(error.message));
|
||||||
}
|
}
|
||||||
yield put(onNewAssetRequest(claimName, null, null, null, extension));
|
yield call(newAssetRequest, onNewAssetRequest(claimName, null, null, null, extension));
|
||||||
}
|
}
|
||||||
|
|
||||||
function* handleShowPageUri (action) {
|
export function * handleShowPageUri (action) {
|
||||||
console.log('handleShowPageUri');
|
console.log('handleShowPageUri');
|
||||||
const { identifier, claim } = action.data;
|
const { identifier, claim } = action.data;
|
||||||
if (identifier) {
|
if (identifier) {
|
||||||
|
|
37
react/utils/canonicalLink.js
Normal file
37
react/utils/canonicalLink.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
const { site: { host } } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
|
const createBasicCanonicalLink = (page) => {
|
||||||
|
if (!page) {
|
||||||
|
return `${host}`;
|
||||||
|
};
|
||||||
|
return `${host}/${page}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAssetCanonicalLink = (asset) => {
|
||||||
|
let channelName, certificateId, name, claimId;
|
||||||
|
if (asset.claimData) {
|
||||||
|
({ channelName, certificateId, name, claimId } = asset.claimData);
|
||||||
|
};
|
||||||
|
if (channelName) {
|
||||||
|
return `${host}/${channelName}:${certificateId}/${name}`;
|
||||||
|
};
|
||||||
|
return `${host}/${claimId}/${name}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createChannelCanonicalLink = (channel) => {
|
||||||
|
const { name, longId } = channel;
|
||||||
|
return `${host}/${name}:${longId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCanonicalLink = (asset, channel, page) => {
|
||||||
|
if (asset) {
|
||||||
|
return createAssetCanonicalLink(asset);
|
||||||
|
}
|
||||||
|
if (channel) {
|
||||||
|
return createChannelCanonicalLink(channel);
|
||||||
|
}
|
||||||
|
if (page) {
|
||||||
|
return createBasicCanonicalLink(page);
|
||||||
|
}
|
||||||
|
return createBasicCanonicalLink();
|
||||||
|
};
|
|
@ -35,4 +35,4 @@ module.exports = {
|
||||||
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
|
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ module.exports = {
|
||||||
'([^:$#/]*)' + // value (stops at the first separator or end)
|
'([^:$#/]*)' + // value (stops at the first separator or end)
|
||||||
'([:$#]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end)
|
'([:$#]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end)
|
||||||
);
|
);
|
||||||
const [proto, value, modifierSeperator, modifier] = componentsRegex
|
const [proto, value, modifierSeperator, modifier] = componentsRegex // eslint-disable-line no-unused-vars
|
||||||
.exec(identifier)
|
.exec(identifier)
|
||||||
.map(match => match || null);
|
.map(match => match || null);
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ module.exports = {
|
||||||
'([^:$#/.]*)' + // name (stops at the first extension)
|
'([^:$#/.]*)' + // name (stops at the first extension)
|
||||||
'([:$#.]?)([^/]*)' // extension separator, extension (stops at the first path separator or end)
|
'([:$#.]?)([^/]*)' // extension separator, extension (stops at the first path separator or end)
|
||||||
);
|
);
|
||||||
const [proto, claimName, extensionSeperator, extension] = componentsRegex
|
const [proto, claimName, extensionSeperator, extension] = componentsRegex // eslint-disable-line no-unused-vars
|
||||||
.exec(name)
|
.exec(name)
|
||||||
.map(match => match || null);
|
.map(match => match || null);
|
||||||
|
|
||||||
|
|
96
react/utils/metaTags.js
Normal file
96
react/utils/metaTags.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
const { site: { title, host, description }, claim: { defaultThumbnail, defaultDescription } } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
|
const determineOgThumbnailContentType = (thumbnail) => {
|
||||||
|
if (thumbnail) {
|
||||||
|
const fileExt = thumbnail.substring(thumbnail.lastIndexOf('.'));
|
||||||
|
switch (fileExt) {
|
||||||
|
case 'jpeg':
|
||||||
|
case 'jpg':
|
||||||
|
return 'image/jpeg';
|
||||||
|
case 'png':
|
||||||
|
return 'image/png';
|
||||||
|
case 'gif':
|
||||||
|
return 'image/gif';
|
||||||
|
case 'mp4':
|
||||||
|
return 'video/mp4';
|
||||||
|
default:
|
||||||
|
return 'image/jpeg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBasicMetaTags = () => {
|
||||||
|
return [
|
||||||
|
{property: 'og:title', content: title},
|
||||||
|
{property: 'og:url', content: host},
|
||||||
|
{property: 'og:site_name', content: title},
|
||||||
|
{property: 'og:description', content: description},
|
||||||
|
{property: 'twitter:site', content: '@spee_ch'},
|
||||||
|
{property: 'twitter:card', content: 'summary'},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const createChannelMetaTags = (channel) => {
|
||||||
|
const { name, longId } = channel;
|
||||||
|
return [
|
||||||
|
{property: 'og:title', content: `${name} on ${title}`},
|
||||||
|
{property: 'og:url', content: `${host}/${name}:${longId}`},
|
||||||
|
{property: 'og:site_name', content: title},
|
||||||
|
{property: 'og:description', content: `${name}, a channel on ${title}`},
|
||||||
|
{property: 'twitter:site', content: '@spee_ch'},
|
||||||
|
{property: 'twitter:card', content: 'summary'},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAssetMetaTags = (asset) => {
|
||||||
|
const { claimData } = asset;
|
||||||
|
const { contentType } = claimData;
|
||||||
|
const embedUrl = `${host}/${claimData.claimId}/${claimData.name}`;
|
||||||
|
const showUrl = `${host}/${claimData.claimId}/${claimData.name}`;
|
||||||
|
const source = `${host}/${claimData.claimId}/${claimData.name}.${claimData.fileExt}`;
|
||||||
|
const ogTitle = claimData.title || claimData.name;
|
||||||
|
const ogDescription = claimData.description || defaultDescription;
|
||||||
|
const ogThumbnailContentType = determineOgThumbnailContentType(claimData.thumbnail);
|
||||||
|
const ogThumbnail = claimData.thumbnail || defaultThumbnail;
|
||||||
|
const metaTags = [
|
||||||
|
{property: 'og:title', content: ogTitle},
|
||||||
|
{property: 'og:url', content: showUrl},
|
||||||
|
{property: 'og:site_name', content: title},
|
||||||
|
{property: 'og:description', content: ogDescription},
|
||||||
|
{property: 'og:image:width', content: 600},
|
||||||
|
{property: 'og:image:height', content: 315},
|
||||||
|
{property: 'twitter:site', content: '@spee_ch'},
|
||||||
|
];
|
||||||
|
if (contentType === 'video/mp4' || contentType === 'video/webm') {
|
||||||
|
metaTags.push({property: 'og:video', content: source});
|
||||||
|
metaTags.push({property: 'og:video:secure_url', content: source});
|
||||||
|
metaTags.push({property: 'og:video:type', content: contentType});
|
||||||
|
metaTags.push({property: 'og:image', content: ogThumbnail});
|
||||||
|
metaTags.push({property: 'og:image:type', content: ogThumbnailContentType});
|
||||||
|
metaTags.push({property: 'og:type', content: 'video'});
|
||||||
|
metaTags.push({property: 'twitter:card', content: 'player'});
|
||||||
|
metaTags.push({property: 'twitter:player', content: embedUrl});
|
||||||
|
metaTags.push({property: 'twitter:player:width', content: 600});
|
||||||
|
metaTags.push({property: 'twitter:text:player_width', content: 600});
|
||||||
|
metaTags.push({property: 'twitter:player:height', content: 337});
|
||||||
|
metaTags.push({property: 'twitter:player:stream', content: source});
|
||||||
|
metaTags.push({property: 'twitter:player:stream:content_type', content: contentType});
|
||||||
|
} else {
|
||||||
|
metaTags.push({property: 'og:image', content: source});
|
||||||
|
metaTags.push({property: 'og:image:type', content: contentType});
|
||||||
|
metaTags.push({property: 'og:type', content: 'article'});
|
||||||
|
metaTags.push({property: 'twitter:card', content: 'summary_large_image'});
|
||||||
|
}
|
||||||
|
return metaTags;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMetaTags = (asset, channel) => {
|
||||||
|
if (asset) {
|
||||||
|
return createAssetMetaTags(asset);
|
||||||
|
};
|
||||||
|
if (channel) {
|
||||||
|
return createChannelMetaTags(channel);
|
||||||
|
};
|
||||||
|
return createBasicMetaTags();
|
||||||
|
};
|
8
react/utils/pageTitle.js
Normal file
8
react/utils/pageTitle.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
const { site: { title: siteTitle } } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
|
export const createPageTitle = (pageTitle) => {
|
||||||
|
if (!pageTitle) {
|
||||||
|
return `${siteTitle}`;
|
||||||
|
}
|
||||||
|
return `${siteTitle} - ${pageTitle}`;
|
||||||
|
};
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'cross-fetch/polyfill';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the JSON returned by a network request
|
* Parses the JSON returned by a network request
|
||||||
*
|
*
|
||||||
|
|
9
routes/fallback-routes.js
Normal file
9
routes/fallback-routes.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
const handlePageRender = require('../helpers/handlePageRender.jsx');
|
||||||
|
|
||||||
|
module.exports = app => {
|
||||||
|
// a catch-all route if someone visits a page that does not exist
|
||||||
|
app.use('*', (req, res) => {
|
||||||
|
// send response
|
||||||
|
handlePageRender(req, res);
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,11 +0,0 @@
|
||||||
module.exports = app => {
|
|
||||||
// route for the home page
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.status(200).render('index');
|
|
||||||
});
|
|
||||||
// a catch-all route if someone visits a page that does not exist
|
|
||||||
app.use('*', ({ originalUrl, ip }, res) => {
|
|
||||||
// send response
|
|
||||||
res.status(404).render('404');
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,24 +1,29 @@
|
||||||
const { site } = require('../config/speechConfig.js');
|
const { site } = require('../config/speechConfig.js');
|
||||||
|
const handlePageRender = require('../helpers/handlePageRender.jsx');
|
||||||
|
|
||||||
module.exports = (app) => {
|
module.exports = (app) => {
|
||||||
|
// route for the home page
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
handlePageRender(req, res);
|
||||||
|
});
|
||||||
// route to display login page
|
// route to display login page
|
||||||
app.get('/login', (req, res) => {
|
app.get('/login', (req, res) => {
|
||||||
res.status(200).render('index');
|
handlePageRender(req, res);
|
||||||
});
|
});
|
||||||
// route to show 'about' page
|
// route to show 'about' page
|
||||||
app.get('/about', (req, res) => {
|
app.get('/about', (req, res) => {
|
||||||
res.status(200).render('index');
|
handlePageRender(req, res);
|
||||||
});
|
});
|
||||||
// route to display a list of the trending images
|
// route to display a list of the trending images
|
||||||
app.get('/trending', (req, res) => {
|
app.get('/trending', (req, res) => {
|
||||||
res.status(301).redirect('/popular');
|
res.status(301).redirect('/popular');
|
||||||
});
|
});
|
||||||
app.get('/popular', ({ ip, originalUrl }, res) => {
|
app.get('/popular', (req, res) => {
|
||||||
res.status(200).render('index');
|
handlePageRender(req, res);
|
||||||
});
|
});
|
||||||
// route to display a list of the trending images
|
// route to display a list of the trending images
|
||||||
app.get('/new', ({ ip, originalUrl }, res) => {
|
app.get('/new', (req, res) => {
|
||||||
res.status(200).render('index');
|
handlePageRender(req, res);
|
||||||
});
|
});
|
||||||
// route to send embedable video player (for twitter)
|
// route to send embedable video player (for twitter)
|
||||||
app.get('/embed/:claimId/:name', ({ params }, res) => {
|
app.get('/embed/:claimId/:name', ({ params }, res) => {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
const { sendGAServeEvent } = require('../helpers/googleAnalytics');
|
const { sendGAServeEvent } = require('../helpers/googleAnalytics');
|
||||||
|
|
||||||
const { determineResponseType, flipClaimNameAndIdForBackwardsCompatibility, logRequestData, getClaimIdAndServeAsset } = require('../helpers/serveHelpers.js');
|
const { determineResponseType, flipClaimNameAndIdForBackwardsCompatibility, logRequestData, getClaimIdAndServeAsset } = require('../helpers/serveHelpers.js');
|
||||||
const lbryUri = require('../helpers/lbryUri.js');
|
const lbryUri = require('../helpers/lbryUri.js');
|
||||||
|
const handleShowRender = require('../helpers/handleShowRender.jsx');
|
||||||
const SERVE = 'SERVE';
|
const SERVE = 'SERVE';
|
||||||
|
|
||||||
module.exports = (app) => {
|
module.exports = (app) => {
|
||||||
// route to serve a specific asset using the channel or claim id
|
// route to serve a specific asset using the channel or claim id
|
||||||
app.get('/:identifier/:claim', ({ headers, ip, originalUrl, params }, res) => {
|
app.get('/:identifier/:claim', (req, res) => {
|
||||||
|
const { headers, ip, originalUrl, params } = req;
|
||||||
// decide if this is a show request
|
// decide if this is a show request
|
||||||
let hasFileExtension;
|
let hasFileExtension;
|
||||||
try {
|
try {
|
||||||
|
@ -17,7 +17,7 @@ module.exports = (app) => {
|
||||||
}
|
}
|
||||||
let responseType = determineResponseType(hasFileExtension, headers);
|
let responseType = determineResponseType(hasFileExtension, headers);
|
||||||
if (responseType !== SERVE) {
|
if (responseType !== SERVE) {
|
||||||
return res.status(200).render('index');
|
return handleShowRender(req, res);
|
||||||
}
|
}
|
||||||
// handle serve request
|
// handle serve request
|
||||||
// send google analytics
|
// send google analytics
|
||||||
|
@ -45,7 +45,8 @@ module.exports = (app) => {
|
||||||
getClaimIdAndServeAsset(channelName, channelClaimId, claimName, claimId, originalUrl, ip, res);
|
getClaimIdAndServeAsset(channelName, channelClaimId, claimName, claimId, originalUrl, ip, res);
|
||||||
});
|
});
|
||||||
// route to serve the winning asset at a claim or a channel page
|
// route to serve the winning asset at a claim or a channel page
|
||||||
app.get('/:claim', ({ headers, ip, originalUrl, params, query }, res) => {
|
app.get('/:claim', (req, res) => {
|
||||||
|
const { headers, ip, originalUrl, params } = req;
|
||||||
// decide if this is a show request
|
// decide if this is a show request
|
||||||
let hasFileExtension;
|
let hasFileExtension;
|
||||||
try {
|
try {
|
||||||
|
@ -55,7 +56,7 @@ module.exports = (app) => {
|
||||||
}
|
}
|
||||||
let responseType = determineResponseType(hasFileExtension, headers);
|
let responseType = determineResponseType(hasFileExtension, headers);
|
||||||
if (responseType !== SERVE) {
|
if (responseType !== SERVE) {
|
||||||
return res.status(200).render('index');
|
return handleShowRender(req, res);
|
||||||
}
|
}
|
||||||
// handle serve request
|
// handle serve request
|
||||||
// send google analytics
|
// send google analytics
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
<div class="row row--tall flex-container--column flex-container--center-center">
|
|
||||||
<h3>404: Not Found</h3>
|
|
||||||
<p>That page does not exist. Return <a class="link--primary" href="/">home</a>.</p>
|
|
||||||
</div>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<div class="row row--tall flex-container--column">
|
|
||||||
<div id="react-app" class="row row--tall flex-container--column">
|
|
||||||
<div class="row row--padded row--tall flex-container--column flex-container--center-center">
|
|
||||||
<p>loading...</p>
|
|
||||||
{{> progressBar}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/bundle/bundle.js"></script>
|
|
|
@ -1,19 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
|
|
||||||
<head>
|
|
||||||
{{ placeCommonHeaderTags }}
|
|
||||||
<meta name="twitter:card" content="summary" />
|
|
||||||
<meta name="twitter:site" content="@spee_ch" />
|
|
||||||
<meta property="og:title" content="Spee.ch" />
|
|
||||||
<meta property="og:site_name" content="Spee.ch" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:image" content="https://spee.ch/assets/img/Speech_Logo_Main@OG-02.jpg" />
|
|
||||||
<meta property="og:url" content="http://spee.ch/" />
|
|
||||||
<meta property="og:description" content="Open-source, decentralized image and video sharing." />
|
|
||||||
<!--google font-->
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body id="main-body">
|
|
||||||
{{{ body }}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,14 +1,14 @@
|
||||||
const Path = require('path');
|
const Path = require('path');
|
||||||
|
|
||||||
const REACT_ROOT = Path.resolve(__dirname, 'react/');
|
const REACT_ROOT = Path.resolve(__dirname, 'react/');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry : ['babel-polyfill', 'whatwg-fetch', './react/index.js'],
|
target: 'web',
|
||||||
|
entry : ['babel-polyfill', 'whatwg-fetch', './react/client.js'],
|
||||||
output: {
|
output: {
|
||||||
path : Path.join(__dirname, '/public/bundle/'),
|
path : Path.join(__dirname, 'public/bundle/'),
|
||||||
|
publicPath: 'public/bundle/',
|
||||||
filename : 'bundle.js',
|
filename : 'bundle.js',
|
||||||
},
|
},
|
||||||
watch : true,
|
|
||||||
module: {
|
module: {
|
||||||
loaders: [
|
loaders: [
|
||||||
{
|
{
|
13
webpack.dev.js
Normal file
13
webpack.dev.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
const serverBaseConfig = require('./webpack.server.common.js');
|
||||||
|
const clientBaseConfig = require('./webpack.client.common.js');
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
|
||||||
|
const devBuildConfig = {
|
||||||
|
watch : true,
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
merge(serverBaseConfig, devBuildConfig),
|
||||||
|
merge(clientBaseConfig, devBuildConfig),
|
||||||
|
];
|
22
webpack.prod.js
Normal file
22
webpack.prod.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||||
|
const serverBaseConfig = require('./webpack.server.common.js');
|
||||||
|
const clientBaseConfig = require('./webpack.client.common.js');
|
||||||
|
|
||||||
|
const productionBuildConfig = {
|
||||||
|
devtool: 'source-map',
|
||||||
|
plugins: [
|
||||||
|
new UglifyJSPlugin({
|
||||||
|
sourceMap: true,
|
||||||
|
}),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
merge(serverBaseConfig, productionBuildConfig),
|
||||||
|
merge(clientBaseConfig, productionBuildConfig),
|
||||||
|
];
|
41
webpack.server.common.js
Normal file
41
webpack.server.common.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
const Path = require('path');
|
||||||
|
const nodeExternals = require('webpack-node-externals');
|
||||||
|
const REACT_ROOT = Path.resolve(__dirname, 'react/');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
target: 'node',
|
||||||
|
node : {
|
||||||
|
__dirname: false,
|
||||||
|
},
|
||||||
|
externals: [nodeExternals()],
|
||||||
|
entry : ['babel-polyfill', 'whatwg-fetch', './index.js'],
|
||||||
|
output : {
|
||||||
|
path : Path.join(__dirname, '/'),
|
||||||
|
publicPath: '/',
|
||||||
|
filename : 'server.js',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test : /.jsx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader : 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: ['es2015', 'react', 'stage-2'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test : /.css$/,
|
||||||
|
loader: 'css-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
modules: [
|
||||||
|
REACT_ROOT,
|
||||||
|
'node_modules',
|
||||||
|
__dirname,
|
||||||
|
],
|
||||||
|
extensions: ['.js', '.json', '.jsx', '.css'],
|
||||||
|
},
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue