Initial commit
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
log.txt
|
||||
build/
|
||||
TODO
|
2
backend/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src/config.js
|
16
backend/ecosystem.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
apps : [{
|
||||
name : "backend",
|
||||
script : "./src/index.js",
|
||||
cwd : "/var/www/backend",
|
||||
env_production: {
|
||||
NODE_ENV: "production"
|
||||
},
|
||||
env_development: {
|
||||
NODE_ENV: "development"
|
||||
},
|
||||
env_local: {
|
||||
NODE_ENV: "local"
|
||||
}
|
||||
}]
|
||||
}
|
34
backend/package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "LivepeerEvents",
|
||||
"version": "0.4.2",
|
||||
"description": "",
|
||||
"main": "./src/index.js",
|
||||
"module": "./src/server.js",
|
||||
"scripts": {
|
||||
"prod": "NODE_ENV=production pm2 start ecosystem.config.js",
|
||||
"start": "NODE_ENV=production node ./src/index.js",
|
||||
"dev": "NODE_ENV=development nodemon ./src/index.js",
|
||||
"local": "NODE_ENV=local nodemon ./src/index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Marco van Dijk",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"@alch/alchemy-web3": "^1.2.4",
|
||||
"alchemy-api": "^1.3.3",
|
||||
"coinmarketcap-api": "^3.1.1",
|
||||
"connect-mongo": "^3.1.2",
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"esm": "^3.2.20",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.0",
|
||||
"install": "^0.13.0",
|
||||
"joi": "^14.3.1",
|
||||
"mongoose": "^5.12.3",
|
||||
"npm": "^8.5.2",
|
||||
"web3": "^1.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.18.10"
|
||||
}
|
||||
}
|
183
backend/src/abi/BondingManagerProxy.json
Normal file
2258
backend/src/abi/BondingManagerTarget.json
Normal file
4
backend/src/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
//Starting point of backend. Simply imports ESM and calls the actual server.js to be loaded.
|
||||
|
||||
require = require("esm")(module)
|
||||
module.exports = require("./server.js")
|
31
backend/src/models/event.js
Normal file
@ -0,0 +1,31 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
|
||||
//database schema for users
|
||||
const EventSchema = new mongoose.Schema({
|
||||
address: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
transactionHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
transactionUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
}, { timestamps: true });
|
||||
|
||||
//define variable User, which corresponds with the schema
|
||||
const Event = mongoose.model('Event', EventSchema);
|
||||
//export for use outside of this file
|
||||
export default Event;
|
31
backend/src/models/timelapse.js
Normal file
@ -0,0 +1,31 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const timelapseSchema = new mongoose.Schema({
|
||||
ownerId: {
|
||||
type: mongoose.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
OwnerIp: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fullFilename: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
upvotes: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
downvotes: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
}
|
||||
}, { timestamps: true });
|
||||
|
||||
|
||||
const timelapseObj = mongoose.model('timelapseSchema', timelapseSchema);
|
||||
export default timelapseObj;
|
34
backend/src/models/user.js
Normal file
@ -0,0 +1,34 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
|
||||
//database schema for users
|
||||
const UserSchema = new mongoose.Schema({
|
||||
ip: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
upvotedTimelapses: {
|
||||
type: [mongoose.ObjectId],
|
||||
ref: 'timelapseSchema',
|
||||
required: false,
|
||||
default: []
|
||||
},
|
||||
downvotedTimelapses: {
|
||||
type: [mongoose.ObjectId],
|
||||
ref: 'timelapseSchema',
|
||||
required: false,
|
||||
default: []
|
||||
}
|
||||
}, { timestamps: true });
|
||||
|
||||
|
||||
//takes a database field as input, returns T/F if entry already exists.
|
||||
//iterates through entire user DB, any field
|
||||
UserSchema.statics.doesNotExist = async function (field) {
|
||||
return await this.where(field).countDocuments() === 0;
|
||||
};
|
||||
|
||||
//define variable User, which corresponds with the schema
|
||||
const User = mongoose.model('User', UserSchema);
|
||||
//export for use outside of this file
|
||||
export default User;
|
7
backend/src/routes/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
//In this document all used routes are defined.
|
||||
//(in this case this is just user.js and session.js)
|
||||
import userRouter from './user';
|
||||
import sessionRouter from './session';
|
||||
import livepeerRouter from './livepeer';
|
||||
|
||||
export { userRouter, sessionRouter, livepeerRouter };
|
233
backend/src/routes/livepeer.js
Normal file
@ -0,0 +1,233 @@
|
||||
import express from "express";
|
||||
import Event from '../models/event';
|
||||
const fs = require('fs');
|
||||
const apiRouter = express.Router();
|
||||
import {
|
||||
API_CMC, API_L1_HTTP, API_L2_HTTP, API_L2_WS
|
||||
} from "../config";
|
||||
|
||||
// Get ETH price & LPT coin prices
|
||||
const CoinMarketCap = require('coinmarketcap-api');
|
||||
const cmcClient = new CoinMarketCap(API_CMC);
|
||||
// Get gas price on ETH (L2 already gets exported by the O's themselves)
|
||||
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
|
||||
const web3layer1 = createAlchemyWeb3(API_L1_HTTP);
|
||||
const web3layer2 = createAlchemyWeb3(API_L2_HTTP);
|
||||
const web3layer2WS = createAlchemyWeb3(API_L2_WS);
|
||||
|
||||
// Update CMC related api calls every 5 minutes
|
||||
const timeoutCMC = 300000;
|
||||
let cmcPriceGet = 0;
|
||||
let ethPrice = 0;
|
||||
let lptPrice = 0;
|
||||
let cmcQuotes = {};
|
||||
let cmcCache = {};
|
||||
|
||||
// Update alchemy related API calls every 2 seconds
|
||||
const timeoutAlchemy = 2000;
|
||||
let l2Gwei = 0;
|
||||
let l1Gwei = 0;
|
||||
let l2block = 0;
|
||||
let l1block = 0;
|
||||
let arbGet = 0;
|
||||
|
||||
// Gas limits on common contract interactions
|
||||
const redeemRewardGwei = 1053687;
|
||||
const claimTicketGwei = 1333043;
|
||||
const withdrawFeeGwei = 688913;
|
||||
let redeemRewardCostL1 = 0;
|
||||
let redeemRewardCostL2 = 0;
|
||||
let claimTicketCostL1 = 0;
|
||||
let claimTicketCostL2 = 0;
|
||||
let withdrawFeeCostL1 = 0;
|
||||
let withdrawFeeCostL2 = 0;
|
||||
|
||||
|
||||
// Listen to smart contract emitters. Resync with DB every 5 minutes
|
||||
const timeoutEvents = 300000;
|
||||
let eventsCache = [];
|
||||
let eventsGet = 0;
|
||||
// https://arbiscan.io/address/0x35Bcf3c30594191d53231E4FF333E8A770453e40#events
|
||||
const BondingManagerTargetJson = fs.readFileSync('src/abi/BondingManagerTarget.json');
|
||||
const BondingManagerTargetAbi = JSON.parse(BondingManagerTargetJson);
|
||||
const BondingManagerProxyAddr = "0x35Bcf3c30594191d53231E4FF333E8A770453e40";
|
||||
const contractInstance = new web3layer2WS.eth.Contract(BondingManagerTargetAbi.abi, BondingManagerProxyAddr);
|
||||
var BondingManagerProxyListener = contractInstance.events.allEvents(async (error, event) => {
|
||||
try {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
console.log('New event emitted on', BondingManagerProxyAddr);
|
||||
// Push obj of event to cache and create a new entry for it in the DB
|
||||
const eventObj = {
|
||||
address: event.address,
|
||||
transactionHash: event.transactionHash,
|
||||
transactionUrl: "https://arbiscan.io/tx/" + event.transactionHash,
|
||||
name: event.event,
|
||||
data: event.returnValues
|
||||
}
|
||||
const dbObj = new Event(eventObj);
|
||||
await dbObj.save();
|
||||
eventsCache.push(eventObj);
|
||||
}
|
||||
catch (err) {
|
||||
console.log("FATAL ERROR: ", err);
|
||||
}
|
||||
});
|
||||
console.log("listening for events on", BondingManagerProxyAddr)
|
||||
|
||||
// Splits of the big CMC object into separate datas
|
||||
const parseCmc = async function () {
|
||||
try {
|
||||
cmcCache = await cmcClient.getTickers({ limit: 200 });
|
||||
for (var idx = 0; idx < cmcCache.data.length; idx++) {
|
||||
const coinData = cmcCache.data[idx];
|
||||
// Handle specific coins only for the grafana endpoint
|
||||
if (coinData.symbol == "ETH") {
|
||||
ethPrice = coinData.quote.USD.price;
|
||||
} else if (coinData.symbol == "LPT") {
|
||||
lptPrice = coinData.quote.USD.price;
|
||||
}
|
||||
// Sort by name->quotes for quotes endpoint
|
||||
cmcQuotes[coinData.symbol] = coinData.quote.USD;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Queries Alchemy for block info and fees
|
||||
const parseL1Blockchain = async function () {
|
||||
const l1Wei = await web3layer1.eth.getGasPrice();
|
||||
l1block = await web3layer1.eth.getBlockNumber();
|
||||
l1Gwei = l1Wei / 1000000000;
|
||||
redeemRewardCostL1 = (redeemRewardGwei * l1Gwei) / 1000000000;
|
||||
claimTicketCostL1 = (claimTicketGwei * l1Gwei) / 1000000000;
|
||||
withdrawFeeCostL1 = (withdrawFeeGwei * l1Gwei) / 1000000000;
|
||||
}
|
||||
const parseL2Blockchain = async function () {
|
||||
const l2Wei = await web3layer2.eth.getGasPrice();
|
||||
l2block = await web3layer2.eth.getBlockNumber();
|
||||
l2Gwei = l2Wei / 1000000000;
|
||||
redeemRewardCostL2 = (redeemRewardGwei * l2Gwei) / 1000000000;
|
||||
claimTicketCostL2 = (claimTicketGwei * l2Gwei) / 1000000000;
|
||||
withdrawFeeCostL2 = (withdrawFeeGwei * l2Gwei) / 1000000000;
|
||||
}
|
||||
const parseEthBlockchain = async function () {
|
||||
await Promise.all([parseL1Blockchain(), parseL2Blockchain()]);
|
||||
}
|
||||
|
||||
// Export livepeer and eth coin prices and L1 Eth gas price
|
||||
apiRouter.get("/grafana", async (req, res) => {
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
// Update blockchain data if the cached data has expired
|
||||
if (now - arbGet > timeoutAlchemy) {
|
||||
await parseEthBlockchain();
|
||||
arbGet = now;
|
||||
}
|
||||
// Update coin prices once their data has expired
|
||||
if (now - cmcPriceGet > timeoutCMC) {
|
||||
await parseCmc();
|
||||
cmcPriceGet = now;
|
||||
}
|
||||
res.send({
|
||||
timestamp: now,
|
||||
cmcTime: cmcPriceGet,
|
||||
blockchainTime: arbGet,
|
||||
l1GasFeeInGwei: l1Gwei,
|
||||
l2GasFeeInGwei: l2Gwei,
|
||||
ethPriceInDollar: ethPrice,
|
||||
lptPriceInDollar: lptPrice,
|
||||
redeemRewardCostL1,
|
||||
redeemRewardCostL2,
|
||||
claimTicketCostL1,
|
||||
claimTicketCostL2,
|
||||
withdrawFeeCostL1,
|
||||
withdrawFeeCostL2,
|
||||
quotes: cmcQuotes
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.get("/cmc", async (req, res) => {
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
// Update cmc once their data has expired
|
||||
if (now - cmcPriceGet > timeoutCMC) {
|
||||
await parseCmc();
|
||||
cmcPriceGet = now;
|
||||
}
|
||||
res.send(cmcCache);
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.get("/blockchains", async (req, res) => {
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
// Update blockchain data if the cached data has expired
|
||||
if (now - arbGet > timeoutAlchemy) {
|
||||
await parseEthBlockchain();
|
||||
arbGet = now;
|
||||
}
|
||||
res.send({
|
||||
timestamp: now,
|
||||
l1block,
|
||||
l2block,
|
||||
blockchainTime: arbGet,
|
||||
l1GasFeeInGwei: l1Gwei,
|
||||
l2GasFeeInGwei: l2Gwei,
|
||||
redeemRewardCostL1,
|
||||
redeemRewardCostL2,
|
||||
claimTicketCostL1,
|
||||
claimTicketCostL2,
|
||||
withdrawFeeCostL1,
|
||||
withdrawFeeCostL2
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
apiRouter.get("/quotes", async (req, res) => {
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
// Update cmc once their data has expired
|
||||
if (now - cmcPriceGet > timeoutCMC) {
|
||||
cmcCache = await cmcClient.getTickers({ limit: 200 });
|
||||
await parseCmc();
|
||||
cmcPriceGet = now;
|
||||
}
|
||||
res.send(cmcQuotes);
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.get("/getEvents", async (req, res) => {
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
// Update cmc once their data has expired
|
||||
if (now - eventsGet > timeoutEvents) {
|
||||
eventsCache = await Event.find({}, {
|
||||
address: 1,
|
||||
transactionHash: 1,
|
||||
transactionUrl: 1,
|
||||
name: 1,
|
||||
data: 1,
|
||||
_id: 0});
|
||||
eventsGet = now;
|
||||
}
|
||||
res.send(eventsCache);
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default apiRouter;
|
51
backend/src/routes/session.js
Normal file
@ -0,0 +1,51 @@
|
||||
import express from "express";
|
||||
import User from "../models/user";
|
||||
import { SESS_NAME } from "../config";
|
||||
|
||||
const sessionRouter = express.Router();
|
||||
|
||||
sessionRouter.post("", async (req, res) => {
|
||||
try {
|
||||
const username = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||
const user = await User.findOne({ ip: username });
|
||||
if (user) {
|
||||
console.log("User logged in as " + user.ip);
|
||||
req.session.user = {ip: user.ip};
|
||||
res.send({ip: user.ip});
|
||||
} else {
|
||||
const newUser = new User({ ip: username});
|
||||
await newUser.save();
|
||||
console.log("User logged in as " + user.ip);
|
||||
req.session.user = {ip: newUser.ip};
|
||||
res.send({ip: newUser.ip});
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(401).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
//on delete request
|
||||
sessionRouter.delete("", ({ session }, res) => {
|
||||
try {
|
||||
const user = session.user;
|
||||
if (user) {
|
||||
console.log(user.username + " is logging out");
|
||||
session.destroy(err => {
|
||||
if (err) throw (err);
|
||||
res.clearCookie(SESS_NAME);
|
||||
res.send(user);
|
||||
});
|
||||
} else {
|
||||
throw new Error('Sessie kon niet worden verwijderd');
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(422).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
//on get request
|
||||
sessionRouter.get("", ({ session: { user } }, res) => {
|
||||
res.send({ user });
|
||||
});
|
||||
|
||||
export default sessionRouter;
|
106
backend/src/routes/user.js
Normal file
@ -0,0 +1,106 @@
|
||||
//The userRouter is used to handle user related functions
|
||||
import express from 'express';
|
||||
import User from '../models/user';
|
||||
import timelapseObj from '../models/timelapse';
|
||||
|
||||
const userRouter = express.Router();
|
||||
|
||||
userRouter.post("/getVisitorStats", async (req, res) => {
|
||||
try {
|
||||
const totalUserCount = await User.countDocuments();
|
||||
const activeUserCount = await User.countDocuments({ $or: [
|
||||
{"upvotedTimelapses.0": { "$exists": true }},
|
||||
{"downvotedTimelapses.0": { "$exists": true }}
|
||||
]});
|
||||
res.send({totalVisitorCount: totalUserCount,
|
||||
activeVisitorCount: activeUserCount});
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
userRouter.post("/getCurrentUserVotes", async (req, res) => {
|
||||
console.log(req.session);
|
||||
try {
|
||||
const userObj = await User.findOne({ip: req.session.user.ip}, {upvotedTimelapses: 1, downvotedTimelapses: 1, _id: 0});
|
||||
res.send(userObj);
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
userRouter.post("/getScoreByTimelapeFilename", async (req, res) => {
|
||||
try {
|
||||
const filename = req.body.fullFilename;
|
||||
const scoreObj = await timelapseObj.findOne({ fullFilename: filename }, { upvotes: 1, downvotes: 1, _id: 1 });
|
||||
if (scoreObj) {
|
||||
res.send(scoreObj);
|
||||
} else {
|
||||
res.send({upvotes: 0, downvotes: 0});
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
userRouter.post("/setVoteOnTimelapse", async (req, res) => {
|
||||
try {
|
||||
var voteValue = req.body.voteValue;
|
||||
const fullFilename = req.body.fullFilename;
|
||||
const username = req.session.user.ip;
|
||||
console.log("voteValue="+voteValue);
|
||||
console.log("fullFilename="+fullFilename);
|
||||
console.log("username="+username);
|
||||
const currentUserObj = await User.findOne({ip: username});
|
||||
console.log(currentUserObj);
|
||||
if (!currentUserObj){
|
||||
throw new Error("User not logged in");
|
||||
}
|
||||
var currentTimelapseObj = await timelapseObj.findOne({fullFilename: fullFilename});
|
||||
if(!currentTimelapseObj){
|
||||
currentTimelapseObj = new timelapseObj({ ownerId: currentUserObj._id, OwnerIp: currentUserObj.ip, fullFilename: fullFilename});
|
||||
await currentTimelapseObj.save();
|
||||
}else{
|
||||
console.log(currentTimelapseObj);
|
||||
if(currentUserObj.upvotedTimelapses.length && currentUserObj.upvotedTimelapses.includes(currentTimelapseObj._id)){
|
||||
currentTimelapseObj.upvotes = currentTimelapseObj.upvotes - 1;
|
||||
await currentTimelapseObj.save()
|
||||
await User.updateOne({ip: username},{
|
||||
$pullAll: {
|
||||
upvotedTimelapses: [currentTimelapseObj._id],
|
||||
},
|
||||
});
|
||||
} else if(currentUserObj.downvotedTimelapses.length && currentUserObj.downvotedTimelapses.includes(currentTimelapseObj._id)){
|
||||
currentTimelapseObj.downvotes = currentTimelapseObj.downvotes - 1;
|
||||
await currentTimelapseObj.save()
|
||||
await User.updateOne({ip: username},{
|
||||
$pullAll: {
|
||||
downvotedTimelapses: [currentTimelapseObj._id],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (voteValue == 1){
|
||||
await User.updateOne(
|
||||
{ ip: username },
|
||||
{ $push: { upvotedTimelapses: currentTimelapseObj._id } }
|
||||
);
|
||||
currentTimelapseObj.upvotes = currentTimelapseObj.upvotes + 1;
|
||||
}else if (voteValue == -1){
|
||||
await User.updateOne(
|
||||
{ ip: username },
|
||||
{ $push: { downvotedTimelapses: currentTimelapseObj._id } }
|
||||
);
|
||||
currentTimelapseObj.downvotes = currentTimelapseObj.downvotes + 1;
|
||||
}
|
||||
await currentTimelapseObj.save();
|
||||
console.log(currentTimelapseObj);
|
||||
res.send(currentTimelapseObj);
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
export default userRouter;
|
89
backend/src/server.js
Normal file
@ -0,0 +1,89 @@
|
||||
//Server logic. Imports all necessary routes, models, etc
|
||||
import express from 'express';
|
||||
import mongoose from 'mongoose';
|
||||
import session from "express-session";
|
||||
import connectStore from "connect-mongo";
|
||||
import { userRouter, sessionRouter, livepeerRouter } from './routes/index';
|
||||
import {
|
||||
PORT, NODE_ENV, MONGO_URI, SESS_NAME, SESS_SECRET, SESS_LIFETIME , MONGO_URI_DEV, MONGO_URI_LOCAL
|
||||
} from "./config";
|
||||
|
||||
const { NODE_ENV: mode } = process.env;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
//first connect with DB
|
||||
if (mode == "production"){
|
||||
await mongoose.connect(MONGO_URI, { useNewUrlParser: true, useFindAndModify: false});
|
||||
}else if (mode == "development"){
|
||||
await mongoose.connect(MONGO_URI_DEV, { useNewUrlParser: true, useFindAndModify: false});
|
||||
}else if (mode == "local"){
|
||||
await mongoose.connect(MONGO_URI_LOCAL, { useNewUrlParser: true, useFindAndModify: false});
|
||||
}else{
|
||||
await mongoose.connect(MONGO_URI, { useNewUrlParser: true, useFindAndModify: false});
|
||||
}
|
||||
console.log('MongoDB connected on ' + mode);
|
||||
//web application framework
|
||||
const app = express();
|
||||
//disable powered by message, which contains information on
|
||||
app.disable('x-powered-by');
|
||||
//parses and validates requests to make things harder for malicious actors
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
//import session module
|
||||
const MongoStore = connectStore(session);
|
||||
|
||||
//define session data
|
||||
app.use(session({
|
||||
name: SESS_NAME,
|
||||
//TODO: change secret in config file
|
||||
secret: SESS_SECRET,
|
||||
//define where to store them
|
||||
store: new MongoStore({
|
||||
mongooseConnection: mongoose.connection,
|
||||
collection: 'session',
|
||||
ttl: parseInt(SESS_LIFETIME) / 1000,
|
||||
}),
|
||||
saveUninitialized: false,
|
||||
proxy: NODE_ENV === "production",
|
||||
resave: false,
|
||||
//cookie to send to users
|
||||
cookie: {
|
||||
sameSite: true,
|
||||
secure: NODE_ENV === 'production',
|
||||
maxAge: parseInt(SESS_LIFETIME)
|
||||
}
|
||||
}));
|
||||
//define default router
|
||||
const apiRouter = express.Router();
|
||||
//which handles any request starting with /api
|
||||
app.use('/api', apiRouter);
|
||||
//but changes to a different router for different paths
|
||||
apiRouter.use('/users', userRouter);
|
||||
apiRouter.use('/session', sessionRouter);
|
||||
apiRouter.use('/livepeer', livepeerRouter);
|
||||
|
||||
// error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
|
||||
res.locals.message = err.message;
|
||||
// set locals, only providing error in development
|
||||
//res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// add this line to include winston logging
|
||||
console.log(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
//actually start server
|
||||
app.listen(PORT, "0.0.0.0", function () {
|
||||
console.log(`Listening on port ${PORT}`);
|
||||
});
|
||||
//and log any errors to the console
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
})();
|
46
package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "LivepeerEvents",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"ethers": "^5.4.4",
|
||||
"http": "^0.0.1-security",
|
||||
"https": "^1.0.0",
|
||||
"md5": "^2.3.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-indiana-drag-scroll": "^2.1.0",
|
||||
"react-markdown": "^7.1.1",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^6.0.2",
|
||||
"react-scripts": "3.2.0",
|
||||
"redux": "^4.1.2",
|
||||
"redux-thunk": "^2.4.1",
|
||||
"styled-components": "^5.3.3"
|
||||
},
|
||||
"proxy": "http://localhost:42609",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
21
public/50x.html
Executable file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Error</title>
|
||||
<style>
|
||||
body {
|
||||
width: 35em;
|
||||
margin: 0 auto;
|
||||
font-family: Tahoma, Verdana, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>An error occurred.</h1>
|
||||
<p>Sorry, the page you are looking for is currently unavailable.<br/>
|
||||
Please try again later.</p>
|
||||
<p>If you are the system administrator of this resource then you should check
|
||||
the error log for details.</p>
|
||||
<p><em>Faithfully yours, nginx.</em></p>
|
||||
</body>
|
||||
</html>
|
BIN
public/background.jpg
Normal file
After Width: | Height: | Size: 3.6 MiB |
BIN
public/dvdvideo.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
public/eth.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
public/github.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
public/grafana.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/home.png
Normal file
After Width: | Height: | Size: 959 B |
21
public/index.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#855dfe" />
|
||||
<meta
|
||||
name="LivepeerEvents"
|
||||
content="marco@stronk.tech"
|
||||
/>
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>LivepeerEvents</title>
|
||||
<meta name="title" content="Stronk" />
|
||||
<meta name="description" content="Contact: marco@stronk.tech" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
BIN
public/livepeer.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
public/loading.png
Normal file
After Width: | Height: | Size: 14 KiB |
14
public/manifest.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"short_name": "LivepeerEvents",
|
||||
"name": "nframe.nl",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#855dfe",
|
||||
"background_color": "#020643"
|
||||
}
|
1
public/metamask.svg
Normal file
After Width: | Height: | Size: 4.9 KiB |
142
public/mistserver.svg
Normal file
@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="svg4368"
|
||||
viewBox="0 0 121.875 90.268784"
|
||||
height="25.475857mm"
|
||||
width="34.395832mm"
|
||||
sodipodi:docname="mistserverblue.svg"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="995"
|
||||
id="namedview23"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.4441545"
|
||||
inkscape:cx="91.00278"
|
||||
inkscape:cy="71.749755"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4368"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<defs
|
||||
id="defs4370" />
|
||||
<metadata
|
||||
id="metadata4373">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
transform="matrix(0.60179374,0,0,0.60179374,-30.069506,-264.76137)"
|
||||
id="g847"
|
||||
style="stroke-width:1.55784273">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#b5d3e2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path54-7"
|
||||
d="m 147.69186,528.8951 5.05228,-3.32212 -49.37108,-75.06356 -5.050441,3.32211 49.369241,75.06357 v 0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#b5d3e2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path48"
|
||||
d="M 234.602,528.24099 H 67.850477 v -6.04541 H 234.602 v 6.04541" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#b5d3e2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path54"
|
||||
d="m 154.76084,528.89509 -5.05228,-3.32212 49.37108,-75.06356 5.05043,3.32211 -49.36923,75.06357 v 0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#b5d3e2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path64"
|
||||
d="m 165.20657,526.60296 c 0,7.72396 -6.25913,13.98126 -13.97941,13.98126 -7.72029,0 -13.98126,-6.2573 -13.98126,-13.98126 0,-7.72029 6.26097,-13.97941 13.98126,-13.97941 7.72028,0 13.97941,6.25912 13.97941,13.97941" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path70"
|
||||
d="m 150.08815,473.69925 51.00726,-23.80203 2.6551,5.42999 -49.49635,25.31477" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path46"
|
||||
d="M 70.198071,524.45454 151.2267,567.71942 232.25275,524.45454 201.74568,455.4456 H 100.70661 Z M 151.2267,574.57186 62.400222,527.14098 96.76964,449.40019 H 205.6832 l 34.36905,77.74079 -88.82555,47.43088 v 0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path50"
|
||||
d="m 84.726929,525.72222 c 0,9.59968 -7.78108,17.38075 -17.379651,17.38075 -9.599122,0 -17.381124,-7.78106 -17.381124,-17.38075 0,-9.59784 7.782002,-17.38076 17.381124,-17.38076 9.598571,0 17.379651,7.78292 17.379651,17.38076" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path52"
|
||||
d="m 252.48632,525.72222 c 0,9.59968 -7.78107,17.38075 -17.38074,17.38075 -9.59968,0 -17.37892,-7.78106 -17.37892,-17.38075 0,-9.59784 7.77924,-17.38076 17.37892,-17.38076 9.59967,0 17.38074,7.78292 17.38074,17.38076" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path58"
|
||||
d="m 215.45866,452.79969 c 0,7.0975 -5.75242,12.8481 -12.84624,12.8481 -7.09565,0 -12.84625,-5.7506 -12.84625,-12.8481 0,-7.09381 5.7506,-12.84625 12.84625,-12.84625 7.09382,0 12.84624,5.75244 12.84624,12.84625" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path60"
|
||||
d="m 112.68722,452.79969 c 0,7.0975 -5.75133,12.8481 -12.84587,12.8481 -7.095288,0 -12.847536,-5.7506 -12.847536,-12.8481 0,-7.09381 5.752247,-12.84625 12.847535,-12.84625 7.094541,0 12.845871,5.75244 12.845871,12.84625" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path62"
|
||||
d="m 168.22835,477.73675 c 0,9.39145 -7.61157,17.00487 -17.00119,17.00487 -9.38963,0 -17.00304,-7.61342 -17.00304,-17.00487 0,-9.38962 7.61341,-17.0012 17.00304,-17.0012 9.38962,0 17.00119,7.61158 17.00119,17.0012" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path72"
|
||||
d="m 233.57896,529.08671 -83.88016,-47.98547 4.56768,-6.72714 82.36743,49.49637 -3.05495,5.21624 v 0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path76"
|
||||
d="m 168.60607,572.57453 c 0,9.59968 -7.78107,17.37891 -17.38075,17.37891 -9.59784,0 -17.37891,-7.77923 -17.37891,-17.37891 0,-9.59968 7.78107,-17.38076 17.37891,-17.38076 9.59968,0 17.38075,7.78108 17.38075,17.38076" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path72-5"
|
||||
d="m 68.873783,529.0867 83.880167,-47.98547 -4.56768,-6.72714 -82.367437,49.49637 3.05495,5.21624 v 0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8cb3cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.55784273"
|
||||
id="path70-3"
|
||||
d="m 152.36456,473.69925 -51.00726,-23.80203 -2.655101,5.42999 49.496351,25.31477" />
|
||||
<rect
|
||||
y="483.39197"
|
||||
x="147.35124"
|
||||
height="95"
|
||||
width="7.75"
|
||||
id="rect1151"
|
||||
style="opacity:1;vector-effect:none;fill:#8cb3cf;fill-opacity:1;stroke:none;stroke-width:2.07712364;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.0 KiB |
BIN
public/prometheus.png
Normal file
After Width: | Height: | Size: 104 KiB |
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
34
src/App.js
Normal file
@ -0,0 +1,34 @@
|
||||
import * as React from "react";
|
||||
import Home from './home.js';
|
||||
import Startup from './loadingScreen.js';
|
||||
import Grafana from './grafana.js';
|
||||
import Livepeer from './livepeer.js';
|
||||
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Routes,
|
||||
Route
|
||||
} from "react-router-dom";
|
||||
|
||||
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Startup>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route exact path='/livepeer' element={<Livepeer />} />
|
||||
<Route exact path='/orchestrator' element={<Grafana />} />
|
||||
<Route path='/' element={<Home />} />
|
||||
</Routes>
|
||||
<div id="dvdlogo">
|
||||
<svg width="153px" height="69px">
|
||||
<g>
|
||||
<path d="M140.186,63.52h-1.695l-0.692,5.236h-0.847l0.77-5.236h-1.693l0.076-0.694h4.158L140.186,63.52L140.186,63.52z M146.346,68.756h-0.848v-4.545l0,0l-2.389,4.545l-1-4.545l0,0l-1.462,4.545h-0.771l1.924-5.931h0.695l0.924,4.006l2.078-4.006 h0.848V68.756L146.346,68.756z M126.027,0.063H95.352c0,0-8.129,9.592-9.654,11.434c-8.064,9.715-9.523,12.32-9.779,13.02 c0.063-0.699-0.256-3.304-3.686-13.148C71.282,8.7,68.359,0.062,68.359,0.062H57.881V0L32.35,0.063H13.169l-1.97,8.131 l14.543,0.062h3.365c9.336,0,15.055,3.747,13.467,10.354c-1.717,7.24-9.91,10.416-18.545,10.416h-3.24l4.191-17.783H10.502 L4.34,37.219h20.578c15.432,0,30.168-8.13,32.709-18.608c0.508-1.906,0.443-6.67-0.764-9.527c0-0.127-0.063-0.191-0.127-0.444 c-0.064-0.063-0.127-0.509,0.127-0.571c0.128-0.062,0.383,0.189,0.445,0.254c0.127,0.317,0.19,0.57,0.19,0.57l13.083,36.965 l33.344-37.6h14.1h3.365c9.337,0,15.055,3.747,13.528,10.354c-1.778,7.24-9.972,10.416-18.608,10.416h-3.238l4.191-17.783h-14.481 l-6.159,25.976h20.576c15.434,0,30.232-8.13,32.709-18.608C152.449,8.193,141.523,0.063,126.027,0.063L126.027,0.063z M71.091,45.981c-39.123,0-70.816,4.512-70.816,10.035c0,5.59,31.693,10.034,70.816,10.034c39.121,0,70.877-4.444,70.877-10.034 C141.968,50.493,110.212,45.981,71.091,45.981L71.091,45.981z M68.55,59.573c-8.956,0-16.196-1.523-16.196-3.365 c0-1.84,7.239-3.303,16.196-3.303c8.955,0,16.195,1.463,16.195,3.303C84.745,58.05,77.505,59.573,68.55,59.573L68.55,59.573z" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</Router>
|
||||
</Startup>
|
||||
);
|
||||
}
|
27
src/actions/error.js
Normal file
@ -0,0 +1,27 @@
|
||||
{/* Ability to receive and clear errors as dispatch actions
|
||||
Requires this in files where errors can be received:
|
||||
const mapStateToProps = ({ errors }) => ({
|
||||
errors
|
||||
});
|
||||
*/}
|
||||
export const RECEIVE_ERRORS = "RECEIVE_ERRORS";
|
||||
export const RECEIVE_NOTIFICATIONS = "RECEIVE_NOTIFICATIONS";
|
||||
export const CLEAR_ERRORS = "CLEAR_ERRORS";
|
||||
|
||||
export const receiveErrors = ({ message }) => ({
|
||||
type: RECEIVE_ERRORS,
|
||||
message
|
||||
});
|
||||
|
||||
export const clearErrors = () => ({
|
||||
type: CLEAR_ERRORS
|
||||
});
|
||||
|
||||
const receiveNotification = (message) => ({
|
||||
type: RECEIVE_NOTIFICATIONS,
|
||||
message
|
||||
})
|
||||
|
||||
export const receiveNotifications = (title, message) => async dispatch => {
|
||||
return dispatch(receiveNotification({title, message}));
|
||||
};
|
43
src/actions/livepeer.js
Normal file
@ -0,0 +1,43 @@
|
||||
import * as apiUtil from "../util/livepeer";
|
||||
import { receiveErrors } from "./error";
|
||||
|
||||
export const RECEIVE_QUOTES = "RECEIVE_QUOTES";
|
||||
export const RECEIVE_BLOCKCHAIN_DATA = "RECEIVE_BLOCKCHAIN_DATA";
|
||||
export const RECEIVE_EVENTS = "RECEIVE_EVENTS";
|
||||
|
||||
const setQuotes = message => ({
|
||||
type: RECEIVE_QUOTES, message
|
||||
});
|
||||
const setBlockchainData = message => ({
|
||||
type: RECEIVE_BLOCKCHAIN_DATA, message
|
||||
});
|
||||
const setEvents = message => ({
|
||||
type: RECEIVE_EVENTS, message
|
||||
});
|
||||
|
||||
export const getQuotes = () => async dispatch => {
|
||||
const response = await apiUtil.getQuotes();
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return dispatch(setQuotes(data));
|
||||
}
|
||||
return dispatch(receiveErrors(data));
|
||||
};
|
||||
|
||||
export const getBlockchainData = () => async dispatch => {
|
||||
const response = await apiUtil.getBlockchainData();
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return dispatch(setBlockchainData(data));
|
||||
}
|
||||
return dispatch(receiveErrors(data));
|
||||
};
|
||||
|
||||
export const getEvents = () => async dispatch => {
|
||||
const response = await apiUtil.getEvents();
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return dispatch(setEvents(data));
|
||||
}
|
||||
return dispatch(receiveErrors(data));
|
||||
};
|
18
src/actions/session.js
Normal file
@ -0,0 +1,18 @@
|
||||
import * as apiUtil from "../util/session";
|
||||
import { receiveErrors } from "./error";
|
||||
|
||||
export const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER";
|
||||
|
||||
const receiveCurrentUser = user => ({
|
||||
type: RECEIVE_CURRENT_USER,
|
||||
user
|
||||
});
|
||||
|
||||
export const login = () => async dispatch => {
|
||||
const response = await apiUtil.login();
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return dispatch(receiveCurrentUser(data));
|
||||
}
|
||||
return dispatch(receiveErrors(data));
|
||||
};
|
50
src/actions/user.js
Normal file
@ -0,0 +1,50 @@
|
||||
import * as apiUtil from "../util/user";
|
||||
import { receiveErrors } from "./error";
|
||||
|
||||
export const RECEIVE_VISITOR_STATS = "RECEIVE_VISITOR_STATS";
|
||||
export const RECEIVE_CURRENT_USER_VOTES = "RECEIVE_CURRENT_USER_VOTES";
|
||||
|
||||
const setVisitorStats = message => ({
|
||||
type: RECEIVE_VISITOR_STATS, message
|
||||
});
|
||||
|
||||
|
||||
const setCurrentUserVotes = message => ({
|
||||
type: RECEIVE_CURRENT_USER_VOTES, message
|
||||
});
|
||||
|
||||
export const getVisitorStats = () => async dispatch => {
|
||||
const response = await apiUtil.getVisitorStats();
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return dispatch(setVisitorStats(data));
|
||||
}
|
||||
return dispatch(receiveErrors(data));
|
||||
};
|
||||
|
||||
export const getCurrentUserVotes = () => async dispatch => {
|
||||
const response = await apiUtil.getCurrentUserVotes();
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return dispatch(setCurrentUserVotes(data));
|
||||
}
|
||||
return dispatch(receiveErrors(data));
|
||||
};
|
||||
|
||||
export const getScoreByTimelapeFilename = (fullFilename) => async dispatch => {
|
||||
const response = await apiUtil.getScoreByTimelapeFilename(fullFilename);
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return data;
|
||||
}
|
||||
return dispatch(receiveErrors(data));
|
||||
};
|
||||
|
||||
export const setVoteOnTimelapse = (voteValue, fullFilename) => async dispatch => {
|
||||
const response = await apiUtil.setVoteOnTimelapse(voteValue, fullFilename);
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return data;
|
||||
}
|
||||
return dispatch(receiveErrors(data));
|
||||
};
|
47
src/eventButton.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
const EventButton = (obj) => {
|
||||
let eventSpecificInfo;
|
||||
if (obj.name == "EarningsClaimed") {
|
||||
eventSpecificInfo = <div className="row">
|
||||
<p>(Round {obj.data.endRound}) Claim: {obj.data.delegator} earned {obj.data.rewards / 1000000000000000000} Eth @ Orchestrator {obj.data.delegate}</p>
|
||||
</div>
|
||||
} else if (obj.name == "Unbond") {
|
||||
eventSpecificInfo = <div className="row">
|
||||
<p>(Round {obj.data.withdrawRound}) Unbond: {obj.data.delegator} unbonded {obj.data.amount / 1000000000000000000} Eth @ Orchestrator {obj.data.delegate}</p>
|
||||
</div>
|
||||
} else if (obj.name == "TransferBond") {
|
||||
eventSpecificInfo = <div className="row">
|
||||
<p>TransferBond: transfered bond worth {obj.data.amount / 1000000000000000000} Eth from {obj.data.oldDelegator} to {obj.data.newDelegator}</p>
|
||||
</div>
|
||||
} else if (obj.name == "Bond") {
|
||||
eventSpecificInfo = <div className="row">
|
||||
<p>Bond: {obj.data.delegator} transfered bond worth {obj.data.bondedAmount / 1000000000000000000} Eth from {obj.data.oldDelegate} to {obj.data.newDelegate}</p>
|
||||
</div>
|
||||
} else if (obj.name == "Rebond") {
|
||||
eventSpecificInfo = <div className="row">
|
||||
<p>Rebond: {obj.data.delegator} @ {obj.data.delegate}</p>
|
||||
</div>
|
||||
} else if (obj.name == "WithdrawFees") {
|
||||
eventSpecificInfo = <div className="row">
|
||||
<p>WithdrawFees: {obj.data.amount / 1000000000000000000} Eth {obj.data.delegator} to {obj.data.recipient}</p>
|
||||
</div>
|
||||
} else {
|
||||
eventSpecificInfo = <div className="row">
|
||||
<p>UNIMPLEMENTED: {obj.event}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<a href={obj.transactionUrl}>
|
||||
<button className="waveButton">
|
||||
<img alt="" src="livepeer.png" width="30" height="30" />
|
||||
{eventSpecificInfo}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventButton;
|
71
src/grafana.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import './style.css';
|
||||
import {
|
||||
Navigate, useParams
|
||||
} from "react-router-dom";
|
||||
|
||||
const Grafana = () => {
|
||||
let params = useParams();
|
||||
const [redirectToHome, setRedirectToHome] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [])
|
||||
|
||||
if (redirectToHome) {
|
||||
return <Navigate push to="/" />;
|
||||
}
|
||||
return (
|
||||
<div className="stroke" style={{ margin: 0, padding: 0 }}>
|
||||
<div className="row" style={{ margin: 0, padding: 0 }}>
|
||||
<button className="homeButton" onClick={() => {
|
||||
setRedirectToHome(true);
|
||||
}}>
|
||||
<img alt="" src="/livepeer.png" width="100em" height="100em" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="stroke" style={{ margin: 0, padding: 0 }}>
|
||||
<div className="flexContainer">
|
||||
<div className="stroke" style={{ marginTop: 0, marginBottom: 5, paddingBottom: 0 }}>
|
||||
<div className="stroke roundedOpaque" style={{}}>
|
||||
<div className="row">
|
||||
<h2> <img alt="" src="livepeer.png" width="30" height="30" /> <a href="https://explorer.livepeer.org/accounts/0x847791cbf03be716a7fe9dc8c9affe17bd49ae5e/">Livepeer Orchestrator</a></h2>
|
||||
</div>
|
||||
<div className="stroke roundedOpaque" style={{ borderRadius: "1em", backgroundColor: "#111217" }}>
|
||||
<div className="flexContainer" style={{ justifyContent: "center" }}>
|
||||
<iframe className="halfGrafana" src="https://grafana.stronk.tech/d-solo/71b6OZ0Gz/orchestrator-overview?orgId=1&refresh=5s&theme=dark&panelId=23763572081" height="200" frameBorder="0"></iframe>
|
||||
<iframe className="halfGrafana" src="https://grafana.stronk.tech/d-solo/71b6OZ0Gz/orchestrator-overview?orgId=1&refresh=5s&theme=dark&panelId=23763572082" height="200" frameBorder="0"></iframe>
|
||||
</div>
|
||||
<div className="flexContainer" style={{ justifyContent: "center" }}>
|
||||
<iframe className="fullGrafana" src="https://grafana.stronk.tech/d-solo/71b6OZ0Gz/orchestrator-overview?orgId=1&refresh=5s&theme=dark&panelId=23763572014" height="200" frameBorder="0"></iframe>
|
||||
</div>
|
||||
<div className="flexContainer" style={{ justifyContent: "center" }}>
|
||||
<iframe className="fullGrafana" src="https://grafana.stronk.tech/d-solo/71b6OZ0Gz/orchestrator-overview?orgId=1&refresh=5s&theme=dark&panelId=23763572077" height="400" frameBorder="0"></iframe>
|
||||
</div>
|
||||
<div className="flexContainer" style={{ justifyContent: "center" }}>
|
||||
<iframe className="fullGrafana" src="https://grafana.stronk.tech/d-solo/71b6OZ0Gz/orchestrator-overview?orgId=1&refresh=5s&theme=dark&panelId=23763572056" height="200" frameBorder="0"></iframe>
|
||||
</div>
|
||||
<div className="flexContainer" style={{ justifyContent: "center" }}>
|
||||
<iframe className="fullGrafana" src="https://grafana.stronk.tech/d-solo/71b6OZ0Gz/orchestrator-overview?orgId=1&refresh=5s&theme=dark&panelId=23763572032" height="200" frameBorder="0"></iframe>
|
||||
</div>
|
||||
<div className="flexContainer" style={{ justifyContent: "center" }}>
|
||||
<iframe className="fullGrafana" src="https://grafana.stronk.tech/d-solo/71b6OZ0Gz/orchestrator-overview?orgId=1&from=now-2d&to=now&refresh=5s&theme=dark&panelId=23763572040" height="400" frameBorder="0"></iframe>
|
||||
</div>
|
||||
<div className="row">
|
||||
<a href="https://grafana.stronk.tech/d/71b6OZ0Gz/orchestrator-overview?orgId=1&refresh=5s&theme=dark">
|
||||
<button className="waveButton">
|
||||
<img alt="" src="grafana.png" width="30" height="30" />
|
||||
<p>Full Statistics</p>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Grafana;
|
116
src/home.js
Normal file
@ -0,0 +1,116 @@
|
||||
import * as React from "react";
|
||||
import './style.css';
|
||||
import {
|
||||
Navigate
|
||||
} from "react-router-dom";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
getVisitorStats
|
||||
} from "./actions/user";
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
session: state.session,
|
||||
userstate: state.userstate,
|
||||
errors: state.errors
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
getVisitorStats: () => dispatch(getVisitorStats())
|
||||
});
|
||||
|
||||
|
||||
class Home extends React.Component {
|
||||
state = {
|
||||
redirectToGrafana: false,
|
||||
redirectToLPT: false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.redirectToRunningServices) {
|
||||
return <Navigate push to="/services" />;
|
||||
}
|
||||
else if (this.state.redirectToWavePortal) {
|
||||
return <Navigate push to="/waveportal" />;
|
||||
}
|
||||
if (this.state.redirectToTimelapses) {
|
||||
return <Navigate push to="/timelapse" />;
|
||||
}
|
||||
if (this.state.redirectToTutorialMistOcto) {
|
||||
return <Navigate push to="/guides/mistocto.md" />;
|
||||
}
|
||||
if (this.state.redirectToGrafana) {
|
||||
return <Navigate push to="/orchestrator" />;
|
||||
}
|
||||
if (this.state.redirectToVideoNFT) {
|
||||
return <Navigate push to="/videonft" />;
|
||||
}
|
||||
if (this.state.redirectToLPT) {
|
||||
return <Navigate push to="/livepeer" />;
|
||||
}
|
||||
|
||||
|
||||
var totalVisitorCount = 0;
|
||||
var activeVisitorCount = 0;
|
||||
if (this.props.userstate.visitorStats) {
|
||||
totalVisitorCount = this.props.userstate.visitorStats.totalVisitorCount;
|
||||
activeVisitorCount = this.props.userstate.visitorStats.activeVisitorCount
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stroke" style={{ padding: 0 }}>
|
||||
<div className="row" style={{ margin: 0, padding: 0 }}>
|
||||
<img alt="" src="livepeer.png" width="100em" height="100em" style={{ zIndex: 10 }} />
|
||||
</div>
|
||||
<div className="flexContainer">
|
||||
<div className="stroke roundedOpaque">
|
||||
<div className="row">
|
||||
<h3> Home </h3>
|
||||
</div>
|
||||
<div className="row">
|
||||
<button className="waveButton" onClick={() => {
|
||||
this.setState({ redirectToGrafana: true });
|
||||
}}>
|
||||
<p>Livepeer Transcoder</p>
|
||||
</button>
|
||||
</div>
|
||||
<div className="row">
|
||||
<button className="waveButton" onClick={() => {
|
||||
this.setState({ redirectToLPT: true });
|
||||
}}>
|
||||
<p>Livepeer Blockchain</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="alwaysOnBottom showNeverOnMobile" style={{ margin: 0, padding: 0 }}>
|
||||
<div className="row" style={{ margin: 0, padding: 0 }}>
|
||||
<h4 className="lightText" style={{ margin: 0, padding: 0 }}>
|
||||
Connected as {this.props.session.ip || "?"}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="row" style={{ margin: 0, padding: 0 }}>
|
||||
<h3 className="lightText" style={{ margin: 0, padding: 0 }}>
|
||||
{totalVisitorCount} unique visitors / {activeVisitorCount} of which have interacted with this website
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="alwaysOnBottomRight" style={{ margin: 0, padding: 0 }}>
|
||||
<h6 className="lightText" style={{ margin: 0, padding: 0 }}>
|
||||
nframe.tech / nframe.nl
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Home);
|
19
src/index.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import configureStore from "./store/store";
|
||||
import { Provider } from "react-redux";
|
||||
import { checkLoggedIn } from "./util/session";
|
||||
|
||||
const renderApp = preloadedState => {
|
||||
const store = configureStore(preloadedState);
|
||||
window.state = store.getState;
|
||||
|
||||
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
|
||||
};
|
||||
|
||||
(async () => renderApp(await checkLoggedIn()))();
|
||||
|
||||
|
||||
|
||||
|
186
src/livepeer.js
Normal file
@ -0,0 +1,186 @@
|
||||
import * as React from "react";
|
||||
import './style.css';
|
||||
import {
|
||||
Navigate
|
||||
} from "react-router-dom";
|
||||
import ScrollContainer from 'react-indiana-drag-scroll';
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
getQuotes, getBlockchainData, getEvents
|
||||
} from "./actions/livepeer";
|
||||
import EventButton from "./eventButton";
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
session: state.session,
|
||||
userstate: state.userstate,
|
||||
errors: state.errors,
|
||||
livepeer: state.livepeerstate
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
getQuotes: () => dispatch(getQuotes()),
|
||||
getBlockchainData: () => dispatch(getBlockchainData()),
|
||||
getEvents: () => dispatch(getEvents())
|
||||
});
|
||||
|
||||
class Livepeer extends React.Component {
|
||||
state = {
|
||||
redirectToHome: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.getQuotes();
|
||||
this.props.getBlockchainData();
|
||||
this.props.getEvents();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.redirectToHome) {
|
||||
return <Navigate push to="/" />;
|
||||
}
|
||||
|
||||
let lptPrice = 0;
|
||||
let ethPrice = 0;
|
||||
let lptPriceChange24h = 0;
|
||||
let ethPriceChange24h = 0;
|
||||
if (this.props.livepeer.quotes) {
|
||||
if (this.props.livepeer.quotes.LPT) {
|
||||
lptPrice = this.props.livepeer.quotes.LPT.price;
|
||||
lptPriceChange24h = this.props.livepeer.quotes.LPT.percent_change_24h;
|
||||
}
|
||||
if (this.props.livepeer.quotes.ETH) {
|
||||
ethPrice = this.props.livepeer.quotes.ETH.price;
|
||||
ethPriceChange24h = this.props.livepeer.quotes.ETH.percent_change_24h;
|
||||
}
|
||||
}
|
||||
|
||||
let blockchainTime = 0;
|
||||
let l1Block = 0;
|
||||
let l2Block = 0;
|
||||
let l1GasFeeInGwei = 0;
|
||||
let l2GasFeeInGwei = 0;
|
||||
let redeemRewardCostL1 = 0;
|
||||
let redeemRewardCostL2 = 0;
|
||||
let claimTicketCostL1 = 0;
|
||||
let claimTicketCostL2 = 0;
|
||||
let withdrawFeeCostL1 = 0;
|
||||
let withdrawFeeCostL2 = 0;
|
||||
if (this.props.livepeer.blockchains) {
|
||||
blockchainTime = this.props.livepeer.blockchains.timestamp;
|
||||
l1GasFeeInGwei = this.props.livepeer.blockchains.l1GasFeeInGwei;
|
||||
l2GasFeeInGwei = this.props.livepeer.blockchains.l2GasFeeInGwei;
|
||||
redeemRewardCostL1 = this.props.livepeer.blockchains.redeemRewardCostL1;
|
||||
redeemRewardCostL2 = this.props.livepeer.blockchains.redeemRewardCostL2;
|
||||
claimTicketCostL1 = this.props.livepeer.blockchains.claimTicketCostL1;
|
||||
claimTicketCostL2 = this.props.livepeer.blockchains.claimTicketCostL2;
|
||||
withdrawFeeCostL1 = this.props.livepeer.blockchains.withdrawFeeCostL1;
|
||||
withdrawFeeCostL2 = this.props.livepeer.blockchains.withdrawFeeCostL2;
|
||||
l1Block = this.props.livepeer.blockchains.l1block;
|
||||
l2Block = this.props.livepeer.blockchains.l2block;
|
||||
}
|
||||
|
||||
let redeemRewardCostL1USD;
|
||||
let redeemRewardCostL2USD;
|
||||
let claimTicketCostL1USD;
|
||||
let claimTicketCostL2USD;
|
||||
let withdrawFeeCostL1USD;
|
||||
let withdrawFeeCostL2USD;
|
||||
if (l1GasFeeInGwei && ethPrice) {
|
||||
if (redeemRewardCostL1) {
|
||||
redeemRewardCostL1USD = redeemRewardCostL1 * ethPrice;
|
||||
}
|
||||
if (claimTicketCostL1) {
|
||||
claimTicketCostL1USD = claimTicketCostL1 * ethPrice;
|
||||
}
|
||||
if (withdrawFeeCostL1) {
|
||||
withdrawFeeCostL1USD = withdrawFeeCostL1 * ethPrice;
|
||||
}
|
||||
}
|
||||
if (l2GasFeeInGwei && ethPrice) {
|
||||
if (redeemRewardCostL2) {
|
||||
redeemRewardCostL2USD = redeemRewardCostL2 * ethPrice;
|
||||
}
|
||||
if (claimTicketCostL2) {
|
||||
claimTicketCostL2USD = claimTicketCostL2 * ethPrice;
|
||||
}
|
||||
if (withdrawFeeCostL2) {
|
||||
withdrawFeeCostL2USD = withdrawFeeCostL2 * ethPrice;
|
||||
}
|
||||
}
|
||||
|
||||
let eventsList = [];
|
||||
if (this.props.livepeer.events){
|
||||
eventsList = this.props.livepeer.events;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flexContainer">
|
||||
<div className="stroke" style={{ margin: 0, padding: 0 }}>
|
||||
|
||||
</div>
|
||||
<div className="stroke" style={{ margin: 0, padding: 0 }}>
|
||||
<div className="row" style={{ margin: 0, padding: 0 }}>
|
||||
<button className="homeButton" onClick={() => {
|
||||
this.setState({ redirectToHome: true });
|
||||
}}>
|
||||
<img alt="" src="livepeer.png" width="100em" height="100em" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="roundedOpaque" style={{ padding: 0, width: 'unset' }}>
|
||||
{eventsList.map((eventObj, idx) => {
|
||||
console.log(eventObj);
|
||||
// TODO: make something that groups shit as long as the eventObj.transactionUrl is the same
|
||||
return <EventButton
|
||||
key={eventObj.transactionUrl+idx}
|
||||
transactionUrl={eventObj.transactionUrl}
|
||||
transactionHash={eventObj.transactionHash}
|
||||
name={eventObj.name}
|
||||
data={eventObj.data}
|
||||
address={eventObj.address}
|
||||
/>
|
||||
})}
|
||||
</div>
|
||||
</div >
|
||||
<div className="stroke" style={{ padding: 0 }}>
|
||||
<div className="separator showOnlyOnMobile" />
|
||||
<div className="main-container">
|
||||
<div className="content-wrapper">
|
||||
<ScrollContainer className="overflow-container" hideScrollbars={false}>
|
||||
<div className="overflow-content roundedOpaque" style={{ cursor: 'grab' }}>
|
||||
<h3>Price Info</h3>
|
||||
<h4>Current LPT price: {lptPrice}</h4>
|
||||
<h4>Current LPT price change: {lptPriceChange24h}%</h4>
|
||||
<h4>Current ETH price: {ethPrice}</h4>
|
||||
<h4>Current ETH price change: {ethPriceChange24h}%</h4>
|
||||
<h3>Cost Info</h3>
|
||||
<h5>Last updated @ {blockchainTime}</h5>
|
||||
<h4>Current layer 1 gas fee in GWEI: {l1GasFeeInGwei}</h4>
|
||||
<h4>Current layer 1 is at block: {l1Block}</h4>
|
||||
<h4>Current layer 1 cost to redeem daily reward: {redeemRewardCostL1} eth = ${redeemRewardCostL1USD}</h4>
|
||||
<h4>Current layer 1 cost to claim a winning ticket: {claimTicketCostL1} eth = ${claimTicketCostL1USD}</h4>
|
||||
<h4>Current layer 1 cost to withdraw Eth fees: {withdrawFeeCostL1} eth = ${withdrawFeeCostL1USD}</h4>
|
||||
<h4>Current layer 2 gas fee in GWEI: {l2GasFeeInGwei}</h4>
|
||||
<h4>Current layer 2 is at block: {l2Block}</h4>
|
||||
<h4>Current layer 2 cost to redeem daily reward: {redeemRewardCostL2} eth = ${redeemRewardCostL2USD}</h4>
|
||||
<h4>Current layer 2 cost to claim a winning ticket: {claimTicketCostL2} eth = ${claimTicketCostL2USD}</h4>
|
||||
<h4>Current layer 2 cost to withdraw Eth fees: {withdrawFeeCostL2} eth = ${withdrawFeeCostL2USD}</h4>
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Livepeer);
|
35
src/loadingScreen.js
Normal file
@ -0,0 +1,35 @@
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
getVisitorStats
|
||||
} from "./actions/user";
|
||||
import { login } from "./actions/session";
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
session: state.session,
|
||||
userstate: state.userstate,
|
||||
errors: state.errors
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
getVisitorStats: () => dispatch(getVisitorStats()),
|
||||
login: () => dispatch(login())
|
||||
});
|
||||
|
||||
|
||||
class Startup extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.login();
|
||||
this.props.getVisitorStats();
|
||||
}
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Startup);
|
25
src/reducers/errors/errors.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { RECEIVE_CURRENT_USER } from "../../actions/session";
|
||||
import { CLEAR_ERRORS, RECEIVE_ERRORS, RECEIVE_NOTIFICATIONS } from "../../actions/error";
|
||||
|
||||
export default (state = [], { message, type }) => {
|
||||
Object.freeze(state);
|
||||
switch (type) {
|
||||
case RECEIVE_ERRORS:
|
||||
return [...state, {
|
||||
title: "Foutmelding",
|
||||
message: message,
|
||||
}
|
||||
];
|
||||
case RECEIVE_NOTIFICATIONS:
|
||||
return [...state, {
|
||||
title: message.title,
|
||||
message: message.message,
|
||||
}
|
||||
];
|
||||
case RECEIVE_CURRENT_USER:
|
||||
case CLEAR_ERRORS:
|
||||
return [];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
19
src/reducers/livepeer/livepeerstate.js
Normal file
@ -0,0 +1,19 @@
|
||||
import {
|
||||
RECEIVE_QUOTES,
|
||||
RECEIVE_BLOCKCHAIN_DATA,
|
||||
RECEIVE_EVENTS
|
||||
} from "../../actions/livepeer";
|
||||
|
||||
export default (state = {}, { type, message }) => {
|
||||
Object.freeze(state);
|
||||
switch (type) {
|
||||
case RECEIVE_QUOTES:
|
||||
return { ...state, quotes: message };
|
||||
case RECEIVE_BLOCKCHAIN_DATA:
|
||||
return { ...state, blockchains: message };
|
||||
case RECEIVE_EVENTS:
|
||||
return { ...state, events: message };
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
};
|
13
src/reducers/root.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { combineReducers } from "redux";
|
||||
import errors from "./errors/errors";
|
||||
import session from "./session/session";
|
||||
import userstate from "./userstate/userstate";
|
||||
import livepeerstate from "./livepeer/livepeerstate";
|
||||
{/*Reducers define how the state of the application changes when actions are sent into the store. They take in the current state and the action that was performed.
|
||||
This file combines all reducers in use so they are all accessible for redux*/}
|
||||
export default combineReducers({
|
||||
session,
|
||||
errors,
|
||||
userstate,
|
||||
livepeerstate
|
||||
});
|
13
src/reducers/session/session.js
Normal file
@ -0,0 +1,13 @@
|
||||
import {
|
||||
RECEIVE_CURRENT_USER
|
||||
} from "../../actions/session";
|
||||
const _nullSession = { userId: null, username: null, ip: null }
|
||||
export default (state = _nullSession, { type, user }) => {
|
||||
Object.freeze(state);
|
||||
switch (type) {
|
||||
case RECEIVE_CURRENT_USER:
|
||||
return user;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
16
src/reducers/userstate/userstate.js
Normal file
@ -0,0 +1,16 @@
|
||||
import {
|
||||
RECEIVE_VISITOR_STATS,
|
||||
RECEIVE_CURRENT_USER_VOTES
|
||||
} from "../../actions/user";
|
||||
|
||||
export default (state = {}, { type, message }) => {
|
||||
Object.freeze(state);
|
||||
switch (type) {
|
||||
case RECEIVE_VISITOR_STATS:
|
||||
return { ...state, visitorStats: message};
|
||||
case RECEIVE_CURRENT_USER_VOTES:
|
||||
return { ...state, currentVotes: message};
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
};
|
18
src/store/store.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { createStore, applyMiddleware, compose } from "redux";
|
||||
import thunk from "redux-thunk";
|
||||
import reducer from "../reducers/root";
|
||||
|
||||
|
||||
function configureStore(preloadedState) {
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
const store = createStore(reducer, preloadedState, composeEnhancers(
|
||||
applyMiddleware(thunk)
|
||||
));
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
|
||||
export default (preloadedState) => (
|
||||
configureStore(preloadedState)
|
||||
);
|
550
src/style.css
Normal file
@ -0,0 +1,550 @@
|
||||
a:hover, a:visited, a:link, a:active{
|
||||
text-decoration: none;
|
||||
color: rgba(0, 0, 0, 0.875);
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 5px;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-image: url("/background.jpg");
|
||||
background-repeat: no-repeat center center fixed;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
code, pre{
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
h2, h3, h1, h4, h5, h6 {
|
||||
text-shadow: 0.5px 0.5px 0.8px #948dff;
|
||||
color: #1a1b26;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
a {
|
||||
text-shadow: 0.5px 0.5px 0.8px #948dff;
|
||||
color: #1a1b26;
|
||||
}
|
||||
|
||||
p {
|
||||
text-shadow: 0.5px 0.5px 0.8px #948dff;
|
||||
color: #1a1b26;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
box-shadow: 0px 0px 8px 4px rgba(8, 7, 56, 0.692);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba(146, 144, 196, 0.9);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(51, 50, 78, 0.9);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(25, 25, 59, 0.9);
|
||||
}
|
||||
|
||||
#dvdlogo {
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
-webkit-animation: moveX 10s linear 0s infinite alternate, moveY 17s linear 0s infinite alternate, changeColour 30s, linear, 0s, infinite, normal, none, infinite;
|
||||
-moz-animation: moveX 10s linear 0s infinite alternate, moveY 17s linear 0s infinite alternate, changeColour 30s, linear, 0s, infinite, normal, none, infinite;
|
||||
-o-animation: moveX 10s linear 0s infinite alternate, moveY 17s linear 0s infinite alternate, changeColour 30s, linear, 0s, infinite, normal, none, infinite;
|
||||
animation: moveX 10s linear 0s infinite alternate, moveY 17s linear 0s infinite alternate, changeColour 30s, linear, 0s, infinite, normal, none, infinite;
|
||||
animation-iteration-count:infinite;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
@-webkit-keyframes moveX {
|
||||
from { left: 0; } to { left: calc(100vw - 153px); }
|
||||
}
|
||||
@-moz-keyframes moveX {
|
||||
from { left: 0; } to { left: calc(100vw - 153px); }
|
||||
}
|
||||
@-o-keyframes moveX {
|
||||
from { left: 0; } to { left: calc(100vw - 153px); }
|
||||
}
|
||||
@keyframes moveX {
|
||||
from { left: 0; } to { left: calc(100vw - 153px); }
|
||||
}
|
||||
|
||||
@keyframes changeColour {
|
||||
0% { fill: #ff6969; }
|
||||
14% { fill: #fd9644; }
|
||||
28% { fill: #fed330; }
|
||||
42% { fill: #2dc22d; }
|
||||
56% { fill: #45d8f2; }
|
||||
70% { fill: #5e6cea; }
|
||||
84% { fill: #c22dc2; }
|
||||
100% { fill: #ff6969; }
|
||||
}
|
||||
|
||||
@-webkit-keyframes moveY {
|
||||
from { top: 0; } to { top: calc(100vh - 69px); }
|
||||
}
|
||||
@-moz-keyframes moveY {
|
||||
from { top: 0; } to { top: calc(100vh - 69px); }
|
||||
}
|
||||
@-o-keyframes moveY {
|
||||
from { top: 0; } to { top: calc(100vh - 69px); }
|
||||
}
|
||||
@keyframes moveY {
|
||||
from { top: 0; } to { top: calc(100vh - 69px); }
|
||||
}
|
||||
|
||||
.serviceButton {
|
||||
width: 100%;
|
||||
margin: 0.4em;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
text-decoration: none;
|
||||
color: rgba(0, 0, 0, 0.875);
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.camBox {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.cam {
|
||||
display: flex;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
align-self: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.fullGrafana {
|
||||
width: 900px;
|
||||
}
|
||||
.halfGrafana {
|
||||
width: 450px;
|
||||
}
|
||||
|
||||
.lightText {
|
||||
color: rgba(162, 161, 255, 0.5);
|
||||
}
|
||||
|
||||
.hostinfo {
|
||||
cursor: default;
|
||||
text-align: start;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
user-select: text;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: x-small;
|
||||
color: rgba(15, 15, 15, 0.8750);
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
-webkit-box-shadow: inset 3px 3px 12px 2px rgba(28, 28, 170, 0.2);
|
||||
-moz-box-shadow: inset 3px 3px 12px 2px rgba(28, 28, 170, 0.2);
|
||||
box-shadow: inset 3px 3px 12px 2px rgba(28, 28, 170, 0.2);
|
||||
}
|
||||
|
||||
.flexContainer {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.stroke {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-top: 20px;
|
||||
margin: 10px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.row {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.flexItem {
|
||||
padding: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 10px;
|
||||
line-height: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.waveButton {
|
||||
min-width: 200px;
|
||||
cursor: pointer;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(163, 161, 255, 0.9);
|
||||
box-shadow: 4px 5px 3px 2px rgba(8, 7, 56, 0.692);
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
z-index: 3;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.waveButton:hover {
|
||||
background-color: rgba(122, 121, 207, 0.9);
|
||||
}
|
||||
.waveButton:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.homeButton {
|
||||
min-width: 200px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
background-color: transparent;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
z-index: 10;
|
||||
}
|
||||
.homeButton:hover {
|
||||
box-shadow: 4px 5px 3px 2px rgba(8, 7, 56, 0.692);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.homeButton:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.searchField {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
background-color: rgba(44, 96, 238, 0.199);
|
||||
padding: 5px;
|
||||
border-radius: 20px;
|
||||
color:rgba(8, 7, 56, 0.747);
|
||||
}
|
||||
.searchField::placeholder {
|
||||
color:rgba(8, 7, 56, 0.445);
|
||||
}
|
||||
.searchField:focus {
|
||||
outline: none !important;
|
||||
border:2px solid rgba(49, 13, 134, 0.459);
|
||||
box-shadow: 0px 0px 3px 2px rgba(53, 118, 138, 0.8);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
height: calc(100vh - 40px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fixed-container {
|
||||
height: 50px;
|
||||
padding: 10px;
|
||||
border-radius: 50px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0px; /* IMPORTANT: you need this for non-chrome browsers */
|
||||
}
|
||||
|
||||
.overflow-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
.overflow-content {
|
||||
color: black;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.noCursor {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.roundedOpaque {
|
||||
background-color: rgba(180, 175, 252, 0.80);
|
||||
box-shadow: 9px 13px 18px 8px rgba(8, 7, 56, 0.692);
|
||||
border-radius: 30px;
|
||||
box-sizing: border-box;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.mainContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.dataContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bio {
|
||||
text-align: center;
|
||||
color: black;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.waveDiv {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.blockyBlockRoundedCornersWazzup{
|
||||
background-color: rgba(163, 161, 255, 0.9);
|
||||
margin-top: 6px;
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.stilo {
|
||||
-webkit-user-select: none;
|
||||
background-color: hsl(0, 0%, 90%);
|
||||
transition: background-color 300ms;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
padding: 20px;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.showOnlyOnMobile {
|
||||
visibility: hidden;
|
||||
display:none;
|
||||
}
|
||||
|
||||
.showNeverOnMobile {
|
||||
visibility: visible;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.mistvideo {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: calc(50vw);
|
||||
height: calc((9 / 16 ) * 50vw);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border:none;
|
||||
border-top:25px dotted rgba(56, 19, 124, 0.6);
|
||||
width: 30vw;
|
||||
height: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mistvideo-controls svg.mist.icon:hover .fill,
|
||||
.mistvideo-controls svg.mist.icon:hover .semiFill,
|
||||
.mistvideo-volume_container:hover .fill,
|
||||
.mistvideo-volume_container:hover .semiFill {
|
||||
fill: rgba(179, 14, 14, 0.875);
|
||||
}
|
||||
.mistvideo-controls svg.mist.icon:hover .stroke,
|
||||
.mistvideo-volume_container:hover .stroke {
|
||||
stroke: rgba(179, 14, 14, 0.875);
|
||||
}
|
||||
|
||||
.markdown {
|
||||
padding: 50px;
|
||||
width: 50vw;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
align-self: center;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.centered {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.alwaysOnBottom {
|
||||
z-index: 0;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 0px;
|
||||
transform: translate(-50%, 0);
|
||||
margin: 0 auto;
|
||||
user-select: none;
|
||||
}
|
||||
.alwaysOnBottomRight {
|
||||
z-index: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0px;
|
||||
/* transform: translate(-50%, -50%); */
|
||||
margin: 0 auto;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (max-aspect-ratio: 1/1) {
|
||||
.fullGrafana {
|
||||
width: calc(100vw - 2em);
|
||||
}
|
||||
.halfGrafana {
|
||||
width: calc(100vw - 2em);
|
||||
}
|
||||
.shrinkSize {
|
||||
width: 50%;
|
||||
}
|
||||
.flexContainer {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.roundedOpaque {
|
||||
background-color: none;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.stroke {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
.showOnlyOnMobile {
|
||||
visibility: visible;
|
||||
display:block;
|
||||
}
|
||||
.showNeverOnMobile {
|
||||
visibility: hidden;
|
||||
display:none;
|
||||
}
|
||||
.mistvideo {
|
||||
width: calc(100vw - 20px);
|
||||
height: calc((9 / 16 ) * (100vw - 20px));
|
||||
}
|
||||
.mobileNoPadding {
|
||||
padding: 0;
|
||||
}
|
||||
.main-container {
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
.markdown {
|
||||
width: 80vw;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
28
src/util/livepeer.js
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
export const getQuotes = () => (
|
||||
fetch("api/livepeer/quotes", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export const getBlockchainData = () => (
|
||||
fetch("api/livepeer/blockchains", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export const getEvents = () => (
|
||||
fetch("api/livepeer/getEvents", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
);
|
35
src/util/session.js
Normal file
@ -0,0 +1,35 @@
|
||||
export const login = user => (
|
||||
fetch("api/session", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export const signup = user => (
|
||||
fetch("api/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export const logout = () => (
|
||||
fetch("api/session", { method: "DELETE" })
|
||||
);
|
||||
|
||||
export const checkLoggedIn = async () => {
|
||||
const response = await fetch('/api/session');
|
||||
const { user } = await response.json();
|
||||
let preloadedState = {};
|
||||
if (user) {
|
||||
preloadedState = {
|
||||
session: user
|
||||
};
|
||||
}
|
||||
return preloadedState;
|
||||
};
|
40
src/util/user.js
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
|
||||
export const getVisitorStats = () => (
|
||||
fetch("api/users/getVisitorStats", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export const getCurrentUserVotes = () => (
|
||||
fetch("api/users/getCurrentUserVotes", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export const getScoreByTimelapeFilename = (fullFilename) => (
|
||||
fetch("api/users/getScoreByTimelapeFilename", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ fullFilename }),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export const setVoteOnTimelapse = (voteValue, fullFilename) => (
|
||||
fetch("api/users/setVoteOnTimelapse", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ voteValue, fullFilename }),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|