350 open graph react #360
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"extends": "standard",
|
||||
"extends": ["standard", "standard-jsx"],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"jest": true,
|
||||
"node": true
|
||||
"node": true,
|
||||
"browser": true
|
||||
},
|
||||
"globals": {
|
||||
"GENTLY": true
|
||||
|
|
5
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
node_modules
|
||||
.idea
|
||||
config/sequelizeCliConfig.js
|
||||
config/speechConfig.js
|
||||
config/speechConfig.js
|
||||
public/bundle
|
||||
server.js
|
||||
webpack.config.js
|
||||
|
|
10
README.md
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
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
|
||||
* 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`
|
||||
* create your `speechConfig.js` file
|
||||
* copy `speechConfig.js.example` and name it `speechConfig.js`
|
||||
* replace the `null` values in the config file with the appropriate values for your environement
|
||||
* to start the server, from your command line run `node speech.js`
|
||||
* To run hot, use `nodemon` instead of `node`
|
||||
* replace the `null` values in the config file with the appropriate values for your environment
|
||||
* build the app by running `npm run build-prod`
|
||||
* to start the server, run `npm run start`
|
||||
* visit [localhost:3000](http://localhost:3000)
|
||||
|
||||
## Tests
|
||||
|
|
|
@ -7,31 +7,31 @@ module.exports = {
|
|||
const userName = channelName.substring(1);
|
||||
logger.debug(`authenticateChannelCredentials > channelName: ${channelName} username: ${userName} pass: ${userPassword}`);
|
||||
db.User
|
||||
.findOne({where: { userName }})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
logger.debug('no user found');
|
||||
.findOne({where: { userName }})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
logger.debug('no user found');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
return user.comparePassword(userPassword, (passwordErr, isMatch) => {
|
||||
if (passwordErr) {
|
||||
logger.error('comparePassword error:', passwordErr);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
return user.comparePassword(userPassword, (passwordErr, isMatch) => {
|
||||
if (passwordErr) {
|
||||
logger.error('comparePassword error:', passwordErr);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
if (!isMatch) {
|
||||
logger.debug('incorrect password');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
logger.debug('...password was a match...');
|
||||
resolve(true);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
if (!isMatch) {
|
||||
logger.debug('incorrect password');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
logger.debug('...password was a match...');
|
||||
resolve(true);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
authenticateIfNoUserToken (channelName, channelPassword, user) {
|
||||
|
|
|
@ -23,8 +23,10 @@ module.exports = {
|
|||
uploadDirectory: null, // enter file path to where uploads/publishes should be stored
|
||||
},
|
||||
site: {
|
||||
name: 'Spee.ch',
|
||||
host: 'https://spee.ch',
|
||||
title: 'Spee.ch',
|
||||
name : 'Spee.ch',
|
||||
host : 'https://spee.ch',
|
||||
description: 'Open-source, decentralized image and video sharing.'
|
||||
},
|
||||
claim: {
|
||||
defaultTitle : 'Spee.ch',
|
||||
|
|
|
@ -10,80 +10,80 @@ module.exports = {
|
|||
let publishResults, certificateId, channelName;
|
||||
// publish the file
|
||||
return lbryApi.publishClaim(publishParams)
|
||||
.then(tx => {
|
||||
logger.info(`Successfully published ${publishParams.name} ${fileName}`, tx);
|
||||
publishResults = tx;
|
||||
// get the channel information
|
||||
if (publishParams.channel_name) {
|
||||
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
|
||||
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
|
||||
} else {
|
||||
logger.debug('this claim was not published in a channel');
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.then(channel => {
|
||||
.then(tx => {
|
||||
logger.info(`Successfully published ${publishParams.name} ${fileName}`, tx);
|
||||
publishResults = tx;
|
||||
// get the channel information
|
||||
if (publishParams.channel_name) {
|
||||
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
|
||||
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
|
||||
} else {
|
||||
logger.debug('this claim was not published in a channel');
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.then(channel => {
|
||||
// set channel information
|
||||
certificateId = null;
|
||||
channelName = null;
|
||||
if (channel) {
|
||||
certificateId = channel.channelClaimId;
|
||||
channelName = channel.channelName;
|
||||
}
|
||||
logger.debug(`certificateId: ${certificateId}`);
|
||||
})
|
||||
.then(() => {
|
||||
certificateId = null;
|
||||
channelName = null;
|
||||
if (channel) {
|
||||
certificateId = channel.channelClaimId;
|
||||
channelName = channel.channelName;
|
||||
}
|
||||
logger.debug(`certificateId: ${certificateId}`);
|
||||
})
|
||||
.then(() => {
|
||||
// create the File record
|
||||
const fileRecord = {
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
title : publishParams.metadata.title,
|
||||
description: publishParams.metadata.description,
|
||||
address : publishParams.claim_address,
|
||||
outpoint : `${publishResults.txid}:${publishResults.nout}`,
|
||||
height : 0,
|
||||
fileName,
|
||||
filePath : publishParams.file_path,
|
||||
fileType,
|
||||
nsfw : publishParams.metadata.nsfw,
|
||||
};
|
||||
// create the Claim record
|
||||
const claimRecord = {
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
title : publishParams.metadata.title,
|
||||
description: publishParams.metadata.description,
|
||||
address : publishParams.claim_address,
|
||||
thumbnail : publishParams.metadata.thumbnail,
|
||||
outpoint : `${publishResults.txid}:${publishResults.nout}`,
|
||||
height : 0,
|
||||
contentType: fileType,
|
||||
nsfw : publishParams.metadata.nsfw,
|
||||
amount : publishParams.bid,
|
||||
certificateId,
|
||||
channelName,
|
||||
};
|
||||
// upsert criteria
|
||||
const upsertCriteria = {
|
||||
name : publishParams.name,
|
||||
claimId: publishResults.claim_id,
|
||||
};
|
||||
// upsert the records
|
||||
return Promise.all([db.upsert(db.File, fileRecord, upsertCriteria, 'File'), db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim')]);
|
||||
})
|
||||
.then(([file, claim]) => {
|
||||
logger.debug('File and Claim records successfully created');
|
||||
return Promise.all([file.setClaim(claim), claim.setFile(file)]);
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug('File and Claim records successfully associated');
|
||||
resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim;
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('PUBLISH ERROR', error);
|
||||
publishHelpers.deleteTemporaryFile(publishParams.file_path); // delete the local file
|
||||
reject(error);
|
||||
});
|
||||
const fileRecord = {
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
title : publishParams.metadata.title,
|
||||
description: publishParams.metadata.description,
|
||||
address : publishParams.claim_address,
|
||||
outpoint : `${publishResults.txid}:${publishResults.nout}`,
|
||||
height : 0,
|
||||
fileName,
|
||||
filePath : publishParams.file_path,
|
||||
fileType,
|
||||
nsfw : publishParams.metadata.nsfw,
|
||||
};
|
||||
// create the Claim record
|
||||
const claimRecord = {
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
title : publishParams.metadata.title,
|
||||
description: publishParams.metadata.description,
|
||||
address : publishParams.claim_address,
|
||||
thumbnail : publishParams.metadata.thumbnail,
|
||||
outpoint : `${publishResults.txid}:${publishResults.nout}`,
|
||||
height : 0,
|
||||
contentType: fileType,
|
||||
nsfw : publishParams.metadata.nsfw,
|
||||
amount : publishParams.bid,
|
||||
certificateId,
|
||||
channelName,
|
||||
};
|
||||
// upsert criteria
|
||||
const upsertCriteria = {
|
||||
name : publishParams.name,
|
||||
claimId: publishResults.claim_id,
|
||||
};
|
||||
// upsert the records
|
||||
return Promise.all([db.upsert(db.File, fileRecord, upsertCriteria, 'File'), db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim')]);
|
||||
})
|
||||
.then(([file, claim]) => {
|
||||
logger.debug('File and Claim records successfully created');
|
||||
return Promise.all([file.setClaim(claim), claim.setFile(file)]);
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug('File and Claim records successfully associated');
|
||||
resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim;
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('PUBLISH ERROR', error);
|
||||
publishHelpers.deleteTemporaryFile(publishParams.file_path); // delete the local file
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
checkClaimNameAvailability (name) {
|
||||
|
|
|
@ -6,11 +6,11 @@ module.exports = function () {
|
|||
|
||||
for (let configCategoryKey in config) {
|
||||
if (config.hasOwnProperty(configCategoryKey)) {
|
||||
// get the final variables for each config category
|
||||
// get the final variables for each config category
|
||||
const configVariables = config[configCategoryKey];
|
||||
for (let configVarKey in configVariables) {
|
||||
if (configVariables.hasOwnProperty(configVarKey)) {
|
||||
// print each variable
|
||||
// print each variable
|
||||
logger.debug(`CONFIG CHECK: ${configCategoryKey}.${configVarKey} === ${configVariables[configVarKey]}`);
|
||||
}
|
||||
}
|
||||
|
|
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
|
@ -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
|
@ -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 expressHandlebars = require('express-handlebars');
|
||||
const Handlebars = require('handlebars');
|
||||
const handlebarsHelpers = require('./helpers/handlebarsHelpers.js');
|
||||
const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js');
|
||||
const config = require('./config/speechConfig.js');
|
||||
const logger = require('winston');
|
||||
const { getDownloadDirectory } = require('./helpers/lbryApi');
|
||||
const helmet = require('helmet');
|
||||
const PORT = 3000; // set port
|
||||
const app = express(); // create an Express application
|
||||
|
@ -54,9 +52,8 @@ app.use(passport.session());
|
|||
|
||||
// configure handlebars & register it with express app
|
||||
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
|
||||
helpers : handlebarsHelpers, // custom defined helpers
|
||||
});
|
||||
app.engine('handlebars', hbs.engine);
|
||||
app.set('view engine', 'handlebars');
|
||||
|
@ -67,19 +64,12 @@ app.use(populateLocalsDotUser);
|
|||
// start the server
|
||||
db.sequelize
|
||||
.sync() // sync sequelize
|
||||
.then(() => { // get the download directory from the daemon
|
||||
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
|
||||
.then(() => { // require routes
|
||||
require('./routes/auth-routes.js')(app);
|
||||
require('./routes/api-routes.js')(app);
|
||||
require('./routes/page-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');
|
||||
return http.Server(app);
|
||||
})
|
|
@ -176,15 +176,15 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
this.findOne({
|
||||
where: {name, claimId},
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -318,15 +318,15 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
this.findOne({
|
||||
where: {name, claimId},
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
// const fs = require('fs');
|
||||
// const path = require('path');
|
||||
const Sequelize = require('sequelize');
|
||||
const basename = path.basename(module.filename);
|
||||
// const basename = path.basename(module.filename);
|
||||
const logger = require('winston');
|
||||
const config = require('../config/speechConfig.js');
|
||||
const { database, username, password } = config.sql;
|
||||
|
@ -30,16 +30,19 @@ sequelize
|
|||
logger.error('Sequelize was unable to connect to the database:', err);
|
||||
});
|
||||
|
||||
// add each model to the db object
|
||||
fs
|
||||
.readdirSync(__dirname)
|
||||
.filter(file => {
|
||||
return (file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js');
|
||||
})
|
||||
.forEach(file => {
|
||||
const model = sequelize['import'](path.join(__dirname, file));
|
||||
db[model.name] = model;
|
||||
});
|
||||
// manually add each model to the db
|
||||
const Certificate = require('./certificate.js');
|
||||
const Channel = require('./channel.js');
|
||||
const Claim = require('./claim.js');
|
||||
const File = require('./file.js');
|
||||
const Request = require('./request.js');
|
||||
const User = require('./user.js');
|
||||
db['Certificate'] = sequelize.import('Certificate', Certificate);
|
||||
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
|
||||
Object.keys(db).forEach(modelName => {
|
||||
|
|
34
package.json
|
@ -6,12 +6,14 @@
|
|||
"scripts": {
|
||||
"test": "mocha --recursive",
|
||||
"test-all": "mocha --recursive",
|
||||
"start": "node speech.js",
|
||||
"start": "node server.js",
|
||||
"start-dev": "nodemon server.js",
|
||||
"lint": "eslint .",
|
||||
"fix": "eslint . --fix",
|
||||
"precommit": "eslint .",
|
||||
"babel": "babel",
|
||||
"webpack": "webpack"
|
||||
"build-dev": "webpack --config webpack.dev.js",
|
||||
"build-prod": "webpack --config webpack.prod.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -35,18 +37,20 @@
|
|||
"config": "^1.26.1",
|
||||
"connect-multiparty": "^2.0.0",
|
||||
"cookie-session": "^2.0.0-beta.3",
|
||||
"cross-fetch": "^1.1.1",
|
||||
"express": "^4.15.2",
|
||||
"express-handlebars": "^3.0.0",
|
||||
"form-data": "^2.3.1",
|
||||
"helmet": "^3.8.1",
|
||||
"mysql2": "^1.3.5",
|
||||
"nodemon": "^1.11.0",
|
||||
"node-fetch": "^2.0.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-ga": "^2.4.1",
|
||||
"react-helmet": "^5.2.0",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"redux": "^3.7.2",
|
||||
|
@ -57,6 +61,7 @@
|
|||
"sequelize-cli": "^3.0.0-3",
|
||||
"sleep": "^5.1.1",
|
||||
"universal-analytics": "^0.4.13",
|
||||
"webpack-node-externals": "^1.6.0",
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
"winston": "^2.3.1",
|
||||
"winston-slack-webhook": "billbitt/winston-slack-webhook"
|
||||
|
@ -64,22 +69,29 @@
|
|||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-http": "^3.0.0",
|
||||
"eslint": "3.19.0",
|
||||
"eslint-config-standard": "10.2.1",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"eslint-plugin-node": "^4.2.2",
|
||||
"eslint-plugin-promise": "3.5.0",
|
||||
"eslint-plugin-react": "6.10.3",
|
||||
"eslint-plugin-standard": "3.0.1",
|
||||
"css-loader": "^0.28.9",
|
||||
"eslint": "4.18.0",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-config-standard-jsx": "^5.0.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^4.2.3",
|
||||
"eslint-plugin-promise": "^3.5.0",
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"husky": "^0.13.4",
|
||||
"mocha": "^4.0.1",
|
||||
"nodemon": "^1.15.1",
|
||||
"redux-devtools": "^3.4.1",
|
||||
"webpack": "^3.10.0"
|
||||
"regenerator-transform": "^0.12.3",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-merge": "^4.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,19 +8,19 @@ function returnUserAndChannelInfo (userInstance) {
|
|||
userInfo['id'] = userInstance.id;
|
||||
userInfo['userName'] = userInstance.userName;
|
||||
userInstance
|
||||
.getChannel()
|
||||
.then(({channelName, channelClaimId}) => {
|
||||
userInfo['channelName'] = channelName;
|
||||
userInfo['channelClaimId'] = channelClaimId;
|
||||
return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName);
|
||||
})
|
||||
.then(shortChannelId => {
|
||||
userInfo['shortChannelId'] = shortChannelId;
|
||||
resolve(userInfo);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
.getChannel()
|
||||
.then(({channelName, channelClaimId}) => {
|
||||
userInfo['channelName'] = channelName;
|
||||
userInfo['channelClaimId'] = channelClaimId;
|
||||
return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName);
|
||||
})
|
||||
.then(shortChannelId => {
|
||||
userInfo['shortChannelId'] = shortChannelId;
|
||||
resolve(userInfo);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -32,34 +32,34 @@ module.exports = new PassportLocalStrategy(
|
|||
(username, password, done) => {
|
||||
logger.debug('logging user in');
|
||||
return db
|
||||
.User
|
||||
.findOne({where: {userName: username}})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
// logger.debug('no user found');
|
||||
.User
|
||||
.findOne({where: {userName: username}})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
// logger.debug('no user found');
|
||||
return done(null, false, {message: 'Incorrect username or password'});
|
||||
}
|
||||
user.comparePassword(password, (passwordErr, isMatch) => {
|
||||
if (passwordErr) {
|
||||
logger.error('passwordErr:', passwordErr);
|
||||
return done(null, false, {message: passwordErr});
|
||||
}
|
||||
if (!isMatch) {
|
||||
// logger.debug('incorrect password');
|
||||
return done(null, false, {message: 'Incorrect username or password'});
|
||||
}
|
||||
user.comparePassword(password, (passwordErr, isMatch) => {
|
||||
if (passwordErr) {
|
||||
logger.error('passwordErr:', passwordErr);
|
||||
return done(null, false, {message: passwordErr});
|
||||
}
|
||||
if (!isMatch) {
|
||||
// logger.debug('incorrect password');
|
||||
return done(null, false, {message: 'Incorrect username or password'});
|
||||
}
|
||||
logger.debug('Password was a match, returning User');
|
||||
return returnUserAndChannelInfo(user)
|
||||
.then((userInfo) => {
|
||||
return done(null, userInfo);
|
||||
})
|
||||
.catch(error => {
|
||||
return done(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
return done(error);
|
||||
logger.debug('Password was a match, returning User');
|
||||
return returnUserAndChannelInfo(user)
|
||||
.then((userInfo) => {
|
||||
return done(null, userInfo);
|
||||
})
|
||||
.catch(error => {
|
||||
return done(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
return done(error);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
25176
public/bundle/bundle.js
|
@ -3,7 +3,7 @@ import * as actions from 'constants/show_action_types';
|
|||
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
|
||||
|
||||
// basic request parsing
|
||||
export function handleShowPageUri (params) {
|
||||
export function onHandleShowPageUri (params) {
|
||||
return {
|
||||
type: actions.HANDLE_SHOW_URI,
|
||||
data: params,
|
||||
|
@ -12,7 +12,7 @@ export function handleShowPageUri (params) {
|
|||
|
||||
export function onRequestError (error) {
|
||||
return {
|
||||
type: actions.REQUEST_UPDATE_ERROR,
|
||||
type: actions.REQUEST_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) {
|
||||
return {
|
||||
type: actions.REQUEST_LIST_ADD,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Request from 'utils/request';
|
||||
const { site: { host } } = require('../../config/speechConfig.js');
|
||||
|
||||
export function getLongClaimId (name, modifier) {
|
||||
// console.log('getting long claim id for asset:', name, modifier);
|
||||
|
@ -15,25 +16,23 @@ export function getLongClaimId (name, modifier) {
|
|||
body['claimName'] = name;
|
||||
const params = {
|
||||
method : 'POST',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body : JSON.stringify(body),
|
||||
};
|
||||
// create url
|
||||
const url = `/api/claim/long-id`;
|
||||
const url = `${host}/api/claim/long-id`;
|
||||
// return the request promise
|
||||
return Request(url, params);
|
||||
};
|
||||
|
||||
export function getShortId (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);
|
||||
};
|
||||
|
||||
export function getClaimData (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);
|
||||
};
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import Request from 'utils/request';
|
||||
import request from '../utils/request';
|
||||
const { site: { host } } = require('../../config/speechConfig.js');
|
||||
|
||||
export function getChannelData (name, id) {
|
||||
console.log('getting channel data for channel:', name, id);
|
||||
if (!id) id = 'none';
|
||||
const url = `/api/channel/data/${name}/${id}`;
|
||||
return request(url);
|
||||
const url = `${host}/api/channel/data/${name}/${id}`;
|
||||
return Request(url);
|
||||
};
|
||||
|
||||
export function getChannelClaims (name, longId, page) {
|
||||
console.log('getting channel claims for channel:', name, longId);
|
||||
if (!page) page = 1;
|
||||
const url = `/api/channel/claims/${name}/${longId}/${page}`;
|
||||
const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;
|
||||
return Request(url);
|
||||
};
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import Request from 'utils/request';
|
||||
const { site: { host } } = require('../../config/speechConfig.js');
|
||||
|
||||
export function checkFileAvailability (name, claimId) {
|
||||
const url = `/api/file/availability/${name}/${claimId}`;
|
||||
const url = `${host}/api/file/availability/${name}/${claimId}`;
|
||||
return Request(url);
|
||||
}
|
||||
|
||||
export function triggerClaimGet (name, claimId) {
|
||||
const url = `/api/claim/get/${name}/${claimId}`;
|
||||
const url = `${host}/api/claim/get/${name}/${claimId}`;
|
||||
return Request(url);
|
||||
}
|
||||
|
|
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
|
@ -0,0 +1,45 @@
|
|||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import React from 'react';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import { hydrate } from 'react-dom';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import { Provider } from 'react-redux';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import Reducer from 'reducers';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import rootSaga from 'sagas';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import GAListener from 'components/GAListener';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
import App from './app';
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
// get the state from a global variable injected into the server-generated HTML
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
const preloadedState = window.__PRELOADED_STATE__ || null;
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
// Allow the passed state to be garbage-collected
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
delete window.__PRELOADED_STATE__;
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
// create and apply middleware
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
const middleware = applyMiddleware(sagaMiddleware);
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
const reduxMiddleware = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose(middleware, window.__REDUX_DEVTOOLS_EXTENSION__()) : middleware;
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
// create teh store
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
let store;
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
if (preloadedState) {
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
store = createStore(Reducer, preloadedState, reduxMiddleware);
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
} else {
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
store = createStore(Reducer, reduxMiddleware);
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
}
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
// run the saga middlweare
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
sagaMiddleware.run(rootSaga);
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
// render the app
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
hydrate(
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
<Provider store={store}>
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
<BrowserRouter>
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
<GAListener>
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
<App />
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
</GAListener>
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
</BrowserRouter>
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
</Provider>,
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
document.getElementById('react-app')
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
||||
);
|
||||
Probably don't need these Probably don't need these `console.log`s here.
|
|
@ -1,27 +1,29 @@
|
|||
import React from 'react';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import SEO from 'components/SEO';
|
||||
|
||||
class AboutPage extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<div className="column column--5 column--med-10 align-content-top">
|
||||
<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><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://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>
|
||||
<SEO pageTitle={'About'} pageUri={'about'} />
|
||||
<NavBar />
|
||||
<div className='row row--padded'>
|
||||
<div className='column column--5 column--med-10 align-content-top'>
|
||||
<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><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://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>
|
||||
</div>
|
||||
</div><div className="column column--5 column--med-10 align-content-top">
|
||||
<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>
|
||||
</div><div className='column column--5 column--med-10 align-content-top'>
|
||||
<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 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>
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,6 @@ class AssetDisplay extends React.Component {
|
|||
this.props.onFileRequest(name, claimId);
|
||||
}
|
||||
render () {
|
||||
console.log('rendering assetdisplay', this.props);
|
||||
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
|
||||
return (
|
||||
<div id="asset-display-component">
|
||||
|
|
|
@ -3,9 +3,9 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
|
||||
const directSourceLink = `${claimId}/${name}.${fileExt}`;
|
||||
const showUrlLink = `${claimId}/${name}`;
|
||||
const showUrlLink = `/${claimId}/${name}`;
|
||||
return (
|
||||
<div className="asset-holder">
|
||||
<div className='asset-holder'>
|
||||
<Link to={showUrlLink} >
|
||||
{(() => {
|
||||
switch (contentType) {
|
||||
|
@ -13,16 +13,16 @@ const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
|
|||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
return (
|
||||
<img className={'asset-preview'} src={directSourceLink} alt={name}/>
|
||||
<img className={'asset-preview'} src={directSourceLink} alt={name} />
|
||||
);
|
||||
case 'image/gif':
|
||||
return (
|
||||
<img className={'asset-preview'} src={directSourceLink} alt={name}/>
|
||||
<img className={'asset-preview'} src={directSourceLink} alt={name} />
|
||||
);
|
||||
case 'video/mp4':
|
||||
return (
|
||||
<video className={'asset-preview'}>
|
||||
<source src={directSourceLink} type={contentType}/>
|
||||
<source src={directSourceLink} type={contentType} />
|
||||
</video>
|
||||
);
|
||||
default:
|
||||
|
|
|
@ -7,9 +7,9 @@ class ErrorPage extends React.Component {
|
|||
const { error } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<p>{error}</p>
|
||||
<NavBar />
|
||||
<div className='row row--padded'>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -18,6 +18,6 @@ class ErrorPage extends React.Component {
|
|||
|
||||
ErrorPage.propTypes = {
|
||||
error: PropTypes.string.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
export default ErrorPage;
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import React from 'react';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import Helmet from 'react-helmet';
|
||||
const { site: { title, host } } = require('../../../config/speechConfig.js');
|
||||
|
||||
class FourOhForPage extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<h2>404</h2>
|
||||
<p>That page does not exist</p>
|
||||
<Helmet>
|
||||
<title>{title} - 404</title>
|
||||
<link rel='canonical' href={`${host}/404`} />
|
||||
</Helmet>
|
||||
<NavBar />
|
||||
<div className='row row--padded'>
|
||||
<h2>404</h2>
|
||||
<p>That page does not exist</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import React from 'react';
|
||||
import SEO from 'components/SEO';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import PublishTool from 'containers/PublishTool';
|
||||
|
||||
class PublishPage extends React.Component {
|
||||
class HomePage extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className={'row row--tall flex-container--column'}>
|
||||
<NavBar/>
|
||||
<SEO />
|
||||
<NavBar />
|
||||
<div className={'row row--tall row--padded flex-container--column'}>
|
||||
<PublishTool/>
|
||||
<PublishTool />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default PublishPage;
|
||||
export default HomePage;
|
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 SEO from 'components/SEO';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import ErrorPage from 'components/ErrorPage';
|
||||
import AssetTitle from 'components/AssetTitle';
|
||||
|
@ -9,29 +10,31 @@ class ShowAssetDetails extends React.Component {
|
|||
render () {
|
||||
const { asset } = this.props;
|
||||
if (asset) {
|
||||
const { name } = asset.claimData;
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--tall row--padded">
|
||||
<div className="column column--10">
|
||||
<SEO pageTitle={`${name} - details`} asset={asset} />
|
||||
<NavBar />
|
||||
<div className='row row--tall row--padded'>
|
||||
<div className='column column--10'>
|
||||
<AssetTitle />
|
||||
</div>
|
||||
<div className="column column--5 column--sml-10 align-content-top">
|
||||
<div className="row row--padded">
|
||||
<div className='column column--5 column--sml-10 align-content-top'>
|
||||
<div className='row row--padded'>
|
||||
<AssetDisplay />
|
||||
</div>
|
||||
</div><div className="column column--5 column--sml-10 align-content-top">
|
||||
<div className="row row--padded">
|
||||
<AssetInfo />
|
||||
</div><div className='column column--5 column--sml-10 align-content-top'>
|
||||
<div className='row row--padded'>
|
||||
<AssetInfo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ErrorPage error={'loading asset data...'}/>
|
||||
<ErrorPage error={'loading asset data...'} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
import React from 'react';
|
||||
import SEO from 'components/SEO';
|
||||
import { Link } from 'react-router-dom';
|
||||
import AssetDisplay from 'components/AssetDisplay';
|
||||
|
||||
class ShowLite extends React.Component {
|
||||
render () {
|
||||
const { asset } = this.props;
|
||||
return (
|
||||
<div className="row row--tall flex-container--column flex-container--center-center">
|
||||
{ (asset) &&
|
||||
<div>
|
||||
<AssetDisplay />
|
||||
<Link id="asset-boilerpate" className="link--primary fine-print" to={`/${asset.claimId}/${asset.name}`}>hosted via Spee.ch</Link>
|
||||
if (asset) {
|
||||
const { name, claimId } = asset.claimData;
|
||||
return (
|
||||
<div className='row row--tall flex-container--column flex-container--center-center'>
|
||||
<SEO pageTitle={name} asset={asset} />
|
||||
<div>
|
||||
<AssetDisplay />
|
||||
<Link id='asset-boilerpate' className='link--primary fine-print' to={`/${claimId}/${name}`}>hosted
|
||||
via Spee.ch</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p>loading asset data...</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import SEO from 'components/SEO';
|
||||
import ErrorPage from 'components/ErrorPage';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay';
|
||||
|
@ -10,14 +11,15 @@ class ShowChannel extends React.Component {
|
|||
const { name, longId, shortId } = channel;
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--tall row--padded">
|
||||
<div className="column column--10">
|
||||
<h2>channel name: {name || 'loading...'}</h2>
|
||||
<p className={'fine-print'}>full channel id: {longId || 'loading...'}</p>
|
||||
<p className={'fine-print'}>short channel id: {shortId || 'loading...'}</p>
|
||||
<SEO pageTitle={name} channel={channel} />
|
||||
<NavBar />
|
||||
<div className='row row--tall row--padded'>
|
||||
<div className='column column--10'>
|
||||
<h2>channel name: {name}</h2>
|
||||
<p className={'fine-print'}>full channel id: {longId}</p>
|
||||
<p className={'fine-print'}>short channel id: {shortId}</p>
|
||||
</div>
|
||||
<div className="column column--10">
|
||||
<div className='column column--10'>
|
||||
<ChannelClaimsDisplay />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,7 +27,7 @@ class ShowChannel extends React.Component {
|
|||
);
|
||||
};
|
||||
return (
|
||||
<ErrorPage error={'loading channel data...'}/>
|
||||
<ErrorPage error={'loading channel data...'} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// request actions
|
||||
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 CHANNEL_REQUEST_NEW = 'CHANNEL_REQUEST_NEW';
|
||||
export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';
|
||||
|
|
|
@ -22,6 +22,6 @@ const mapDispatchToProps = dispatch => {
|
|||
dispatch(updateSelectedChannel(value));
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import SEO from 'components/SEO';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import ChannelLoginForm from 'containers/ChannelLoginForm';
|
||||
import ChannelCreateForm from 'containers/ChannelCreateForm';
|
||||
|
||||
class PublishPage extends React.Component {
|
||||
class LoginPage extends React.Component {
|
||||
componentWillReceiveProps (newProps) {
|
||||
// re-route the user to the homepage if the user is logged in
|
||||
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
|
||||
|
@ -15,24 +16,25 @@ class PublishPage extends React.Component {
|
|||
render () {
|
||||
You can move all of these utility functions into the You can move all of these utility functions into the `SEO` component. Just pass in the minimum values needed and create the actual title/link/tags inside the component.
Then you don't need to add the same three lines to every page component. Then in the component you can check if
Then you don't need to add the same three lines to every page component.
`<SEO title="Login" link="login" />`
or
`<SEO title="Channel" link="channel" channel={channel} />`
Then in the component you can check if `channel` or `asset` was passed in and build the meta tags accordingly.
```
if (!props.asset || !props.channel) {
metaTags = createBasicMetaTags();
}
```
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<div className="column column--5 column--med-10 align-content-top">
|
||||
<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>
|
||||
<SEO pageTitle={'Login'} pageUri={'login'} />
|
||||
<NavBar />
|
||||
<div className='row row--padded'>
|
||||
<div className='column column--5 column--med-10 align-content-top'>
|
||||
<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>
|
||||
</div>
|
||||
</div><div className='column column--5 column--med-10 align-content-top'>
|
||||
<div className='column column--8 column--med-10'>
|
||||
<h3 className='h3--no-bottom'>Log in to an existing channel:</h3>
|
||||
<ChannelLoginForm />
|
||||
<h3 className='h3--no-bottom'>Create a brand new channel:</h3>
|
||||
<ChannelCreateForm />
|
||||
</div>
|
||||
</div><div className="column column--5 column--med-10 align-content-top">
|
||||
<div className="column column--8 column--med-10">
|
||||
<h3 className="h3--no-bottom">Log in to an existing channel:</h3>
|
||||
<ChannelLoginForm />
|
||||
<h3 className="h3--no-bottom">Create a brand new channel:</h3>
|
||||
<ChannelCreateForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default withRouter(PublishPage);
|
||||
export default withRouter(LoginPage);
|
||||
|
|
|
@ -14,6 +14,6 @@ const mapDispatchToProps = dispatch => {
|
|||
dispatch(updateMetadata(name, value));
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { handleShowPageUri } from 'actions/show';
|
||||
import { onHandleShowPageUri } from 'actions/show';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
|
@ -10,7 +10,7 @@ const mapStateToProps = ({ show }) => {
|
|||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
handleShowPageUri,
|
||||
onHandleShowPageUri,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
|
@ -8,18 +8,18 @@ import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types
|
|||
|
||||
class ShowPage extends React.Component {
|
||||
componentDidMount () {
|
||||
this.props.handleShowPageUri(this.props.match.params);
|
||||
this.props.onHandleShowPageUri(this.props.match.params);
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.match.params !== this.props.match.params) {
|
||||
this.props.handleShowPageUri(nextProps.match.params);
|
||||
this.props.onHandleShowPageUri(nextProps.match.params);
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const { error, requestType } = this.props;
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage error={error}/>
|
||||
<ErrorPage error={error} />
|
||||
);
|
||||
}
|
||||
switch (requestType) {
|
||||
|
|
|
@ -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) {
|
||||
switch (action.type) {
|
||||
// handle request
|
||||
case actions.REQUEST_UPDATE_ERROR:
|
||||
case actions.REQUEST_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
request: Object.assign({}, state.request, {
|
||||
error: action.data,
|
||||
}),
|
||||
});
|
||||
case actions.CHANNEL_REQUEST_NEW:
|
||||
case actions.ASSET_REQUEST_NEW:
|
||||
case actions.REQUEST_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
request: Object.assign({}, state.request, {
|
||||
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;
|
|
@ -4,7 +4,7 @@ import { updateFileAvailability, updateDisplayAssetError } from 'actions/show';
|
|||
import { UNAVAILABLE, AVAILABLE } from 'constants/asset_display_states';
|
||||
import { checkFileAvailability, triggerClaimGet } from 'api/fileApi';
|
||||
|
||||
function* retrieveFile (action) {
|
||||
function * retrieveFile (action) {
|
||||
const name = action.data.name;
|
||||
const claimId = action.data.claimId;
|
||||
// see if the file is available
|
||||
|
@ -28,6 +28,6 @@ function* retrieveFile (action) {
|
|||
yield put(updateFileAvailability(AVAILABLE));
|
||||
};
|
||||
|
||||
export function* watchFileIsRequested () {
|
||||
export function * watchFileIsRequested () {
|
||||
yield takeLatest(actions.FILE_REQUESTED, retrieveFile);
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ import { watchNewAssetRequest } from './show_asset';
|
|||
import { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel';
|
||||
import { watchFileIsRequested } from './file';
|
||||
|
||||
export default function* rootSaga () {
|
||||
export default function * rootSaga () {
|
||||
yield all([
|
||||
watchHandleShowPageUri(),
|
||||
watchNewAssetRequest(),
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
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 { selectShowState } from 'selectors/show';
|
||||
|
||||
function* newAssetRequest (action) {
|
||||
const { requestId, name, modifier } = action.data;
|
||||
const state = yield select(selectShowState);
|
||||
export function * newAssetRequest (action) {
|
||||
const { requestType, requestId, name, modifier } = action.data;
|
||||
// put an action to update the request in redux
|
||||
yield put(onRequestUpdate(requestType, requestId));
|
||||
// is this an existing request?
|
||||
// If this uri is in the request list, it's already been fetched
|
||||
const state = yield select(selectShowState);
|
||||
if (state.requestList[requestId]) {
|
||||
console.log('that request already exists in the request list!');
|
||||
return null;
|
||||
|
@ -52,6 +54,6 @@ function* newAssetRequest (action) {
|
|||
yield put(onRequestError(null));
|
||||
};
|
||||
|
||||
export function* watchNewAssetRequest () {
|
||||
export function * watchNewAssetRequest () {
|
||||
yield takeLatest(actions.ASSET_REQUEST_NEW, newAssetRequest);
|
||||
};
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import {call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
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 { selectShowState } from 'selectors/show';
|
||||
|
||||
function* getNewChannelAndUpdateChannelList (action) {
|
||||
const { requestId, channelName, channelId } = action.data;
|
||||
const state = yield select(selectShowState);
|
||||
export function * newChannelRequest (action) {
|
||||
const { requestType, requestId, channelName, channelId } = action.data;
|
||||
// put an action to update the request in redux
|
||||
yield put(onRequestUpdate(requestType, requestId));
|
||||
// is this an existing request?
|
||||
// If this uri is in the request list, it's already been fetched
|
||||
const state = yield select(selectShowState);
|
||||
if (state.requestList[requestId]) {
|
||||
console.log('that request already exists in the request list!');
|
||||
return null;
|
||||
|
@ -44,11 +46,11 @@ function* getNewChannelAndUpdateChannelList (action) {
|
|||
yield put(onRequestError(null));
|
||||
}
|
||||
|
||||
export function* watchNewChannelRequest () {
|
||||
yield takeLatest(actions.CHANNEL_REQUEST_NEW, getNewChannelAndUpdateChannelList);
|
||||
export function * watchNewChannelRequest () {
|
||||
yield takeLatest(actions.CHANNEL_REQUEST_NEW, newChannelRequest);
|
||||
};
|
||||
|
||||
function* getNewClaimsAndUpdateChannel (action) {
|
||||
function * getNewClaimsAndUpdateChannel (action) {
|
||||
const { channelKey, name, longId, page } = action.data;
|
||||
let claimsData;
|
||||
try {
|
||||
|
@ -59,6 +61,6 @@ function* getNewClaimsAndUpdateChannel (action) {
|
|||
yield put(updateChannelClaims(channelKey, claimsData));
|
||||
}
|
||||
|
||||
export function* watchUpdateChannelClaims () {
|
||||
export function * watchUpdateChannelClaims () {
|
||||
yield takeLatest(actions.CHANNEL_CLAIMS_UPDATE_ASYNC, getNewClaimsAndUpdateChannel);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { call, put, takeLatest } from 'redux-saga/effects';
|
||||
import * as actions from 'constants/show_action_types';
|
||||
import { onRequestError, onNewChannelRequest, onNewAssetRequest } from 'actions/show';
|
||||
import { newAssetRequest } from 'sagas/show_asset';
|
||||
import { newChannelRequest } from 'sagas/show_channel';
|
||||
import lbryUri from 'utils/lbryUri';
|
||||
|
||||
function* parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
||||
function * parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
||||
console.log('parseAndUpdateIdentifierAndClaim');
|
||||
// this is a request for an asset
|
||||
// claim will be an asset claim
|
||||
|
@ -17,11 +19,11 @@ function* parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
|||
}
|
||||
// trigger an new action to update the store
|
||||
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');
|
||||
// this could be a request for an asset or a channel page
|
||||
// claim could be an asset claim or a channel claim
|
||||
|
@ -34,7 +36,7 @@ function* parseAndUpdateClaimOnly (claim) {
|
|||
// trigger an new action to update the store
|
||||
// return early if this request is for a channel
|
||||
if (isChannel) {
|
||||
return yield put(onNewChannelRequest(channelName, channelClaimId));
|
||||
return yield call(newChannelRequest, onNewChannelRequest(channelName, channelClaimId));
|
||||
}
|
||||
// if not for a channel, parse the claim request
|
||||
let claimName, extension;
|
||||
|
@ -43,10 +45,10 @@ function* parseAndUpdateClaimOnly (claim) {
|
|||
} catch (error) {
|
||||
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');
|
||||
const { identifier, claim } = action.data;
|
||||
if (identifier) {
|
||||
|
@ -55,6 +57,6 @@ function* handleShowPageUri (action) {
|
|||
yield call(parseAndUpdateClaimOnly, claim);
|
||||
};
|
||||
|
||||
export function* watchHandleShowPageUri () {
|
||||
export function * watchHandleShowPageUri () {
|
||||
yield takeLatest(actions.HANDLE_SHOW_URI, handleShowPageUri);
|
||||
};
|
||||
|
|
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.');
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@ module.exports = {
|
|||
'([^:$#/]*)' + // value (stops at the first 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)
|
||||
.map(match => match || null);
|
||||
|
||||
|
@ -56,7 +56,7 @@ module.exports = {
|
|||
'([^:$#/.]*)' + // name (stops at the first extension)
|
||||
'([:$#.]?)([^/]*)' // 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)
|
||||
.map(match => match || null);
|
||||
|
||||
|
|
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
|
@ -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
|
||||
*
|
||||
|
|
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 handlePageRender = require('../helpers/handlePageRender.jsx');
|
||||
|
||||
module.exports = (app) => {
|
||||
// route for the home page
|
||||
app.get('/', (req, res) => {
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to display login page
|
||||
app.get('/login', (req, res) => {
|
||||
res.status(200).render('index');
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to show 'about' page
|
||||
app.get('/about', (req, res) => {
|
||||
res.status(200).render('index');
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to display a list of the trending images
|
||||
app.get('/trending', (req, res) => {
|
||||
res.status(301).redirect('/popular');
|
||||
});
|
||||
app.get('/popular', ({ ip, originalUrl }, res) => {
|
||||
res.status(200).render('index');
|
||||
app.get('/popular', (req, res) => {
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to display a list of the trending images
|
||||
app.get('/new', ({ ip, originalUrl }, res) => {
|
||||
res.status(200).render('index');
|
||||
app.get('/new', (req, res) => {
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to send embedable video player (for twitter)
|
||||
app.get('/embed/:claimId/:name', ({ params }, res) => {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
const { sendGAServeEvent } = require('../helpers/googleAnalytics');
|
||||
|
||||
const { determineResponseType, flipClaimNameAndIdForBackwardsCompatibility, logRequestData, getClaimIdAndServeAsset } = require('../helpers/serveHelpers.js');
|
||||
const lbryUri = require('../helpers/lbryUri.js');
|
||||
|
||||
const handleShowRender = require('../helpers/handleShowRender.jsx');
|
||||
const SERVE = 'SERVE';
|
||||
|
||||
module.exports = (app) => {
|
||||
// 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
|
||||
let hasFileExtension;
|
||||
try {
|
||||
|
@ -17,7 +17,7 @@ module.exports = (app) => {
|
|||
}
|
||||
let responseType = determineResponseType(hasFileExtension, headers);
|
||||
if (responseType !== SERVE) {
|
||||
return res.status(200).render('index');
|
||||
return handleShowRender(req, res);
|
||||
}
|
||||
// handle serve request
|
||||
// send google analytics
|
||||
|
@ -45,7 +45,8 @@ module.exports = (app) => {
|
|||
getClaimIdAndServeAsset(channelName, channelClaimId, claimName, claimId, originalUrl, ip, res);
|
||||
});
|
||||
// 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
|
||||
let hasFileExtension;
|
||||
try {
|
||||
|
@ -55,7 +56,7 @@ module.exports = (app) => {
|
|||
}
|
||||
let responseType = determineResponseType(hasFileExtension, headers);
|
||||
if (responseType !== SERVE) {
|
||||
return res.status(200).render('index');
|
||||
return handleShowRender(req, res);
|
||||
}
|
||||
// handle serve request
|
||||
// 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 REACT_ROOT = Path.resolve(__dirname, 'react/');
|
||||
|
||||
module.exports = {
|
||||
entry : ['babel-polyfill', 'whatwg-fetch', './react/index.js'],
|
||||
target: 'web',
|
||||
entry : ['babel-polyfill', 'whatwg-fetch', './react/client.js'],
|
||||
output: {
|
||||
path : Path.join(__dirname, '/public/bundle/'),
|
||||
filename: 'bundle.js',
|
||||
path : Path.join(__dirname, 'public/bundle/'),
|
||||
publicPath: 'public/bundle/',
|
||||
filename : 'bundle.js',
|
||||
},
|
||||
watch : true,
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
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
|
@ -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
|
@ -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'],
|
||||
},
|
||||
};
|
Probably don't need these
console.log
s here.