Fixes for the Tour
This commit is contained in:
parent
ddf534fea6
commit
c0b0d66467
21 changed files with 812 additions and 628 deletions
|
@ -13,30 +13,41 @@ if (window.location.href.search && window.location.href.split("?url=")[1]) { //
|
|||
|
||||
|
||||
|
||||
$("body").on("click", "[data-action]", event => {
|
||||
event.preventDefault();
|
||||
document.querySelector("body").addEventListener("click", event => {
|
||||
if (event.target.dataset.action) {
|
||||
event.preventDefault();
|
||||
document.querySelector(".tour").classList.add("waiting");
|
||||
handleExamples(event.target);
|
||||
}
|
||||
|
||||
$(".tour").addClass("waiting");
|
||||
if (
|
||||
event.explicitOriginalTarget.classList &&
|
||||
event.explicitOriginalTarget.classList[0] === "tour__content__meme__canvas__thumbnail"
|
||||
) {
|
||||
for (const thumbnail of document.querySelectorAll(".tour__content__meme__canvas__thumbnail")) {
|
||||
thumbnail.classList.remove("selected");
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
handleExamples(event);
|
||||
$(".tour").removeClass("waiting");
|
||||
}, 2500); // "rate-limit" to allow example divs time to populate
|
||||
event.explicitOriginalTarget.classList.add("selected");
|
||||
updateCanvas(event.explicitOriginalTarget);
|
||||
}
|
||||
});
|
||||
|
||||
$("body").on("click", ".tour__content__meme__canvas__thumbnail", event => {
|
||||
$(".tour__content__meme__canvas__thumbnail").removeClass("selected");
|
||||
|
||||
event.currentTarget.className += " selected";
|
||||
updateCanvas(event.currentTarget);
|
||||
});
|
||||
|
||||
$("#fetch-claim-uri").on("keyup", event => {
|
||||
document.getElementById("fetch-claim-uri").addEventListener("keyup", event => {
|
||||
const key = event.keyCode ? event.keyCode : event.which;
|
||||
if (key === 13 && $("#fetch-claim-uri").val()) fetchMetadata(1, $("#fetch-claim-uri").val());
|
||||
|
||||
if (
|
||||
key === 13 &&
|
||||
document.getElementById("fetch-claim-uri").value.length > 0
|
||||
) fetchMetadata(1, document.getElementById("fetch-claim-uri").value);
|
||||
});
|
||||
|
||||
$("body").on("keyup", "#meme-top-line, #meme-bottom-line", () => updateCanvas());
|
||||
document.querySelector("body").addEventListener("keyup", event => {
|
||||
if (
|
||||
event.target.id === "meme-top-line" ||
|
||||
event.target.id === "meme-bottom-line"
|
||||
) updateCanvas();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
@ -94,17 +105,18 @@ function debounce(func, wait, immediate) {
|
|||
}
|
||||
|
||||
function initializeTour() {
|
||||
$(".tour").addClass("waiting");
|
||||
$("#fetch-claim-uri").val("").focus(); // reset
|
||||
$(".tour__sidebar__example:nth-child(1)").addClass("active");
|
||||
document.querySelector(".tour").classList.add("waiting");
|
||||
document.querySelector("#fetch-claim-uri").value = "";
|
||||
document.querySelector("#fetch-claim-uri").focus();
|
||||
document.querySelector(".tour__navigation__example:nth-child(1)").classList.add("active");
|
||||
|
||||
send(JSON.stringify({
|
||||
"message": "landed on tour"
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
$(".tour").removeClass("waiting");
|
||||
}, 2500);
|
||||
document.querySelector(".tour__navigation__example:nth-child(1)").click();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
|
||||
|
@ -200,10 +212,10 @@ function getMemeInfo() { // TODO: Error handling
|
|||
|
||||
const handleExamples = debounce(event => {
|
||||
let exampleNumber;
|
||||
const data = event.currentTarget.dataset;
|
||||
const data = event.dataset;
|
||||
|
||||
if (!parseInt($(".tour__sidebar__example.active")[0].dataset.example)) return;
|
||||
exampleNumber = parseInt($(".tour__sidebar__example.active")[0].dataset.example);
|
||||
if (!parseInt($(".tour__navigation__example.active")[0].dataset.example)) return;
|
||||
exampleNumber = parseInt($(".tour__navigation__example.active")[0].dataset.example);
|
||||
|
||||
switch(data.action) {
|
||||
case "choose claim":
|
||||
|
@ -224,8 +236,8 @@ const handleExamples = debounce(event => {
|
|||
$("#tour-url button").text("Resolve");
|
||||
if ($("#tour-url")[0].style.display === "none") $("#tour-url").show();
|
||||
|
||||
$(".tour__sidebar__example").removeClass("active");
|
||||
$(".tour__sidebar__example:nth-child(1)").addClass("active");
|
||||
$(".tour__navigation__example").removeClass("active");
|
||||
$(".tour__navigation__example:nth-child(1)").addClass("active");
|
||||
|
||||
$("#tour-loader").empty().show();
|
||||
$("#tour-results").empty().show();
|
||||
|
@ -244,8 +256,8 @@ const handleExamples = debounce(event => {
|
|||
$("#fetch-claim-uri").val(""); // reset URL bar
|
||||
$("#tour-url").hide();
|
||||
|
||||
$(".tour__sidebar__example").removeClass("active");
|
||||
$(".tour__sidebar__example:nth-child(2)").addClass("active");
|
||||
$(".tour__navigation__example").removeClass("active");
|
||||
$(".tour__navigation__example:nth-child(2)").addClass("active");
|
||||
|
||||
$("#tour-loader").empty().show();
|
||||
$("#tour-results").empty().show();
|
||||
|
@ -266,8 +278,8 @@ const handleExamples = debounce(event => {
|
|||
// $("#tour-url").after("<p>In the LBRY app, you can financially support your favorite creators by donating LBRY Coin (LBC). In this example, we are donating LBC in your stead.</p>");
|
||||
if ($("#tour-url")[0].style.display === "none") $("#tour-url").show();
|
||||
|
||||
$(".tour__sidebar__example").removeClass("active");
|
||||
$(".tour__sidebar__example:nth-child(3)").addClass("active");
|
||||
$(".tour__navigation__example").removeClass("active");
|
||||
$(".tour__navigation__example:nth-child(3)").addClass("active");
|
||||
|
||||
$("#tour-loader").empty().show();
|
||||
$("#tour-results").empty().show();
|
||||
|
|
|
@ -39,30 +39,29 @@ module.exports = exports = (state, emit) => {
|
|||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||
|
||||
<!--/ Open Graph /-->
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:title" content="${config.meta.title}"/>
|
||||
<meta property="og:url" content="https://lbry.tech${state.href}"/>
|
||||
<meta property="og:site_name" content="${config.meta.title}"/>
|
||||
<meta property="og:image" content="/assets/images/apple-touch-icon.png"/>
|
||||
<meta property="og:image" content="/assets/media/images/og-image.png"/>
|
||||
<meta property="og:image:height" content="720"/>
|
||||
<meta property="og:image:width" content="1280"/>
|
||||
<meta property="og:locale" content="en_US"/>
|
||||
<meta property="og:site_name" content="${config.meta.title}"/>
|
||||
<meta property="og:title" content="${title}"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:url" content="https://lbry.tech${state.href}"/>
|
||||
|
||||
<!--/ Social/App Stuff /-->
|
||||
<meta name="apple-mobile-web-app-title" content="${config.meta.title}"/>
|
||||
<meta name="application-name" content="${config.meta.title}"/>
|
||||
<meta name="msapplication-TileColor" content="${config.meta.color}"/>
|
||||
<meta name="msapplication-TileImage" content="/assets/images/apple-touch-icon.png"/>
|
||||
<meta name="msapplication-TileImage" content="/assets/apple-touch-icon.png"/>
|
||||
<meta name="theme-color" content="${config.meta.color}"/>
|
||||
|
||||
<link rel="apple-touch-icon" href="/assets/images/apple-touch-icon.png"/>
|
||||
<link rel="icon" href="/assets/images/favicon.svg" type="image/svg+xml"/>
|
||||
<link rel="mask-icon" href="/assets/images/favicon.svg" color="${config.meta.color}"/>
|
||||
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png"/>
|
||||
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml"/>
|
||||
<link rel="mask-icon" href="/assets/favicon.svg" color="${config.meta.color}"/>
|
||||
<link rel="shortcut icon" href="/assets/favicon.ico"/>
|
||||
|
||||
<link href="/assets/bundle.css" rel="stylesheet"/>
|
||||
<link rel="stylesheet" href="/assets/bundle.css"/>
|
||||
|
||||
<script src="/assets/scripts/vendor/zepto.js"></script>
|
||||
|
||||
<script>const ws = new WebSocket(location.origin.replace(/^http/, "ws"));</script>
|
||||
<script src="/assets/scripts/sockets.js"></script>
|
||||
`;
|
||||
};
|
||||
|
|
|
@ -30,9 +30,9 @@ export default class Navigation extends Nanocomponent {
|
|||
url: "/overview"
|
||||
},
|
||||
{
|
||||
name: "Tour",
|
||||
title: "Take a Tour",
|
||||
url: "/tour"
|
||||
name: "Playground",
|
||||
title: "Experience LBRY",
|
||||
url: "/playground"
|
||||
},
|
||||
{
|
||||
name: "Resources",
|
||||
|
|
79
app/components/playground.js
Normal file
79
app/components/playground.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
"use strict";
|
||||
|
||||
|
||||
|
||||
// P A C K A G E S
|
||||
|
||||
import dedent from "dedent";
|
||||
import html from "choo/html";
|
||||
import raw from "choo/html/raw";
|
||||
|
||||
|
||||
|
||||
// E X P O R T
|
||||
|
||||
export default function () {
|
||||
return dedent`
|
||||
<section class="tour">
|
||||
<ul class="tour__navigation">
|
||||
${raw(navigation())}
|
||||
</ul>
|
||||
<p class="tour__description" id="tour-example-description"></p>
|
||||
<section class="tour__content">${raw(example1())}</section>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// H E L P E R S
|
||||
|
||||
function example1() {
|
||||
return html`
|
||||
<div class="tour__content__urlbar" id="tour-url">
|
||||
<span>lbry://</span><input id="fetch-claim-uri" placeholder=" Enter a LBRY address or select a video below" type="text"/>
|
||||
<button class="button" data-action="execute claim" type="button">Resolve</button>
|
||||
</div>
|
||||
|
||||
<div class="tour__content__trends" id="tour-loader"></div>
|
||||
<div id="tour-results"></div>
|
||||
|
||||
<script>
|
||||
document.getElementById("tour-example-description").textContent = document.querySelector("[data-action='tour, example 1']").dataset.description
|
||||
</script>
|
||||
`;
|
||||
}
|
||||
|
||||
function navigation() { // TODO: Save tutorial position to localStorage
|
||||
return dedent`
|
||||
<li
|
||||
class="tour__navigation__example"
|
||||
data-action="tour, example 1"
|
||||
data-description="In this example, you can see what runs under the hood when selecting content to view in the LBRY app."
|
||||
data-example="1"
|
||||
>
|
||||
<button type="button">Resolve</button>
|
||||
<span>Get details of media (aka, "claim" metadata)</span>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="tour__navigation__example"
|
||||
data-action="tour, example 2"
|
||||
data-description="Sometimes you want to create content, not just consume it. In this example, you can create a meme and upload it to LBRY!"
|
||||
data-example="2"
|
||||
>
|
||||
<button type="button">Publish</button>
|
||||
<span>Create a meme and upload it to the LBRY blockchain</span>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="tour__navigation__example"
|
||||
data-action="tour, example 3"
|
||||
data-description="In the LBRY app, you can financially support your favorite creators by donating LBRY Coin (LBC). In this example, we are donating LBC in your stead."
|
||||
data-example="3"
|
||||
>
|
||||
<button type="button">Support</button>
|
||||
<span>Support creators on LBRY with a tip, on us!</span>
|
||||
</li>
|
||||
`;
|
||||
}
|
BIN
app/dist/media/images/og-image.png
vendored
Normal file
BIN
app/dist/media/images/og-image.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
72
app/dist/scripts/sockets.js
vendored
72
app/dist/scripts/sockets.js
vendored
|
@ -1,34 +1,60 @@
|
|||
/* global $, log, ws */ "use strict";
|
||||
"use strict";
|
||||
|
||||
|
||||
|
||||
// const log = console.log; // eslint-disable-line
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initializeWebSocketConnection();
|
||||
setInterval(checkWebSocketConnection, 5000);
|
||||
});
|
||||
|
||||
|
||||
|
||||
ws.onmessage = socket => {
|
||||
const data = JSON.parse(socket.data);
|
||||
let ws = null;
|
||||
|
||||
switch (true) {
|
||||
case data.message === "updated html":
|
||||
$(data.selector).html(data.html);
|
||||
$("#emailMessage").val("");
|
||||
break;
|
||||
function checkWebSocketConnection() {
|
||||
if (!ws || ws.readyState === 3) initializeWebSocketConnection();
|
||||
}
|
||||
|
||||
case data.message === "notification": // TODO: Make work with appending so multiple notifications can be sent
|
||||
$("#flash-container").html(`<div class="flash active${data.type ? " " + data.type : ""}">${data.details}</div>`);
|
||||
function initializeWebSocketConnection() {
|
||||
ws = new WebSocket(location.origin.replace(/^http/, "ws"));
|
||||
|
||||
setTimeout(() => {
|
||||
$("#flash-container").html("");
|
||||
}, 2100);
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connection established"); // eslint-disable-line
|
||||
};
|
||||
|
||||
break;
|
||||
ws.onmessage = socket => {
|
||||
const data = JSON.parse(socket.data);
|
||||
|
||||
default:
|
||||
log(data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
switch (true) {
|
||||
case data.message === "updated html":
|
||||
document.querySelector(data.selector).innerHTML = data.html;
|
||||
document.getElementById("emailAddress").value = "";
|
||||
document.getElementById("emailMessage").innerHTML = "";
|
||||
if (document.getElementById("temp-loader")) document.getElementById("temp-loader").style.display = "none";
|
||||
document.querySelector(".tour").classList.remove("waiting");
|
||||
break;
|
||||
|
||||
case data.message === "notification": // TODO: Make work with appending so multiple notifications can be sent
|
||||
document.getElementById("flash-container").innerHTML =
|
||||
`<div class="flash active${data.type ? " " + data.type : ""}">${data.details}</div>`;
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("flash-container").innerHTML = "";
|
||||
}, 2100);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(data); // eslint-disable-line
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("WebSocket connection lost"); // eslint-disable-line
|
||||
checkWebSocketConnection(); // reconnect now
|
||||
};
|
||||
}
|
||||
|
||||
function send(msg) { // eslint-disable-line
|
||||
socketReady(ws, () => ws.send(msg));
|
||||
|
@ -36,11 +62,11 @@ function send(msg) { // eslint-disable-line
|
|||
|
||||
function socketReady(socket, callback) {
|
||||
setTimeout(() => {
|
||||
if (socket.readyState === 1) {
|
||||
if (socket && socket.readyState === 1) {
|
||||
if (callback !== undefined) callback();
|
||||
return;
|
||||
} else {
|
||||
socketReady(socket, callback);
|
||||
}
|
||||
|
||||
return socketReady(socket, callback);
|
||||
}, 5);
|
||||
}
|
||||
|
|
|
@ -12,11 +12,11 @@ const stringifyObject = require("stringify-object");
|
|||
|
||||
// V A R I A B L E S
|
||||
|
||||
const randomString = local("/app/helpers/random-string");
|
||||
const randomString = local("app/helpers/random-string");
|
||||
const loadLanguages = require("prismjs/components/");
|
||||
const logSlackError = local("/app/helpers/slack");
|
||||
const publishMeme = local("/app/helpers/publish-meme");
|
||||
const uploadImage = local("/app/helpers/upload-image");
|
||||
const logSlackError = local("app/helpers/slack");
|
||||
const publishMeme = local("app/helpers/publish-meme");
|
||||
const uploadImage = local("app/helpers/upload-image");
|
||||
|
||||
loadLanguages(["json"]);
|
||||
|
||||
|
@ -126,7 +126,6 @@ module.exports = exports = (data, socket) => {
|
|||
<h3>Response</h3>
|
||||
${explorerNotice}
|
||||
<pre><code class="language-json">${renderedCode}</code></pre>
|
||||
<script>$("#temp-loader").hide();</script>
|
||||
`),
|
||||
"message": "updated html",
|
||||
"selector": `#example${data.example}-result`
|
||||
|
@ -195,7 +194,6 @@ module.exports = exports = (data, socket) => {
|
|||
<h3>Response</h3>
|
||||
${explorerNotice}
|
||||
<pre><code class="language-json">${renderedCode}</code></pre>
|
||||
<script>$("#temp-loader").hide();</script>
|
||||
`),
|
||||
"message": "updated html",
|
||||
"selector": `#example${data.example}-result`
|
||||
|
|
|
@ -2,6 +2,46 @@
|
|||
|
||||
|
||||
|
||||
// P A C K A G E S
|
||||
|
||||
const async = require("async");
|
||||
const color = require("colorette");
|
||||
const local = require("app-root-path").require;
|
||||
const octokit = require("@octokit/rest")();
|
||||
const redis = require("redis");
|
||||
|
||||
// V A R I A B L E S
|
||||
|
||||
const logSlackError = local("app/helpers/slack");
|
||||
const relativeDate = local("app/modules/relative-date");
|
||||
let client;
|
||||
|
||||
// R E D I S
|
||||
|
||||
if (typeof process.env.GITHUB_OAUTH_TOKEN !== "undefined") {
|
||||
octokit.authenticate({
|
||||
type: "oauth",
|
||||
token: process.env.GITHUB_OAUTH_TOKEN
|
||||
});
|
||||
} else process.stdout.write(`${color.red("[missing]")} GitHub token`);
|
||||
|
||||
if (typeof process.env.REDISCLOUD_URL !== "undefined") {
|
||||
client = redis.createClient(process.env.REDISCLOUD_URL);
|
||||
|
||||
client.on("error", redisError => {
|
||||
process.env.NODE_ENV === "development" ?
|
||||
process.stdout.write(`\n${color.yellow("Unable to connect to Redis client.")}\nYou may be missing an .env file or your connection was reset.`) :
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *REDIS ERROR:* ```" + JSON.parse(JSON.stringify(redisError)) + "```" + "\n" +
|
||||
"> _Cause: Someone is trying to run LBRY.tech locally without environment variables OR Heroku is busted_\n"
|
||||
)
|
||||
;
|
||||
});
|
||||
} else process.stdout.write(`${color.red("[missing]")} Redis client URL`);
|
||||
|
||||
|
||||
|
||||
// P R O G R A M
|
||||
|
||||
function generateEvent(event) {
|
||||
|
@ -84,6 +124,44 @@ function generateEvent(event) {
|
|||
}
|
||||
}
|
||||
|
||||
function generateGitHubFeed(displayGitHubFeed) {
|
||||
if (typeof process.env.REDISCLOUD_URL !== "undefined") {
|
||||
client.zrevrange("events", 0, 9, (err, reply) => {
|
||||
if (err) return; // TODO: Render a div with nice error message
|
||||
|
||||
const events = [];
|
||||
const renderedEvents = [];
|
||||
|
||||
reply.forEach(item => events.push(JSON.parse(item)));
|
||||
|
||||
for (const event of events) {
|
||||
renderedEvents.push(`
|
||||
<div class='github-feed__event'>
|
||||
<a href="${generateUrl("actor", event)}" target="_blank" rel="noopener noreferrer">
|
||||
<img src="${event.actor.avatar_url}" class="github-feed__event__avatar" alt=""/>
|
||||
</a>
|
||||
|
||||
<p>
|
||||
${generateEvent(event)}
|
||||
<a href="${generateUrl("repo", event)}" title="View this repo on GitHub" target="_blank" rel="noopener noreferrer"><strong>${event.repo.name}</strong></a>
|
||||
<em class="github-feed__event__time">${relativeDate(new Date(event.created_at))}</em>
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
updateGithubFeed(); // TODO: Update `.last-updated` every minute
|
||||
|
||||
displayGitHubFeed(`
|
||||
<h3>GitHub</h3>
|
||||
<h5 class="last-updated">Last updated: ${new Date().format("YYYY-MM-DD").replace(/-/g, "·")} at ${new Date().add(-4, "hours").format("UTC:H:mm:ss A").toLowerCase()} EST</h5>
|
||||
|
||||
${renderedEvents.join("")}
|
||||
`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function generateUrl(type, event) {
|
||||
switch (type) {
|
||||
case "actor":
|
||||
|
@ -115,6 +193,29 @@ function generateUrl(type, event) {
|
|||
}
|
||||
}
|
||||
|
||||
function updateGithubFeed() {
|
||||
octokit.activity.getEventsForOrg({
|
||||
org: "lbryio",
|
||||
per_page: 20,
|
||||
page: 1
|
||||
}).then(({ data }) => {
|
||||
async.eachSeries(data, (item, callback) => {
|
||||
const eventString = JSON.stringify(item);
|
||||
|
||||
client.zrank("events", eventString, (err, reply) => {
|
||||
if (reply === null) client.zadd("events", item.id, eventString, callback);
|
||||
else callback();
|
||||
});
|
||||
}, () => client.zremrangebyrank("events", 0, -51)); // Keep the latest 50 events
|
||||
}).catch(err => {
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *GITHUB FEED ERROR:* ```" + JSON.parse(JSON.stringify(err)) + "```" + "\n" +
|
||||
"> _Cause: GitHub feed refresh_\n"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// H E L P E R
|
||||
|
@ -129,5 +230,7 @@ function refToBranch(ref) {
|
|||
|
||||
module.exports = exports = {
|
||||
generateEvent,
|
||||
generateUrl
|
||||
generateGitHubFeed,
|
||||
generateUrl,
|
||||
updateGithubFeed
|
||||
};
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
// "use strict"; require("make-promises-safe"); const app = require("./server.js"); // eslint-disable-line
|
||||
|
||||
"use strict"; require("@babel/register"); require("@babel/polyfill");
|
||||
module.exports = exports = require("./client.js");
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"partials/navigation",
|
||||
"partials/mission-statement",
|
||||
"partials/modal",
|
||||
"partials/pre",
|
||||
|
||||
"layout",
|
||||
|
||||
|
|
|
@ -137,6 +137,8 @@
|
|||
|
||||
.__loading {
|
||||
width: 100%; height: 10rem;
|
||||
|
||||
cursor: wait;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
|
@ -149,12 +151,14 @@
|
|||
border-top-color: $teal;
|
||||
border-width: 6px;
|
||||
content: "";
|
||||
cursor: wait;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 7rem; left: 0;
|
||||
|
||||
cursor: wait;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
|
|
|
@ -179,36 +179,7 @@
|
|||
}
|
||||
|
||||
pre {
|
||||
margin-bottom: 2rem; padding: 2rem;
|
||||
|
||||
border-radius: 3px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.33;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
&:not([class]),
|
||||
&.language-text {
|
||||
background-color: #27283e;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.language-yaml {
|
||||
background-color: #27273f;
|
||||
color: #ffe066;
|
||||
|
||||
.atrule {
|
||||
color: #f083ac;
|
||||
}
|
||||
|
||||
.important {
|
||||
color: #ffa94d;
|
||||
}
|
||||
|
||||
.punctuation {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2, h3, h4, h5 {
|
||||
|
|
|
@ -147,6 +147,10 @@
|
|||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid rgba($white, 0.1);
|
||||
border-radius: 0.3rem;
|
||||
|
@ -171,16 +175,6 @@
|
|||
tr:nth-child(even) {
|
||||
background-color: rgba($white, 0.1);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-bottom: 2rem; padding: 1rem;
|
||||
|
||||
border-radius: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.33;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.api__content__body {
|
||||
|
|
|
@ -31,46 +31,47 @@
|
|||
|
||||
|
||||
/**
|
||||
* Tour | Sidebar
|
||||
* Tour | Navigation
|
||||
*
|
||||
* @class .tour__sidebar
|
||||
* @class .tour__navigation
|
||||
*
|
||||
* @class .tour__sidebar__example
|
||||
* @class .tour__navigation__example
|
||||
* @selector {::before}
|
||||
* @selector {:last-of-type}
|
||||
* @state {.active}
|
||||
* @state {:hover}
|
||||
*/
|
||||
|
||||
.tour__sidebar {
|
||||
width: 250px; height: 100%;
|
||||
.tour__navigation {
|
||||
width: 100%;
|
||||
|
||||
float: left;
|
||||
list-style-type: none;
|
||||
padding-top: 1rem;
|
||||
padding-right: 1rem;
|
||||
vertical-align: top;
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1.5rem;
|
||||
|
||||
&::after {
|
||||
@include clearfix;
|
||||
}
|
||||
}
|
||||
|
||||
.tour__sidebar__example {
|
||||
.tour__navigation__example {
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 33.333333%;
|
||||
|
||||
&::before {
|
||||
width: 1rem; height: 1rem;
|
||||
top: 0.5rem; left: 0;
|
||||
width: 100%; height: 2.5rem;
|
||||
top: -0.6rem; left: 0;
|
||||
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
content: attr(data-example);
|
||||
font-size: 0.8rem;
|
||||
content: "example " attr(data-example);
|
||||
font-size: 0.6rem;
|
||||
font-style: italic;
|
||||
line-height: 1.1;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: 1.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
|
@ -93,6 +94,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
&::after {
|
||||
width: 100%; height: 100%;
|
||||
top: 0; left: 0;
|
||||
|
||||
background-color: rgba($white, 0.7);
|
||||
content: "✓";
|
||||
font-size: 3rem;
|
||||
line-height: 0.85;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
&::before,
|
||||
button,
|
||||
span {
|
||||
|
@ -103,7 +118,6 @@
|
|||
background-color: transparent;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
padding-left: 1.3rem;
|
||||
}
|
||||
|
||||
span {
|
||||
|
@ -139,12 +153,10 @@
|
|||
*/
|
||||
|
||||
.tour__content {
|
||||
width: calc(100% - 250px); height: 100%; min-height: 500px;
|
||||
|
||||
border-left: 1px solid rgba($black, 0.05);
|
||||
float: right;
|
||||
padding: 1rem 0 1rem 1rem;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid rgba($black, 0.05);
|
||||
overflow-y: visible;
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
|
||||
.loader {
|
||||
@extend .__loading;
|
||||
|
@ -153,6 +165,10 @@
|
|||
content: "Processing request";
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tour__content__meme {
|
||||
|
@ -167,6 +183,7 @@
|
|||
.tour__content__meme__canvas {
|
||||
float: left;
|
||||
margin-right: 2%;
|
||||
position: relative;
|
||||
width: 48%;
|
||||
|
||||
canvas {
|
||||
|
@ -202,7 +219,7 @@
|
|||
|
||||
.tour__content__meme__editor {
|
||||
float: right;
|
||||
width: 48%;
|
||||
width: 50%;
|
||||
|
||||
h2.__metadata {
|
||||
margin-top: 3rem;
|
||||
|
@ -315,10 +332,11 @@
|
|||
}
|
||||
|
||||
.tour__content__trends {
|
||||
min-width: 0; min-height: 0;
|
||||
|
||||
display: grid;
|
||||
grid-gap: 2%;
|
||||
grid-template-columns: 32% 32% 32%;
|
||||
overflow-y: auto;
|
||||
grid-gap: 1rem;
|
||||
grid-template: repeat(1, 1fr) / repeat(3, 1fr);
|
||||
position: relative;
|
||||
|
||||
&:empty {
|
||||
|
@ -332,7 +350,7 @@
|
|||
|
||||
.tour__content__trend {
|
||||
img {
|
||||
width: 100%; height: 175px;
|
||||
width: 100%; height: 213px;
|
||||
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
|
@ -428,3 +446,20 @@
|
|||
width: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Tour | Description
|
||||
*
|
||||
* @class .tour__description
|
||||
*/
|
||||
|
||||
.tour__description {
|
||||
background-color: rgba($black, 0.05);
|
||||
cursor: default;
|
||||
font-size: 1rem;
|
||||
line-height: 1.33;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
letter-spacing: 0.1rem;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 1301px) {
|
||||
top: 2.15rem; left: 0;
|
||||
|
@ -58,6 +57,7 @@
|
|||
color: rgba($black, 0.045);
|
||||
font-size: 4rem;
|
||||
position: absolute;
|
||||
width: calc(100% - (1rem + 5%));
|
||||
}
|
||||
|
||||
@media (max-width: 1300px) {
|
||||
|
|
30
app/sass/partials/_pre.scss
Normal file
30
app/sass/partials/_pre.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
pre {
|
||||
margin-bottom: 2rem; padding: 2rem;
|
||||
|
||||
line-height: 1.33;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
&:not([class]),
|
||||
&.language-text {
|
||||
background-color: #27283e;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.language-yaml {
|
||||
background-color: #27273f;
|
||||
color: #ffe066;
|
||||
|
||||
.atrule {
|
||||
color: #f083ac;
|
||||
}
|
||||
|
||||
.important {
|
||||
color: #ffa94d;
|
||||
}
|
||||
|
||||
.punctuation {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
394
app/sockets.js
Normal file
394
app/sockets.js
Normal file
|
@ -0,0 +1,394 @@
|
|||
"use strict";
|
||||
|
||||
|
||||
|
||||
// P A C K A G E S
|
||||
|
||||
const html = require("choo/html");
|
||||
const local = require("app-root-path").require;
|
||||
const request = require("request-promise-native");
|
||||
|
||||
// V A R I A B L E S
|
||||
|
||||
const fetchMetadata = local("app/helpers/fetch-metadata");
|
||||
const { generateGitHubFeed } = local("app/helpers/github");
|
||||
const logSlackError = local("app/helpers/slack");
|
||||
|
||||
|
||||
|
||||
// P R O G R A M
|
||||
|
||||
module.exports = exports = (socket, action) => {
|
||||
if (typeof socket !== "object" && typeof action !== "object") return;
|
||||
|
||||
switch(true) {
|
||||
case (action.message === "fetch metadata"):
|
||||
fetchMetadata(action, socket);
|
||||
break;
|
||||
|
||||
case (action.message === "landed on homepage"):
|
||||
generateGitHubFeed(result => {
|
||||
socket.send(JSON.stringify({
|
||||
"html": result,
|
||||
"message": "updated html",
|
||||
"selector": "#github-feed"
|
||||
}));
|
||||
});
|
||||
break;
|
||||
|
||||
case (action.message === "landed on tour"):
|
||||
generateContent(1, result => {
|
||||
socket.send(JSON.stringify({
|
||||
"html": result,
|
||||
"message": "updated html",
|
||||
"selector": "#tour-loader"
|
||||
}));
|
||||
});
|
||||
break;
|
||||
|
||||
case (action.message === "request for tour, example 1"):
|
||||
generateContent(1, result => {
|
||||
socket.send(JSON.stringify({
|
||||
"html": result,
|
||||
"message": "updated html",
|
||||
"selector": "#tour-loader"
|
||||
}));
|
||||
});
|
||||
break;
|
||||
|
||||
case (action.message === "request for tour, example 2"):
|
||||
generateMemeCreator(socket);
|
||||
break;
|
||||
|
||||
case (action.message === "request for tour, example 3"):
|
||||
generateContent(3, result => {
|
||||
socket.send(JSON.stringify({
|
||||
"html": result,
|
||||
"message": "updated html",
|
||||
"selector": "#tour-loader"
|
||||
}));
|
||||
});
|
||||
break;
|
||||
|
||||
case (action.message === "subscribe"):
|
||||
newsletterSubscribe(action, socket);
|
||||
break;
|
||||
|
||||
default:
|
||||
process.stdout.write(action);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// H E L P E R S
|
||||
|
||||
function generateMemeCreator(socket) {
|
||||
const images = [
|
||||
{
|
||||
alt: "Carl Sagan",
|
||||
src: "/assets/media/images/carlsagan2.jpg"
|
||||
},
|
||||
{
|
||||
alt: "Doge",
|
||||
src: "/assets/media/images/doge-meme.jpg"
|
||||
},
|
||||
{
|
||||
alt: "LBRY Logo With Green Background",
|
||||
src: "/assets/media/images/lbry-green.png"
|
||||
}
|
||||
];
|
||||
|
||||
const memePlaceholderData = {
|
||||
bottomLine: {
|
||||
placeholder: "Top line",
|
||||
value: "that I made"
|
||||
},
|
||||
description: {
|
||||
placeholder: "Description",
|
||||
value: "Check out this image I published to LBRY via lbry.tech"
|
||||
},
|
||||
topLine: {
|
||||
placeholder: "Top line",
|
||||
value: "This is an example meme"
|
||||
},
|
||||
title: {
|
||||
placeholder: "Title",
|
||||
value: "Dank Meme Supreme da Cheese"
|
||||
}
|
||||
};
|
||||
|
||||
const renderedImages = [];
|
||||
|
||||
for (const image of images) {
|
||||
renderedImages.push(`<img alt="${image.alt}" class="tour__content__meme__canvas__thumbnail" src="${image.src}"/>`);
|
||||
}
|
||||
|
||||
const memeCreator = html`
|
||||
<div class="tour__content__meme__canvas">
|
||||
<img alt="Base image for LBRY meme creator" id="base-image" style="height: 0; position: absolute; visibility: hidden;"/>
|
||||
<canvas id="meme-canvas" height="300" width="400">Unfortunately, it looks like canvas is <strong>not supported</strong> in your browser</canvas>
|
||||
|
||||
${renderedImages}
|
||||
</div>
|
||||
|
||||
<form class="tour__content__meme__editor">
|
||||
<h2>Image Text</h2>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-top-line">Top line</label>
|
||||
<input id="meme-top-line" name="meme-top-line" placeholder="${memePlaceholderData.topLine.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.topLine.value}" required/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-bottom-line">Bottom line</label>
|
||||
<input id="meme-bottom-line" name="meme-bottom-line" placeholder="${memePlaceholderData.bottomLine.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.bottomLine.value}" required/>
|
||||
</fieldset>
|
||||
|
||||
<h2 class="__metadata">Metadata</h2>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-title">Title</label>
|
||||
<input id="meme-title" name="meme-title" placeholder="${memePlaceholderData.title.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.title.value}" required/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-description">Description</label>
|
||||
<textarea id="meme-description" name="meme-description" placeholder="${memePlaceholderData.description.placeholder}" spellcheck="false" type="text" required>${memePlaceholderData.description.value}</textarea>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-language">Language</label>
|
||||
<select id="meme-language" name="meme-language">
|
||||
<option value="ar">Arabic</option>
|
||||
<option value="zh">Chinese (Mandarin)</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="jp">Japanese</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="">Not specified</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-license">License</label>
|
||||
<select id="meme-license" name="meme-license" required>
|
||||
<option value="Public Domain">Public Domain</option>
|
||||
<option value="Creative Commons Attribution 4.0 International">Creative Commons Attribution 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-ShareAlike 4.0 International">Creative Commons Attribution-ShareAlike 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-NoDerivatives 4.0 International">Creative Commons Attribution-NoDerivatives 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-NonCommercial 4.0 International">Creative Commons Attribution-NonCommercial 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
|
||||
<option value="None">None</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label><input id="meme-nsfw-flag" name="nsfw" type="checkbox"/>NSFW</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<button data-action="upload image" class="__button-black" type="button">Submit</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
detectLanguageAndUpdate();
|
||||
initCanvas();
|
||||
|
||||
document.getElementById("tour-example-description").textContent = document.querySelector("[data-action='tour, example 2']").dataset.description;
|
||||
|
||||
setTimeout(() => {
|
||||
document.querySelector(".tour__content__meme__canvas__thumbnail").click();
|
||||
}, 100);
|
||||
</script>
|
||||
`;
|
||||
|
||||
return socket.send(JSON.stringify({
|
||||
"html": memeCreator,
|
||||
"message": "updated html",
|
||||
"selector": "#tour-loader"
|
||||
}));
|
||||
}
|
||||
|
||||
function generateContent(exampleNumber, displayTrendingContent) {
|
||||
if (exampleNumber === 1) {
|
||||
return getTrendingContent().then(response => {
|
||||
if (!response || !response.success || response.success !== true || !response.data) return "";
|
||||
|
||||
const rawContentCollection = [];
|
||||
const renderedContentCollection = [];
|
||||
const trendingContentData = response.data;
|
||||
|
||||
for (const data of trendingContentData) {
|
||||
rawContentCollection.push(fetchMetadata({ claim: data.url, method: "resolve", example: exampleNumber }));
|
||||
}
|
||||
|
||||
Promise.all(rawContentCollection).then(collection => {
|
||||
for (const part of collection) {
|
||||
try {
|
||||
renderedContentCollection.push(`
|
||||
<figure class="tour__content__trend">
|
||||
<img alt="${part.name}" data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}" src="${part.value.stream.metadata.thumbnail}"/>
|
||||
|
||||
<figcaption data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}">
|
||||
${part.value.stream.metadata.title}
|
||||
<span>${part.channel_name}</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
`);
|
||||
} catch (err) {
|
||||
return; // TODO: Return nice error message
|
||||
}
|
||||
}
|
||||
|
||||
renderedContentCollection.push(`
|
||||
<script>
|
||||
document.getElementById("tour-example-description").textContent = document.querySelector("[data-action='tour, example 1']").dataset.description
|
||||
</script>
|
||||
`);
|
||||
|
||||
displayTrendingContent(renderedContentCollection.join(""));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleNumber === 3) {
|
||||
const approvedUrls = [
|
||||
"LBRY#3db81c073f82fd1bb670c65f526faea3b8546720",
|
||||
"correlation-can-imply-causation#173412f5b1b7aa63a752e8832406aafd9f1ecb4e",
|
||||
"thanos-is-the-protagonist-how-infinity#2a7f5db2678177435b1dee6c9e38e035ead450b6nyte",
|
||||
"epic-arcade-mode-duos-nickatnyte-molt#d81bac6d49b1f92e58c37a5f633a27a45b43405e",
|
||||
"political-correctness-a-force-for-good-a#b4668c0bd096317b44c40738c099b6618095e75f",
|
||||
"10-secrets-hidden-inside-famous-logos#007789cc45cbb4255cf02ba77cbf84ca8e3d7561",
|
||||
"ever-wonder-how-bitcoin-and-other#1ac47b8b3def40a25850dc726a09ce23d09e7009",
|
||||
"bankrupt-pan-am#784b3c215a6f06b663fc1aa292bcb19f29c489bb",
|
||||
"minecraft-in-real-life-iron-man#758dd6497cdfc401ae1f25984738d024d47b50af",
|
||||
"ethan-shows-kyle-warframe-skyvault#8a7401b88d5ed0376d98f16808194d4dcb05b284"
|
||||
];
|
||||
|
||||
const rawContentCollection = [];
|
||||
const renderedContentCollection = [];
|
||||
|
||||
for (const url of approvedUrls) {
|
||||
rawContentCollection.push(fetchMetadata({ claim: url, method: "resolve", example: exampleNumber }));
|
||||
}
|
||||
|
||||
Promise.all(rawContentCollection).then(collection => {
|
||||
for (const part of collection) {
|
||||
try {
|
||||
renderedContentCollection.push(`
|
||||
<figure class="tour__content__trend">
|
||||
<img alt="${part.name}" data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}" src="${part.value.stream.metadata.thumbnail}"/>
|
||||
|
||||
<figcaption data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}">
|
||||
${part.value.stream.metadata.title}
|
||||
<span>${part.channel_name}</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
`);
|
||||
} catch (err) {
|
||||
return; // TODO: Return nice error message
|
||||
}
|
||||
}
|
||||
|
||||
renderedContentCollection.push(`
|
||||
<script>
|
||||
document.getElementById("tour-example-description").textContent = document.querySelector("[data-action='tour, example 3']").dataset.description
|
||||
</script>
|
||||
`);
|
||||
|
||||
displayTrendingContent(renderedContentCollection.join(""));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getTrendingContent() {
|
||||
return new Promise((resolve, reject) => { // eslint-disable-line
|
||||
request({
|
||||
method: "GET",
|
||||
url: "https://api.lbry.io/file/list_trending"
|
||||
}, (error, response, body) => {
|
||||
if (error || !JSON.parse(body)) resolve("Issue fetching content"); // error
|
||||
body = JSON.parse(body);
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function newsletterSubscribe(data, socket) {
|
||||
const email = data.email;
|
||||
|
||||
if (!validateEmail(email)) return socket.send(JSON.stringify({
|
||||
"html": "Your email is invalid",
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
}));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: "POST",
|
||||
url: `https://api.lbry.io/list/subscribe?email=${email}&tag=developer`
|
||||
}).then(body => {
|
||||
if (!body || !JSON.parse(body)) {
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *NEWSLETTER ERROR:* ```¯\\_(ツ)_/¯ This should be an unreachable error```" + "\n" +
|
||||
`> _Cause: ${email} interacted with the form_\n`
|
||||
);
|
||||
|
||||
return resolve(socket.send(JSON.stringify({
|
||||
"html": "Something is terribly wrong",
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
})));
|
||||
}
|
||||
|
||||
body = JSON.parse(body);
|
||||
|
||||
if (!body.success) {
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *NEWSLETTER ERROR:* ```" + JSON.parse(JSON.stringify(body.error)) + "```" + "\n" +
|
||||
`> _Cause: ${email} interacted with the form_\n`
|
||||
);
|
||||
|
||||
return reject(socket.send(JSON.stringify({
|
||||
"html": body.error,
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
})));
|
||||
}
|
||||
|
||||
return resolve(socket.send(JSON.stringify({
|
||||
"html": "Thank you! Please confirm subscription in your inbox.",
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
})));
|
||||
}).catch(welp => {
|
||||
if (welp.statusCode === 409) {
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *NEWSLETTER ERROR:* ```" + JSON.parse(JSON.stringify(welp.error)) + "```" + "\n" +
|
||||
`> _Cause: ${email} interacted with the form_\n`
|
||||
);
|
||||
|
||||
return resolve(socket.send(JSON.stringify({
|
||||
"html": "You have already subscribed!",
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
})));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function validateEmail(email) {
|
||||
const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\\.,;:\s@"]{2,})$/i;
|
||||
return emailRegex.test(String(email));
|
||||
}
|
|
@ -17,8 +17,8 @@ import linkGrid from "../components/link-grid";
|
|||
const featureLinks = linkGrid([
|
||||
{
|
||||
description: "Learn how LBRY works with 3 easy examples",
|
||||
destination: "/tour",
|
||||
label: "Take the Tour",
|
||||
destination: "/playground",
|
||||
label: "Jump into Playground",
|
||||
title: "New to LBRY?"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -85,7 +85,7 @@ module.exports = exports = (state, emit) => { // eslint-disable-line
|
|||
let pageScript = "";
|
||||
if (path === "glossary") pageScript = "<script>" + fs.readFileSync("./app/components/client/glossary-scripts.js", "utf-8") + "</script>";
|
||||
if (path === "overview") pageScript = "<script>" + fs.readFileSync("./app/components/client/ecosystem-scripts.js", "utf-8") + "</script>";
|
||||
if (path === "tour") pageScript = "<script>" + fs.readFileSync("./app/components/client/tour-scripts.js", "utf-8") + "</script>";
|
||||
if (path === "playground") pageScript = "<script>" + fs.readFileSync("./app/components/client/tour-scripts.js", "utf-8") + "</script>";
|
||||
|
||||
return html`
|
||||
<article class="page" itemtype="http://schema.org/BlogPosting">
|
||||
|
|
9
documents/playground.md
Normal file
9
documents/playground.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
title: Playground
|
||||
---
|
||||
|
||||
Check out any of the interactive examples to get a feel for the LBRY protocol!
|
||||
|
||||
LBRY (pronounced "library") is an application layer protocol, similar to HTTP. However, while HTTP links can direct you to decentralized content, the LBRY protocol *itself* is decentralized.
|
||||
|
||||
<Playground/>
|
479
server.js
479
server.js
|
@ -4,10 +4,9 @@
|
|||
|
||||
// P A C K A G E S
|
||||
|
||||
const async = require("async");
|
||||
const color = require("colorette");
|
||||
const cors = require("cors");
|
||||
const dedent = require("dedent");
|
||||
const local = require("app-root-path").require;
|
||||
|
||||
const fastify = require("fastify")({
|
||||
logger: {
|
||||
|
@ -16,42 +15,10 @@ const fastify = require("fastify")({
|
|||
}
|
||||
});
|
||||
|
||||
const html = require("choo/html");
|
||||
const local = require("app-root-path").require;
|
||||
const octokit = require("@octokit/rest")();
|
||||
const redis = require("redis");
|
||||
const request = require("request-promise-native");
|
||||
|
||||
// V A R I A B L E S
|
||||
|
||||
const fetchMetadata = local("app/helpers/fetch-metadata");
|
||||
const github = local("app/helpers/github");
|
||||
const log = console.log; // eslint-disable-line
|
||||
const handleSocketMessages = local("app/sockets");
|
||||
const logSlackError = local("app/helpers/slack");
|
||||
const relativeDate = local("app/modules/relative-date");
|
||||
let client;
|
||||
|
||||
if (typeof process.env.GITHUB_OAUTH_TOKEN !== "undefined") {
|
||||
octokit.authenticate({
|
||||
type: "oauth",
|
||||
token: process.env.GITHUB_OAUTH_TOKEN
|
||||
});
|
||||
} else log(`${color.red("[missing]")} GitHub token`);
|
||||
|
||||
if (typeof process.env.REDISCLOUD_URL !== "undefined") {
|
||||
client = redis.createClient(process.env.REDISCLOUD_URL);
|
||||
|
||||
client.on("error", redisError => {
|
||||
process.env.NODE_ENV === "development" ?
|
||||
log(`\n${color.yellow("Unable to connect to Redis client.")}\nYou may be missing an .env file or your connection was reset.`) :
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *REDIS ERROR:* ```" + JSON.parse(JSON.stringify(redisError)) + "```" + "\n" +
|
||||
"> _Cause: Someone is trying to run LBRY.tech locally without environment variables OR Heroku is busted_\n"
|
||||
)
|
||||
;
|
||||
});
|
||||
} else log(`${color.red("[missing]")} Redis client URL`);
|
||||
|
||||
|
||||
|
||||
|
@ -81,74 +48,10 @@ fastify.ready(err => {
|
|||
fastify.ws.on("connection", socket => {
|
||||
socket.on("message", data => {
|
||||
data = JSON.parse(data);
|
||||
|
||||
switch(data.message) {
|
||||
case "fetch metadata":
|
||||
fetchMetadata(data, socket);
|
||||
break;
|
||||
|
||||
case "landed on homepage":
|
||||
generateGitHubFeed(result => {
|
||||
socket.send(JSON.stringify({
|
||||
"html": result,
|
||||
"message": "updated html",
|
||||
"selector": "#github-feed"
|
||||
}));
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "landed on tour":
|
||||
generateContent(1, result => {
|
||||
socket.send(JSON.stringify({
|
||||
"html": result,
|
||||
"message": "updated html",
|
||||
"selector": "#tour-loader"
|
||||
}));
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "request for tour, example 1":
|
||||
generateContent(1, result => {
|
||||
socket.send(JSON.stringify({
|
||||
"html": result,
|
||||
"message": "updated html",
|
||||
"selector": "#tour-loader"
|
||||
}));
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "request for tour, example 2":
|
||||
generateMemeCreator(socket);
|
||||
break;
|
||||
|
||||
case "request for tour, example 3":
|
||||
generateContent(3, result => {
|
||||
socket.send(JSON.stringify({
|
||||
"html": result,
|
||||
"message": "updated html",
|
||||
"selector": "#tour-loader"
|
||||
}));
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "subscribe":
|
||||
newsletterSubscribe(data, socket);
|
||||
break;
|
||||
|
||||
default:
|
||||
log(data);
|
||||
break;
|
||||
}
|
||||
return handleSocketMessages(socket, data);
|
||||
});
|
||||
|
||||
socket.on("close", () => {
|
||||
// console.log(socket);
|
||||
return socket.terminate();
|
||||
});
|
||||
socket.on("close", () => socket.terminate());
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -159,387 +62,15 @@ fastify.ready(err => {
|
|||
const start = async () => {
|
||||
try {
|
||||
await fastify.listen(process.env.PORT || 8080, process.env.IP || "0.0.0.0");
|
||||
/*
|
||||
await fastify.listen(
|
||||
process.env.NODE_ENV === "development" ?
|
||||
8080 :
|
||||
process.env.PORT
|
||||
);
|
||||
*/
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.env.NODE_ENV === "development" ?
|
||||
log(`\n— ${color.green("⚡")} ${fastify.server.address().port}\n`) :
|
||||
process.stdout.write(`\n— ${color.green("⚡")} ${fastify.server.address().port}\n`) :
|
||||
logSlackError(`Server started at port \`${fastify.server.address().port}\``)
|
||||
;
|
||||
};
|
||||
|
||||
start();
|
||||
|
||||
|
||||
|
||||
// H E L P E R S
|
||||
|
||||
function generateGitHubFeed(displayGitHubFeed) {
|
||||
if (typeof process.env.REDISCLOUD_URL !== "undefined") {
|
||||
client.zrevrange("events", 0, 9, (err, reply) => {
|
||||
if (err) return; // TODO: Render a div with nice error message
|
||||
|
||||
const events = [];
|
||||
const renderedEvents = [];
|
||||
|
||||
reply.forEach(item => events.push(JSON.parse(item)));
|
||||
|
||||
for (const event of events) {
|
||||
renderedEvents.push(`
|
||||
<div class='github-feed__event'>
|
||||
<a href="${github.generateUrl("actor", event)}" target="_blank" rel="noopener noreferrer">
|
||||
<img src="${event.actor.avatar_url}" class="github-feed__event__avatar" alt=""/>
|
||||
</a>
|
||||
|
||||
<p>
|
||||
${github.generateEvent(event)}
|
||||
<a href="${github.generateUrl("repo", event)}" title="View this repo on GitHub" target="_blank" rel="noopener noreferrer"><strong>${event.repo.name}</strong></a>
|
||||
<em class="github-feed__event__time">${relativeDate(new Date(event.created_at))}</em>
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
updateGithubFeed(); // TODO: Update `.last-updated` every minute
|
||||
|
||||
displayGitHubFeed(dedent`
|
||||
<h3>GitHub</h3>
|
||||
<h5 class="last-updated">Last updated: ${new Date().format("YYYY-MM-DD").replace(/-/g, "·")} at ${new Date().add(-4, "hours").format("UTC:H:mm:ss A").toLowerCase()} EST</h5>
|
||||
|
||||
${renderedEvents.join("")}
|
||||
`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function generateMemeCreator(socket) {
|
||||
const images = [
|
||||
{
|
||||
alt: "Carl Sagan",
|
||||
src: "/assets/media/images/carlsagan2.jpg"
|
||||
},
|
||||
{
|
||||
alt: "Doge",
|
||||
src: "/assets/media/images/doge-meme.jpg"
|
||||
},
|
||||
{
|
||||
alt: "LBRY Logo With Green Background",
|
||||
src: "/assets/media/images/lbry-green.png"
|
||||
}
|
||||
];
|
||||
|
||||
const memePlaceholderData = {
|
||||
bottomLine: {
|
||||
placeholder: "Top line",
|
||||
value: "that I made"
|
||||
},
|
||||
description: {
|
||||
placeholder: "Description",
|
||||
value: "Check out this image I published to LBRY via lbry.tech"
|
||||
},
|
||||
topLine: {
|
||||
placeholder: "Top line",
|
||||
value: "This is an example meme"
|
||||
},
|
||||
title: {
|
||||
placeholder: "Title",
|
||||
value: "Dank Meme Supreme da Cheese"
|
||||
}
|
||||
};
|
||||
|
||||
const renderedImages = [];
|
||||
|
||||
for (const image of images) {
|
||||
renderedImages.push(`<img alt="${image.alt}" class="tour__content__meme__canvas__thumbnail" src="${image.src}"/>`);
|
||||
}
|
||||
|
||||
const memeCreator = html`
|
||||
<div class="tour__content__meme__canvas">
|
||||
<img alt="Base image for LBRY meme creator" id="base-image" style="height: 0; visibility: hidden;"/>
|
||||
<canvas id="meme-canvas" height="300" width="400">Unfortunately, it looks like canvas is <strong>not supported</strong> in your browser</canvas>
|
||||
|
||||
${renderedImages}
|
||||
</div>
|
||||
|
||||
<form class="tour__content__meme__editor">
|
||||
<h2>Image Text</h2>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-top-line">Top line</label>
|
||||
<input id="meme-top-line" name="meme-top-line" placeholder="${memePlaceholderData.topLine.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.topLine.value}" required/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-bottom-line">Bottom line</label>
|
||||
<input id="meme-bottom-line" name="meme-bottom-line" placeholder="${memePlaceholderData.bottomLine.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.bottomLine.value}" required/>
|
||||
</fieldset>
|
||||
|
||||
<h2 class="__metadata">Metadata</h2>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-title">Title</label>
|
||||
<input id="meme-title" name="meme-title" placeholder="${memePlaceholderData.title.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.title.value}" required/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-description">Description</label>
|
||||
<textarea id="meme-description" name="meme-description" placeholder="${memePlaceholderData.description.placeholder}" spellcheck="false" type="text" required>${memePlaceholderData.description.value}</textarea>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-language">Language</label>
|
||||
<select id="meme-language" name="meme-language">
|
||||
<option value="ar">Arabic</option>
|
||||
<option value="zh">Chinese (Mandarin)</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="jp">Japanese</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="">Not specified</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="meme-license">License</label>
|
||||
<select id="meme-license" name="meme-license" required>
|
||||
<option value="Public Domain">Public Domain</option>
|
||||
<option value="Creative Commons Attribution 4.0 International">Creative Commons Attribution 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-ShareAlike 4.0 International">Creative Commons Attribution-ShareAlike 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-NoDerivatives 4.0 International">Creative Commons Attribution-NoDerivatives 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-NonCommercial 4.0 International">Creative Commons Attribution-NonCommercial 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
|
||||
<option value="Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
|
||||
<option value="None">None</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label><input id="meme-nsfw-flag" name="nsfw" type="checkbox"/>NSFW</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<button data-action="upload image" class="__button-black" type="button">Submit</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
detectLanguageAndUpdate();
|
||||
initCanvas();
|
||||
|
||||
setTimeout(() => {
|
||||
$(".tour__content__meme__canvas__thumbnail").click();
|
||||
}, 100);
|
||||
</script>
|
||||
`;
|
||||
|
||||
return socket.send(JSON.stringify({
|
||||
"html": memeCreator,
|
||||
"message": "updated html",
|
||||
"selector": "#tour-loader"
|
||||
}));
|
||||
}
|
||||
|
||||
function generateContent(exampleNumber, displayTrendingContent) {
|
||||
if (exampleNumber === 1) {
|
||||
return getTrendingContent().then(response => {
|
||||
if (!response || !response.success || response.success !== true || !response.data) return "";
|
||||
|
||||
const rawContentCollection = [];
|
||||
const renderedContentCollection = [];
|
||||
const trendingContentData = response.data;
|
||||
|
||||
for (const data of trendingContentData) {
|
||||
rawContentCollection.push(fetchMetadata({ claim: data.url, method: "resolve", example: exampleNumber }));
|
||||
}
|
||||
|
||||
Promise.all(rawContentCollection).then(collection => {
|
||||
for (const part of collection) {
|
||||
if (
|
||||
!part.value.stream.metadata.nsfw &&
|
||||
part.value.stream.metadata.thumbnail &&
|
||||
part.channel_name
|
||||
) {
|
||||
renderedContentCollection.push(`
|
||||
<figure class="tour__content__trend">
|
||||
<img alt="${part.name}" data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}" src="${part.value.stream.metadata.thumbnail}"/>
|
||||
|
||||
<figcaption data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}">
|
||||
${part.value.stream.metadata.title}
|
||||
<span>${part.channel_name}</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
displayTrendingContent(renderedContentCollection.join(""));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleNumber === 3) {
|
||||
const approvedUrls = [
|
||||
"LBRY#3db81c073f82fd1bb670c65f526faea3b8546720",
|
||||
"correlation-can-imply-causation#173412f5b1b7aa63a752e8832406aafd9f1ecb4e",
|
||||
"thanos-is-the-protagonist-how-infinity#2a7f5db2678177435b1dee6c9e38e035ead450b6nyte",
|
||||
"epic-arcade-mode-duos-nickatnyte-molt#d81bac6d49b1f92e58c37a5f633a27a45b43405e",
|
||||
"political-correctness-a-force-for-good-a#b4668c0bd096317b44c40738c099b6618095e75f",
|
||||
"10-secrets-hidden-inside-famous-logos#007789cc45cbb4255cf02ba77cbf84ca8e3d7561",
|
||||
"ever-wonder-how-bitcoin-and-other#1ac47b8b3def40a25850dc726a09ce23d09e7009",
|
||||
"bankrupt-pan-am#784b3c215a6f06b663fc1aa292bcb19f29c489bb",
|
||||
"minecraft-in-real-life-iron-man#758dd6497cdfc401ae1f25984738d024d47b50af",
|
||||
"ethan-shows-kyle-warframe-skyvault#8a7401b88d5ed0376d98f16808194d4dcb05b284"
|
||||
];
|
||||
|
||||
const rawContentCollection = [];
|
||||
const renderedContentCollection = [];
|
||||
|
||||
for (const url of approvedUrls) {
|
||||
rawContentCollection.push(fetchMetadata({ claim: url, method: "resolve", example: exampleNumber }));
|
||||
}
|
||||
|
||||
Promise.all(rawContentCollection).then(collection => {
|
||||
for (const part of collection) {
|
||||
if (
|
||||
part &&
|
||||
part.value &&
|
||||
part.value.stream.metadata.thumbnail &&
|
||||
part.channel_name
|
||||
) {
|
||||
renderedContentCollection.push(`
|
||||
<figure class="tour__content__trend">
|
||||
<img alt="${part.name}" data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}" src="${part.value.stream.metadata.thumbnail}"/>
|
||||
|
||||
<figcaption data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}">
|
||||
${part.value.stream.metadata.title}
|
||||
<span>${part.channel_name}</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
displayTrendingContent(renderedContentCollection.join(""));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getTrendingContent() {
|
||||
return new Promise((resolve, reject) => { // eslint-disable-line
|
||||
request({
|
||||
method: "GET",
|
||||
url: "https://api.lbry.io/file/list_trending"
|
||||
}, (error, response, body) => {
|
||||
if (error || !JSON.parse(body)) resolve("Issue fetching content"); // error
|
||||
body = JSON.parse(body);
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function newsletterSubscribe(data, socket) {
|
||||
const email = data.email;
|
||||
|
||||
if (!validateEmail(email)) return socket.send(JSON.stringify({
|
||||
"html": "Your email is invalid",
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
}));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: "POST",
|
||||
url: `https://api.lbry.io/list/subscribe?email=${email}&tag=developer`
|
||||
}).then(body => {
|
||||
if (!body || !JSON.parse(body)) {
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *NEWSLETTER ERROR:* ```¯\\_(ツ)_/¯ This should be an unreachable error```" + "\n" +
|
||||
`> _Cause: ${email} interacted with the form_\n`
|
||||
);
|
||||
|
||||
return resolve(socket.send(JSON.stringify({
|
||||
"html": "Something is terribly wrong",
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
})));
|
||||
}
|
||||
|
||||
body = JSON.parse(body);
|
||||
|
||||
if (!body.success) {
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *NEWSLETTER ERROR:* ```" + JSON.parse(JSON.stringify(body.error)) + "```" + "\n" +
|
||||
`> _Cause: ${email} interacted with the form_\n`
|
||||
);
|
||||
|
||||
return reject(socket.send(JSON.stringify({
|
||||
"html": body.error,
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
})));
|
||||
}
|
||||
|
||||
return resolve(socket.send(JSON.stringify({
|
||||
"html": "Thank you! Please confirm subscription in your inbox.",
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
})));
|
||||
}).catch(welp => {
|
||||
if (welp.statusCode === 409) {
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *NEWSLETTER ERROR:* ```" + JSON.parse(JSON.stringify(welp.error)) + "```" + "\n" +
|
||||
`> _Cause: ${email} interacted with the form_\n`
|
||||
);
|
||||
|
||||
return resolve(socket.send(JSON.stringify({
|
||||
"html": "You have already subscribed!",
|
||||
"message": "updated html",
|
||||
"selector": "#emailMessage"
|
||||
})));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateGithubFeed() {
|
||||
octokit.activity.getEventsForOrg({
|
||||
org: "lbryio",
|
||||
per_page: 20,
|
||||
page: 1
|
||||
}).then(({ data }) => {
|
||||
async.eachSeries(data, (item, callback) => {
|
||||
const eventString = JSON.stringify(item);
|
||||
|
||||
client.zrank("events", eventString, (err, reply) => {
|
||||
if (reply === null) client.zadd("events", item.id, eventString, callback);
|
||||
else callback();
|
||||
});
|
||||
}, () => client.zremrangebyrank("events", 0, -51)); // Keep the latest 50 events
|
||||
}).catch(err => {
|
||||
logSlackError(
|
||||
"\n" +
|
||||
"> *GITHUB FEED ERROR:* ```" + JSON.parse(JSON.stringify(err)) + "```" + "\n" +
|
||||
"> _Cause: GitHub feed refresh_\n"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function validateEmail(email) {
|
||||
const re = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\\.,;:\s@"]{2,})$/i;
|
||||
return re.test(String(email));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue