commit
9df408690a
29 changed files with 275 additions and 136 deletions
|
@ -11,7 +11,7 @@ spee.ch is a single-serving site that reads and publishes images to and from the
|
||||||
* save your connection uri somewhere handy (you will need it when you start the server)
|
* save your connection uri somewhere handy (you will need it when you start the server)
|
||||||
* the uri should be in the form `mysql://user:pass@host:port/dbname`
|
* the uri should be in the form `mysql://user:pass@host:port/dbname`
|
||||||
* clone this repo
|
* clone this repo
|
||||||
* customize `config/develpment.json` by replacing the value of `Database.PublishUploadPath` with a string representing the local path where you want uploaded files to be stored.
|
* customize `config/develpment.json` by replacing the value of `Database.DownloadDirectory` with a string representing the local path where you want uploaded files to be stored.
|
||||||
* run `npm install`
|
* run `npm install`
|
||||||
* to start the server, from your command line run `node server.js` while passing three environmental variables: your lbry wallet address (`LBRY_WALLET_ADDRESS`), your mysql connection uri (`MYSQL_CONNECTION_STRING`), and the environment to run (`NODE_ENV`).
|
* to start the server, from your command line run `node server.js` while passing three environmental variables: your lbry wallet address (`LBRY_WALLET_ADDRESS`), your mysql connection uri (`MYSQL_CONNECTION_STRING`), and the environment to run (`NODE_ENV`).
|
||||||
* i.e. `LBRY_WALLET_ADDRESS=<your wallet address here> MYSQL_CONNECTION_STRING=<your connection uri here> NODE_ENV=development node server.js`
|
* i.e. `LBRY_WALLET_ADDRESS=<your wallet address here> MYSQL_CONNECTION_STRING=<your connection uri here> NODE_ENV=development node server.js`
|
||||||
|
|
|
@ -9,10 +9,9 @@
|
||||||
},
|
},
|
||||||
"Database": {
|
"Database": {
|
||||||
"MySqlConnectionUri": "none",
|
"MySqlConnectionUri": "none",
|
||||||
"PublishUploadPath": "none"
|
"DownloadDirectory": "none"
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": "none",
|
"LogLevel": "none"
|
||||||
"LogDirectory": "none"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,10 +9,9 @@
|
||||||
},
|
},
|
||||||
"Database": {
|
"Database": {
|
||||||
"MySqlConnectionUri": "none",
|
"MySqlConnectionUri": "none",
|
||||||
"PublishUploadPath": "C:\\lbry\\speech\\hosted_content\\"
|
"DownloadDirectory": "/home/ubuntu/Downloads/"
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": "silly",
|
"LogLevel": "silly"
|
||||||
"LogDirectory": "C:\\lbry\\speech\\logs\\"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,4 @@
|
||||||
const fs = require('fs');
|
module.exports = (winston, logLevel) => {
|
||||||
|
|
||||||
module.exports = (winston, logLevel, logDir) => {
|
|
||||||
if (!fs.existsSync(logDir)) {
|
|
||||||
fs.mkdirSync(logDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
winston.configure({
|
winston.configure({
|
||||||
transports: [
|
transports: [
|
||||||
new (winston.transports.Console)({
|
new (winston.transports.Console)({
|
||||||
|
@ -15,16 +9,6 @@ module.exports = (winston, logLevel, logDir) => {
|
||||||
handleExceptions : true,
|
handleExceptions : true,
|
||||||
humanReadableUnhandledException: true,
|
humanReadableUnhandledException: true,
|
||||||
}),
|
}),
|
||||||
new (winston.transports.File)({
|
|
||||||
filename : `${logDir}/speechLogs.log`,
|
|
||||||
level : logLevel,
|
|
||||||
json : false,
|
|
||||||
timestamp : true,
|
|
||||||
colorize : true,
|
|
||||||
prettyPrint : true,
|
|
||||||
handleExceptions : true,
|
|
||||||
humanReadableUnhandledException: true,
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,9 @@
|
||||||
},
|
},
|
||||||
"Database": {
|
"Database": {
|
||||||
"MySqlConnectionUri": "none",
|
"MySqlConnectionUri": "none",
|
||||||
"PublishUploadPath": "/home/lbry/Downloads/"
|
"DownloadDirectory": "/home/lbry/Downloads/"
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": "verbose",
|
"LogLevel": "verbose"
|
||||||
"LogDirectory": "/home/lbry/Logs"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,9 @@
|
||||||
},
|
},
|
||||||
"Database": {
|
"Database": {
|
||||||
"MySqlConnectionUri": "none",
|
"MySqlConnectionUri": "none",
|
||||||
"PublishUploadPath": "/home/ubuntu/Downloads/"
|
"DownloadDirectory": "/home/ubuntu/Downloads/"
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": "debug",
|
"LogLevel": "debug"
|
||||||
"LogDirectory": "/home/ubuntu/Logs"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,8 @@ function getClaimAndHandleResponse (uri, address, height, resolve, reject) {
|
||||||
});
|
});
|
||||||
// resolve the request
|
// resolve the request
|
||||||
resolve({
|
resolve({
|
||||||
|
name,
|
||||||
|
claimId : claim_id,
|
||||||
fileName: file_name,
|
fileName: file_name,
|
||||||
filePath: download_path,
|
filePath: download_path,
|
||||||
fileType: mime_type,
|
fileType: mime_type,
|
||||||
|
|
|
@ -5,7 +5,7 @@ const db = require('../models');
|
||||||
const googleApiKey = config.get('AnalyticsConfig.GoogleId');
|
const googleApiKey = config.get('AnalyticsConfig.GoogleId');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
postToStats (action, url, ipAddress, result) {
|
postToStats (action, url, ipAddress, name, claimId, fileName, fileType, nsfw, result) {
|
||||||
logger.silly(`creating ${action} record for statistics db`);
|
logger.silly(`creating ${action} record for statistics db`);
|
||||||
// make sure the result is a string
|
// make sure the result is a string
|
||||||
if (result && (typeof result !== 'string')) {
|
if (result && (typeof result !== 'string')) {
|
||||||
|
@ -20,6 +20,11 @@ module.exports = {
|
||||||
action,
|
action,
|
||||||
url,
|
url,
|
||||||
ipAddress,
|
ipAddress,
|
||||||
|
name,
|
||||||
|
claimId,
|
||||||
|
fileName,
|
||||||
|
fileType,
|
||||||
|
nsfw,
|
||||||
result,
|
result,
|
||||||
})
|
})
|
||||||
.then()
|
.then()
|
||||||
|
@ -58,20 +63,27 @@ module.exports = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getStatsSummary () {
|
getStatsSummary (startDate) {
|
||||||
logger.debug('retrieving site statistics');
|
logger.debug('retrieving statistics');
|
||||||
const deferred = new Promise((resolve, reject) => {
|
const deferred = new Promise((resolve, reject) => {
|
||||||
// get the raw statistics data
|
// get the raw statistics data
|
||||||
db.Stats
|
db.Stats
|
||||||
.findAll()
|
.findAll({
|
||||||
|
where: {
|
||||||
|
createdAt: {
|
||||||
|
gt: startDate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const resultHashTable = {};
|
let resultHashTable = {};
|
||||||
let totalServe = 0;
|
let totalServe = 0;
|
||||||
let totalPublish = 0;
|
let totalPublish = 0;
|
||||||
let totalShow = 0;
|
let totalShow = 0;
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
let totalSuccess = 0;
|
let totalSuccess = 0;
|
||||||
let totalFailure = 0;
|
let totalFailure = 0;
|
||||||
|
let percentSuccess;
|
||||||
// sumarise the data
|
// sumarise the data
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
let key = data[i].action + data[i].url;
|
let key = data[i].action + data[i].url;
|
||||||
|
@ -114,7 +126,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const percentSuccess = Math.round(totalSuccess / totalCount * 100);
|
percentSuccess = Math.round(totalSuccess / totalCount * 100);
|
||||||
// return results
|
// return results
|
||||||
resolve({ records: resultHashTable, totals: { totalServe, totalPublish, totalShow, totalCount, totalSuccess, totalFailure }, percentSuccess });
|
resolve({ records: resultHashTable, totals: { totalServe, totalPublish, totalShow, totalCount, totalSuccess, totalFailure }, percentSuccess });
|
||||||
})
|
})
|
||||||
|
@ -125,4 +137,68 @@ module.exports = {
|
||||||
});
|
});
|
||||||
return deferred;
|
return deferred;
|
||||||
},
|
},
|
||||||
|
getTrendingClaims (startDate) {
|
||||||
|
logger.debug('retrieving trending statistics');
|
||||||
|
const deferred = new Promise((resolve, reject) => {
|
||||||
|
// get the raw statistics data
|
||||||
|
db.Stats
|
||||||
|
.findAll({
|
||||||
|
where: {
|
||||||
|
createdAt: {
|
||||||
|
gt: startDate,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
claimId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
let resultHashTable = {};
|
||||||
|
let sortableArray = [];
|
||||||
|
let sortedArray;
|
||||||
|
// summarise the data
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
let key = `${data[i].name}#${data[i].claimId}`;
|
||||||
|
if (resultHashTable[key] === undefined) {
|
||||||
|
resultHashTable[key] = {
|
||||||
|
count : 0,
|
||||||
|
details: {
|
||||||
|
name : data[i].name,
|
||||||
|
claimId : data[i].claimId,
|
||||||
|
fileName: data[i].fileName,
|
||||||
|
fileType: data[i].fileType,
|
||||||
|
nsfw : data[i].nsfw,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
resultHashTable[key]['count'] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let objKey in resultHashTable) {
|
||||||
|
if (resultHashTable.hasOwnProperty(objKey)) {
|
||||||
|
sortableArray.push([
|
||||||
|
resultHashTable[objKey]['count'],
|
||||||
|
resultHashTable[objKey]['details'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sortableArray.sort((a, b) => {
|
||||||
|
return b[0] - a[0];
|
||||||
|
});
|
||||||
|
sortedArray = sortableArray.map((a) => {
|
||||||
|
return a[1];
|
||||||
|
});
|
||||||
|
// return results
|
||||||
|
resolve(sortedArray);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
logger.error('sequelize error', error);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return deferred;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,16 +5,16 @@ module.exports = {
|
||||||
handleRequestError (action, originalUrl, ip, error, res) {
|
handleRequestError (action, originalUrl, ip, error, res) {
|
||||||
logger.error('Request Error >>', error);
|
logger.error('Request Error >>', error);
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
postToStats(action, originalUrl, ip, error.response.data.error.messsage);
|
postToStats(action, originalUrl, ip, null, null, null, null, null, error.response.data.error.messsage);
|
||||||
res.status(error.response.status).send(error.response.data.error.message);
|
res.status(error.response.status).send(error.response.data.error.message);
|
||||||
} else if (error.code === 'ECONNREFUSED') {
|
} else if (error.code === 'ECONNREFUSED') {
|
||||||
postToStats(action, originalUrl, ip, 'Connection refused. The daemon may not be running.');
|
postToStats(action, originalUrl, ip, null, null, null, null, null, 'Connection refused. The daemon may not be running.');
|
||||||
res.status(503).send('Connection refused. The daemon may not be running.');
|
res.status(503).send('Connection refused. The daemon may not be running.');
|
||||||
} else if (error.message) {
|
} else if (error.message) {
|
||||||
postToStats(action, originalUrl, ip, error);
|
postToStats(action, originalUrl, ip, null, null, null, null, null, error);
|
||||||
res.status(400).send(error.message);
|
res.status(400).send(error.message);
|
||||||
} else {
|
} else {
|
||||||
postToStats(action, originalUrl, ip, error);
|
postToStats(action, originalUrl, ip, null, null, null, null, null, error);
|
||||||
res.status(400).send(error);
|
res.status(400).send(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
module.exports = (sequelize, { STRING, TEXT }) => {
|
module.exports = (sequelize, { STRING, BOOLEAN, TEXT }) => {
|
||||||
const Stats = sequelize.define(
|
const Stats = sequelize.define(
|
||||||
'Stats',
|
'Stats',
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,26 @@ module.exports = (sequelize, { STRING, TEXT }) => {
|
||||||
ipAddress: {
|
ipAddress: {
|
||||||
type : STRING,
|
type : STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
default : null,
|
},
|
||||||
|
name: {
|
||||||
|
type : STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
claimId: {
|
||||||
|
type : STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
fileName: {
|
||||||
|
type : STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
fileType: {
|
||||||
|
type : STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
nsfw: {
|
||||||
|
type : BOOLEAN,
|
||||||
|
allowNull: true,
|
||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
type : TEXT('long'),
|
type : TEXT('long'),
|
||||||
|
|
|
@ -12,6 +12,29 @@
|
||||||
margin: 2px 5px 2px 5px;
|
margin: 2px 5px 2px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* publish */
|
||||||
|
#drop-zone {
|
||||||
|
border: 1px dashed lightgrey;
|
||||||
|
padding: 1em;
|
||||||
|
height: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#asset-preview-holder {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-generator {
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
left: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 1px;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
/* show routes */
|
/* show routes */
|
||||||
.show-asset {
|
.show-asset {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -53,7 +76,14 @@ button.copy-button {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* learn more */
|
/* trending claims */
|
||||||
|
.asset-trending {
|
||||||
|
width: 21%;
|
||||||
|
margin: 2%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* learn more */
|
||||||
.learn-more {
|
.learn-more {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
@ -115,29 +145,6 @@ button.copy-button {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* publish */
|
|
||||||
#drop-zone {
|
|
||||||
border: 1px dashed lightgrey;
|
|
||||||
padding: 1em;
|
|
||||||
height: 6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#asset-preview-holder {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-generator {
|
|
||||||
display: block;
|
|
||||||
height: 1px;
|
|
||||||
left: 0;
|
|
||||||
object-fit: contain;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
width: 1px;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* meme */
|
/* meme */
|
||||||
canvas {
|
canvas {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
@ -145,13 +152,6 @@ canvas {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meme-fodder-img {
|
|
||||||
width: 21%;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 2% 4% 2% 0px;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* statistics */
|
/* statistics */
|
||||||
.totals-row {
|
.totals-row {
|
||||||
border-top: 1px solid grey;
|
border-top: 1px solid grey;
|
||||||
|
|
|
@ -91,6 +91,13 @@ h4 {
|
||||||
|
|
||||||
/* other */
|
/* other */
|
||||||
|
|
||||||
|
.asset-small {
|
||||||
|
height: 200px;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 10px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
padding: 0.3em;
|
padding: 0.3em;
|
||||||
}
|
}
|
|
@ -8,11 +8,11 @@ const errorHandlers = require('../helpers/libraries/errorHandlers.js');
|
||||||
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
|
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
|
||||||
|
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
const hostedContentPath = config.get('Database.PublishUploadPath');
|
const hostedContentPath = config.get('Database.DownloadDirectory');
|
||||||
|
|
||||||
module.exports = app => {
|
module.exports = app => {
|
||||||
// route to run a claim_list request on the daemon
|
// route to return a file directly
|
||||||
app.get('/api/streamFile/:name', ({ params, headers }, res) => {
|
app.get('/api/streamFile/:name', ({ params }, res) => {
|
||||||
const filePath = `${hostedContentPath}${params.name}`;
|
const filePath = `${hostedContentPath}${params.name}`;
|
||||||
res.status(200).sendFile(filePath);
|
res.status(200).sendFile(filePath);
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@ module.exports = app => {
|
||||||
lbryApi
|
lbryApi
|
||||||
.getClaimsList(params.name)
|
.getClaimsList(params.name)
|
||||||
.then(claimsList => {
|
.then(claimsList => {
|
||||||
postToStats('serve', originalUrl, ip, 'success');
|
postToStats('serve', originalUrl, ip, null, null, null, null, 'success');
|
||||||
res.status(200).json(claimsList);
|
res.status(200).json(claimsList);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -56,7 +56,7 @@ module.exports = app => {
|
||||||
lbryApi
|
lbryApi
|
||||||
.resolveUri(params.uri)
|
.resolveUri(params.uri)
|
||||||
.then(resolvedUri => {
|
.then(resolvedUri => {
|
||||||
postToStats('serve', originalUrl, ip, 'success');
|
postToStats('serve', originalUrl, ip, null, null, null, null, 'success');
|
||||||
res.status(200).json(resolvedUri);
|
res.status(200).json(resolvedUri);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -76,7 +76,7 @@ module.exports = app => {
|
||||||
try {
|
try {
|
||||||
validateFile(file, name, license, nsfw);
|
validateFile(file, name, license, nsfw);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
postToStats('publish', originalUrl, ip, error.message);
|
postToStats('publish', originalUrl, ip, null, null, null, null, error.message);
|
||||||
logger.debug('rejected >>', error.message);
|
logger.debug('rejected >>', error.message);
|
||||||
res.status(400).send(error.message);
|
res.status(400).send(error.message);
|
||||||
return;
|
return;
|
||||||
|
@ -91,7 +91,7 @@ module.exports = app => {
|
||||||
publishController
|
publishController
|
||||||
.publish(publishParams, fileName, fileType)
|
.publish(publishParams, fileName, fileType)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
postToStats('publish', originalUrl, ip, 'success');
|
postToStats('publish', originalUrl, ip, null, null, null, null, 'success');
|
||||||
res.status(200).json(result);
|
res.status(200).json(result);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
|
@ -11,7 +11,7 @@ module.exports = app => {
|
||||||
app.use('*', ({ originalUrl, ip }, res) => {
|
app.use('*', ({ originalUrl, ip }, res) => {
|
||||||
logger.error(`404 on ${originalUrl}`);
|
logger.error(`404 on ${originalUrl}`);
|
||||||
// post to stats
|
// post to stats
|
||||||
postToStats('show', originalUrl, ip, 'Error: 404');
|
postToStats('show', originalUrl, ip, null, null, null, null, null, 'Error: 404');
|
||||||
// send response
|
// send response
|
||||||
res.status(404).render('fourOhFour');
|
res.status(404).render('fourOhFour');
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,14 +52,14 @@ module.exports = (app) => {
|
||||||
if (headers['accept']) { // note: added b/c some requests errored out due to no accept param in header
|
if (headers['accept']) { // note: added b/c some requests errored out due to no accept param in header
|
||||||
const mimetypes = headers['accept'].split(',');
|
const mimetypes = headers['accept'].split(',');
|
||||||
if (mimetypes.includes('text/html')) {
|
if (mimetypes.includes('text/html')) {
|
||||||
postToStats('show', originalUrl, ip, 'success');
|
postToStats('show', originalUrl, ip, fileInfo.name, fileInfo.claimId, fileInfo.fileName, fileInfo.fileType, fileInfo.nsfw, 'success');
|
||||||
res.status(200).render('showLite', { fileInfo });
|
res.status(200).render('showLite', { fileInfo });
|
||||||
} else {
|
} else {
|
||||||
postToStats('serve', originalUrl, ip, 'success');
|
postToStats('serve', originalUrl, ip, fileInfo.name, fileInfo.claimId, fileInfo.fileName, fileInfo.fileType, fileInfo.nsfw, 'success');
|
||||||
serveFile(fileInfo, res);
|
serveFile(fileInfo, res);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
postToStats('serve', originalUrl, ip, 'success');
|
postToStats('serve', originalUrl, ip, fileInfo.name, fileInfo.claimId, fileInfo.fileName, fileInfo.fileType, fileInfo.nsfw, 'success');
|
||||||
serveFile(fileInfo, res);
|
serveFile(fileInfo, res);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -82,14 +82,14 @@ module.exports = (app) => {
|
||||||
if (headers['accept']) { // note: added b/c some requests errored out due to no accept param in header
|
if (headers['accept']) { // note: added b/c some requests errored out due to no accept param in header
|
||||||
const mimetypes = headers['accept'].split(',');
|
const mimetypes = headers['accept'].split(',');
|
||||||
if (mimetypes.includes('text/html')) {
|
if (mimetypes.includes('text/html')) {
|
||||||
postToStats('show', originalUrl, ip, 'success');
|
postToStats('show', originalUrl, ip, fileInfo.name, fileInfo.claimId, fileInfo.fileName, fileInfo.fileType, fileInfo.nsfw, 'success');
|
||||||
res.status(200).render('showLite', { fileInfo });
|
res.status(200).render('showLite', { fileInfo });
|
||||||
} else {
|
} else {
|
||||||
postToStats('serve', originalUrl, ip, 'success');
|
postToStats('serve', originalUrl, ip, fileInfo.name, fileInfo.claimId, fileInfo.fileName, fileInfo.fileType, fileInfo.nsfw, 'success');
|
||||||
serveFile(fileInfo, res);
|
serveFile(fileInfo, res);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
postToStats('serve', originalUrl, ip, 'success');
|
postToStats('serve', originalUrl, ip, fileInfo.name, fileInfo.claimId, fileInfo.fileName, fileInfo.fileType, fileInfo.nsfw, 'success');
|
||||||
serveFile(fileInfo, res);
|
serveFile(fileInfo, res);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const errorHandlers = require('../helpers/libraries/errorHandlers.js');
|
const errorHandlers = require('../helpers/libraries/errorHandlers.js');
|
||||||
const { getClaimByClaimId, getClaimByName, getAllClaims } = require('../controllers/serveController.js');
|
const { getClaimByClaimId, getClaimByName, getAllClaims } = require('../controllers/serveController.js');
|
||||||
const { getStatsSummary, postToStats } = require('../controllers/statsController.js');
|
const { postToStats, getStatsSummary, getTrendingClaims } = require('../controllers/statsController.js');
|
||||||
|
|
||||||
module.exports = (app) => {
|
module.exports = (app) => {
|
||||||
// route to show 'about' page for spee.ch
|
// route to show 'about' page for spee.ch
|
||||||
|
@ -8,30 +8,44 @@ module.exports = (app) => {
|
||||||
// get and render the content
|
// get and render the content
|
||||||
res.status(200).render('about');
|
res.status(200).render('about');
|
||||||
});
|
});
|
||||||
// route to show the meme-fodder meme maker
|
// route to display a list of the trending images
|
||||||
app.get('/meme-fodder/play', ({ ip, originalUrl }, res) => {
|
app.get('/trending', ({ params, headers }, res) => {
|
||||||
// get and render the content
|
const startDate = new Date();
|
||||||
getAllClaims('meme-fodder')
|
startDate.setDate(startDate.getDate() - 1);
|
||||||
.then(orderedFreePublicClaims => {
|
getTrendingClaims(startDate)
|
||||||
postToStats('show', originalUrl, ip, 'success');
|
.then(result => {
|
||||||
res.status(200).render('memeFodder', { claims: orderedFreePublicClaims });
|
res.status(200).render('trending', { trendingAssets: result });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
errorHandlers.handleRequestError('show', originalUrl, ip, error, res);
|
errorHandlers.handleRequestError(error, res);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// route to show statistics for spee.ch
|
// route to show statistics for spee.ch
|
||||||
app.get('/stats', ({ ip, originalUrl }, res) => {
|
app.get('/stats', ({ ip, originalUrl }, res) => {
|
||||||
// get and render the content
|
// get and render the content
|
||||||
getStatsSummary()
|
const startDate = new Date();
|
||||||
|
startDate.setDate(startDate.getDate() - 1);
|
||||||
|
getStatsSummary(startDate)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
postToStats('show', originalUrl, ip, 'success');
|
postToStats('show', originalUrl, ip, null, null, null, null, null, 'success');
|
||||||
res.status(200).render('statistics', result);
|
res.status(200).render('statistics', result);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
errorHandlers.handleRequestError(error, res);
|
errorHandlers.handleRequestError(error, res);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// route to show the meme-fodder meme maker
|
||||||
|
app.get('/meme-fodder/play', ({ ip, originalUrl }, res) => {
|
||||||
|
// get and render the content
|
||||||
|
getAllClaims('meme-fodder')
|
||||||
|
.then(orderedFreePublicClaims => {
|
||||||
|
postToStats('show', originalUrl, ip, null, null, null, null, null, 'success');
|
||||||
|
res.status(200).render('memeFodder', { claims: orderedFreePublicClaims });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
errorHandlers.handleRequestError('show', originalUrl, ip, error, res);
|
||||||
|
});
|
||||||
|
});
|
||||||
// route to display all free public claims at a given name
|
// route to display all free public claims at a given name
|
||||||
app.get('/:name/all', ({ ip, originalUrl, params }, res) => {
|
app.get('/:name/all', ({ ip, originalUrl, params }, res) => {
|
||||||
// get and render the content
|
// get and render the content
|
||||||
|
@ -41,7 +55,7 @@ module.exports = (app) => {
|
||||||
res.status(307).render('noClaims');
|
res.status(307).render('noClaims');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
postToStats('show', originalUrl, ip, 'success');
|
postToStats('show', originalUrl, ip, null, null, null, null, null, 'success');
|
||||||
res.status(200).render('allClaims', { claims: orderedFreePublicClaims });
|
res.status(200).render('allClaims', { claims: orderedFreePublicClaims });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -59,7 +73,7 @@ module.exports = (app) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// serve the file or the show route
|
// serve the file or the show route
|
||||||
postToStats('show', originalUrl, ip, 'success');
|
postToStats('show', originalUrl, ip, fileInfo.name, fileInfo.claimId, fileInfo.fileName, fileInfo.fileType, fileInfo.nsfw, 'success');
|
||||||
res.status(200).render('show', { fileInfo });
|
res.status(200).render('show', { fileInfo });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -77,7 +91,7 @@ module.exports = (app) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// serve the show route
|
// serve the show route
|
||||||
postToStats('show', originalUrl, ip, 'success');
|
postToStats('show', originalUrl, ip, fileInfo.name, fileInfo.claimId, fileInfo.fileName, fileInfo.fileType, fileInfo.nsfw, 'success');
|
||||||
res.status(200).render('show', { fileInfo });
|
res.status(200).render('show', { fileInfo });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
|
@ -27,7 +27,7 @@ module.exports = (app, siofu, hostedContentPath) => {
|
||||||
// listener for when file upload encounters an error
|
// listener for when file upload encounters an error
|
||||||
uploader.on('error', ({ error }) => {
|
uploader.on('error', ({ error }) => {
|
||||||
logger.error('an error occured while uploading', error);
|
logger.error('an error occured while uploading', error);
|
||||||
postToStats('publish', '/', null, error);
|
postToStats('publish', '/', null, null, null, null, null, error);
|
||||||
socket.emit('publish-status', error);
|
socket.emit('publish-status', error);
|
||||||
});
|
});
|
||||||
// listener for when file has been uploaded
|
// listener for when file has been uploaded
|
||||||
|
@ -41,18 +41,18 @@ module.exports = (app, siofu, hostedContentPath) => {
|
||||||
publishController
|
publishController
|
||||||
.publish(publishParams, file.name, file.meta.type)
|
.publish(publishParams, file.name, file.meta.type)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
postToStats('publish', '/', null, 'success');
|
postToStats('publish', '/', null, null, null, null, null, 'success');
|
||||||
socket.emit('publish-complete', { name: publishParams.name, result });
|
socket.emit('publish-complete', { name: publishParams.name, result });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
error = errorHandlers.handlePublishError(error);
|
error = errorHandlers.handlePublishError(error);
|
||||||
postToStats('publish', '/', null, error);
|
postToStats('publish', '/', null, null, null, null, null, error);
|
||||||
socket.emit('publish-failure', error);
|
socket.emit('publish-failure', error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error(`An error occurred in uploading the client's file`);
|
logger.error(`An error occurred in uploading the client's file`);
|
||||||
socket.emit('publish-failure', 'File uploaded, but with errors');
|
socket.emit('publish-failure', 'File uploaded, but with errors');
|
||||||
postToStats('publish', '/', null, 'File uploaded, but with errors');
|
postToStats('publish', '/', null, null, null, null, null, 'File uploaded, but with errors');
|
||||||
// to-do: remove the file if not done automatically
|
// to-do: remove the file if not done automatically
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
19
server.js
19
server.js
|
@ -7,12 +7,11 @@ const Handlebars = require('handlebars');
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
|
|
||||||
const hostedContentPath = config.get('Database.PublishUploadPath');
|
const hostedContentPath = config.get('Database.DownloadDirectory');
|
||||||
|
|
||||||
// configure logging
|
// configure logging
|
||||||
const logLevel = config.get('Logging.LogLevel');
|
const logLevel = config.get('Logging.LogLevel');
|
||||||
const logDir = config.get('Logging.LogDirectory');
|
require('./config/loggerSetup.js')(winston, logLevel);
|
||||||
require('./config/loggerSetup.js')(winston, logLevel, logDir);
|
|
||||||
|
|
||||||
// set port
|
// set port
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
|
@ -92,9 +91,13 @@ const server = require('./routes/sockets-routes.js')(app, siofu, hostedContentPa
|
||||||
// sync sequelize
|
// sync sequelize
|
||||||
// wrap the server in socket.io to intercept incoming sockets requests
|
// wrap the server in socket.io to intercept incoming sockets requests
|
||||||
// start server
|
// start server
|
||||||
db.sequelize.sync().then(() => {
|
db.sequelize.sync()
|
||||||
server.listen(PORT, () => {
|
.then(() => {
|
||||||
winston.info('Trusting proxy?', app.get('trust proxy'));
|
server.listen(PORT, () => {
|
||||||
winston.info(`Server is listening on PORT ${PORT}`);
|
winston.info('Trusting proxy?', app.get('trust proxy'));
|
||||||
|
winston.info(`Server is listening on PORT ${PORT}`);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
winston.log('Error syncing sequelize db:', error);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
|
@ -14,3 +14,4 @@
|
||||||
{{> footer}}
|
{{> footer}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/assets/js/generalFunctions.js"></script>
|
|
@ -4,10 +4,12 @@
|
||||||
{{> publish}}
|
{{> publish}}
|
||||||
{{> learnMore}}
|
{{> learnMore}}
|
||||||
</div>
|
</div>
|
||||||
|
{{> footer}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
<script src="/siofu/client.js"></script>
|
<script src="/siofu/client.js"></script>
|
||||||
|
|
||||||
|
<script src="/assets/js/generalFunctions.js"></script>
|
||||||
<script src="/assets/js/publishFunctions.js"></script>
|
<script src="/assets/js/publishFunctions.js"></script>
|
||||||
<script src="/assets/js/index.js"></script>
|
<script src="/assets/js/index.js"></script>
|
||||||
|
|
|
@ -5,11 +5,10 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
|
<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">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<title>Spee.ch</title>
|
<title>Spee.ch</title>
|
||||||
<link rel="stylesheet" href="/assets/css/allStyle.css" type="text/css">
|
<link rel="stylesheet" href="/assets/css/generalStyle.css" type="text/css">
|
||||||
<link rel="stylesheet" href="/assets/css/componentStyle.css" type="text/css">
|
<link rel="stylesheet" href="/assets/css/componentStyle.css" type="text/css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="/assets/js/generalFunctions.js"></script>
|
|
||||||
{{{ body }}}
|
{{{ body }}}
|
||||||
<!-- google analytics -->
|
<!-- google analytics -->
|
||||||
{{ googleAnalytics }}
|
{{ googleAnalytics }}
|
||||||
|
|
|
@ -10,5 +10,6 @@
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
<script src="/siofu/client.js"></script>
|
<script src="/siofu/client.js"></script>
|
||||||
|
|
||||||
|
<script src="/assets/js/generalFunctions.js"></script>
|
||||||
<script src="/assets/js/memeFodder-draw.js"></script>
|
<script src="/assets/js/memeFodder-draw.js"></script>
|
||||||
<script src="/assets/js/memeFodder-publish.js"></script>
|
<script src="/assets/js/memeFodder-publish.js"></script>
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div id="asset-placeholder" data-filename="{{{fileInfo.fileName}}}">
|
<div id="asset-placeholder">
|
||||||
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||||
<video class="show-asset" autoplay controls>
|
<video class="show-asset" autoplay controls>
|
||||||
<source src="/api/streamFile/{{fileInfo.fileName}}">
|
<source src="/api/streamFile/{{fileInfo.fileName}}">
|
||||||
|
|
|
@ -4,12 +4,24 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="panel links">
|
<div class="panel links">
|
||||||
<h2 class="subheader">Links</h2>
|
<h2 class="subheader">Links</h2>
|
||||||
|
{{!--direct link to asset--}}
|
||||||
<a href="/{{fileInfo.name}}/{{fileInfo.claimId}}">Direct Link</a>
|
<a href="/{{fileInfo.name}}/{{fileInfo.claimId}}">Direct Link</a>
|
||||||
<br/>
|
<br/>
|
||||||
<input type="text" id="direct-link" class="link" readonly="true" spellcheck="false" value="https://spee.ch/{{fileInfo.name}}/{{fileInfo.claimId}}"/>
|
<input type="text" id="direct-link" class="link" readonly="true" spellcheck="false" value="https://spee.ch/{{fileInfo.name}}/{{fileInfo.claimId}}"/>
|
||||||
<button class="copy-button" data-elementtocopy="direct-link" onclick="copyToClipboard(event)">copy</button>
|
<button class="copy-button" data-elementtocopy="direct-link" onclick="copyToClipboard(event)">copy</button>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
{{!--markdown text using asset--}}
|
||||||
|
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||||
|
{{else}}
|
||||||
|
Markdown
|
||||||
|
<br/>
|
||||||
|
<input type="text" id="markdown-text" class="link" readonly="true" spellcheck="false" value='![{{fileInfo.name}}](https://spee.ch/{{fileInfo.name}}/{{fileInfo.claimId}})'/>
|
||||||
|
<button class="copy-button" data-elementtocopy="markdown-text" onclick="copyToClipboard(event)">copy</button>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
{{/ifConditional}}
|
||||||
|
{{!-- html text for embedding asset--}}
|
||||||
Embed HTML
|
Embed HTML
|
||||||
<br/>
|
<br/>
|
||||||
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||||
|
@ -20,7 +32,8 @@
|
||||||
<button class="copy-button" data-elementtocopy="embed-text" onclick="copyToClipboard(event)">copy</button>
|
<button class="copy-button" data-elementtocopy="embed-text" onclick="copyToClipboard(event)">copy</button>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="/show/{{fileInfo.name}}/{{fileInfo.claimId}}">Details</a>
|
{{!-- link to show route for asset--}}
|
||||||
|
<a href="/show/{{fileInfo.name}}/{{fileInfo.claimId}}">Details Link</a>
|
||||||
</br>
|
</br>
|
||||||
<input type="text" id="show-link" class="link" readonly="true" spellcheck="false" value="https://spee.ch/show/{{fileInfo.name}}/{{fileInfo.claimId}}"/>
|
<input type="text" id="show-link" class="link" readonly="true" spellcheck="false" value="https://spee.ch/show/{{fileInfo.name}}/{{fileInfo.claimId}}"/>
|
||||||
<button class="copy-button" data-elementtocopy="show-link" onclick="copyToClipboard(event)">copy</button>
|
<button class="copy-button" data-elementtocopy="show-link" onclick="copyToClipboard(event)">copy</button>
|
||||||
|
@ -43,7 +56,7 @@
|
||||||
<td>{{fileInfo.fileName}}</td>
|
<td>{{fileInfo.fileName}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="left-column">fileType</td>
|
<td class="left-column">File Type</td>
|
||||||
<td>{{#if fileInfo.fileType}}
|
<td>{{#if fileInfo.fileType}}
|
||||||
{{fileInfo.fileType}}
|
{{fileInfo.fileType}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
@ -53,17 +66,3 @@
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type ="text/javascript">
|
|
||||||
// update the link holder
|
|
||||||
function copyToClipboard(event){
|
|
||||||
var elementToCopy = event.target.dataset.elementtocopy;
|
|
||||||
var element = document.getElementById(elementToCopy);
|
|
||||||
element.select();
|
|
||||||
try {
|
|
||||||
var successful = document.execCommand('copy');
|
|
||||||
} catch (err) {
|
|
||||||
alert('Oops, unable to copy');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -5,7 +5,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{#each claims}}
|
{{#each claims}}
|
||||||
<img class="meme-fodder-img" src="/{{this.name}}/{{this.claim_id}}" onclick="newCanvas(this)"/>
|
<img class="asset-small" src="/{{this.name}}/{{this.claim_id}}" onclick="newCanvas(this)"/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
19
views/partials/trendingAssets.handlebars
Normal file
19
views/partials/trendingAssets.handlebars
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<div class="full">
|
||||||
|
<h2>Trending</h2>
|
||||||
|
{{#each trendingAssets}}
|
||||||
|
{{#if this.nsfw}}
|
||||||
|
{{else }}
|
||||||
|
{{#ifConditional this.fileType '===' 'video/mp4'}}
|
||||||
|
<video class="asset-small" controls>
|
||||||
|
<source src="/api/streamFile/{{this.fileName}}">
|
||||||
|
{{!--fallback--}}
|
||||||
|
Your browser does not support the <code>video</code> element.
|
||||||
|
</video>
|
||||||
|
{{else}}
|
||||||
|
<a href="/show/{{this.name}}/{{this.claimId}}">
|
||||||
|
<img class="asset-small" src="/api/streamFile/{{this.fileName}}" />
|
||||||
|
</a>
|
||||||
|
{{/ifConditional}}
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
|
@ -8,3 +8,16 @@
|
||||||
</div>
|
</div>
|
||||||
{{> footer}}
|
{{> footer}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type ="text/javascript">
|
||||||
|
function copyToClipboard(event){
|
||||||
|
var elementToCopy = event.target.dataset.elementtocopy;
|
||||||
|
var element = document.getElementById(elementToCopy);
|
||||||
|
element.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
} catch (err) {
|
||||||
|
alert('Oops, unable to copy');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,4 +1,4 @@
|
||||||
<div id="asset-placeholder" data-filename="{{{fileInfo.fileName}}}">
|
<div id="asset-placeholder">
|
||||||
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||||
<video class="show-asset-lite" autoplay controls>
|
<video class="show-asset-lite" autoplay controls>
|
||||||
<source src="/api/streamFile/{{fileInfo.fileName}}">
|
<source src="/api/streamFile/{{fileInfo.fileName}}">
|
||||||
|
|
4
views/trending.handlebars
Normal file
4
views/trending.handlebars
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<div class="wrapper">
|
||||||
|
{{> topBar}}
|
||||||
|
{{> trendingAssets}}
|
||||||
|
</div>
|
Loading…
Reference in a new issue