diff --git a/README.md b/README.md index adb8b94..1d98944 100755 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ - [lbry/web-daemon](https://github.com/lbryio/web-daemon) - [Node](https://nodejs.org) (version >= 10) +## Notes +- **This repo will not run locally if you do not also have the LBRY app/daemon and the [web-daemon](https://github.com/lbryio/web-daemon) running.** +- When running locally and completing the tipping example in Playground, the LBC donated to a creator comes from _your_ LBC balance. Otherwise, the example will fail. On production, the donated LBC comes from LBRY. + ## Installation `npm i` diff --git a/app/components/client/tour-scripts.js b/app/components/client/tour-scripts.js index e066cfd..7eb7fd3 100644 --- a/app/components/client/tour-scripts.js +++ b/app/components/client/tour-scripts.js @@ -13,30 +13,52 @@ 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()); + + switch(true) { + case (document.querySelector("[data-example='1']").classList.contains("active")): + if ( + key === 13 && + document.getElementById("fetch-claim-uri").value.length > 0 + ) fetchMetadata(1, document.getElementById("fetch-claim-uri").value); + break; + + case (document.querySelector("[data-example='3']").classList.contains("active")): + if ( + key === 13 && + document.getElementById("fetch-claim-uri").value.length > 0 + ) fetchMetadata(3, document.getElementById("fetch-claim-uri").value); + break; + } }); -$("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 +116,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 +223,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 +247,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 +267,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 +289,8 @@ const handleExamples = debounce(event => { // $("#tour-url").after("

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.

"); 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(); diff --git a/app/components/head.js b/app/components/head.js index 97c34d1..9695bb3 100644 --- a/app/components/head.js +++ b/app/components/head.js @@ -39,30 +39,29 @@ module.exports = exports = (state, emit) => { - - - - - + + + + + + + - + - - - + + + - - + - - `; }; diff --git a/app/components/navigation.js b/app/components/navigation.js index 00b505b..9de7ee9 100644 --- a/app/components/navigation.js +++ b/app/components/navigation.js @@ -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", diff --git a/app/components/playground.js b/app/components/playground.js new file mode 100644 index 0000000..a0c5ad1 --- /dev/null +++ b/app/components/playground.js @@ -0,0 +1,82 @@ +"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` +
+ +

+
${raw(example1())}
+
+ `; +} + + + +// H E L P E R S + +function example1() { + return html` +
+ lbry:// + +
+ + +
+ + + `; +} + +function navigation() { // TODO: Save tutorial position to localStorage + return dedent` +
  • + + Get details of media (aka, "claim" metadata) +
  • + +
  • + + Create a meme and upload it to the LBRY blockchain +
  • + +
  • + + Support creators on LBRY with a tip, on us! +
  • + `; +} diff --git a/app/components/tour.js b/app/components/tour.js deleted file mode 100644 index 5585ebd..0000000 --- a/app/components/tour.js +++ /dev/null @@ -1,62 +0,0 @@ -"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` -
    - -
    ${raw(example1())}
    -
    - `; -} - - - -// H E L P E R S - -function example1() { - return html` -
    - lbry:// - -
    - - -
    - `; -} - -function sidebar() { // TODO: Save tutorial position to localStorage - return dedent` -
  • - - Get details of media (aka, "claim" metadata)
    - In this example, you can see what runs under the hood when selecting content to view in the LBRY app. -
  • - -
  • - - Create a meme and upload it to the LBRY blockchain
    - Sometimes you want to create content, not just consume it. In this example, you can create a meme and upload it to LBRY! -
  • - -
  • - - Support creators on LBRY with a tip, on us!
    - 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. -
  • - `; -} diff --git a/app/dist/media/images/og-image.png b/app/dist/media/images/og-image.png new file mode 100644 index 0000000..e9d3247 Binary files /dev/null and b/app/dist/media/images/og-image.png differ diff --git a/app/dist/scripts/sockets.js b/app/dist/scripts/sockets.js index adcebe9..f4b0e8d 100644 --- a/app/dist/scripts/sockets.js +++ b/app/dist/scripts/sockets.js @@ -1,34 +1,85 @@ -/* 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(`
    ${data.details}
    `); +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 = ""; + + // `data.example` is added when updating HTML. + // This is when the results of an example are sent to the client. + if (data.example) { + if (!document.querySelector(`[data-example="${data.example}"`).classList.contains("completed")) { + document.getElementById("tour-example-description").classList.remove("success"); + } + + document.querySelector(`[data-example="${data.example}"`).classList.add("completed"); + document.getElementById("tour-example-description").classList.add("success"); + + document.getElementById("tour-example-description").innerHTML = + document.querySelector(`[data-example="${data.example}"`).dataset.success; + } + + // If `data.example` isn't found, reset the description area. + else { + document.getElementById("tour-example-description").classList.remove("success"); + + document.getElementById("tour-example-description").innerHTML = + document.querySelector(".tour__navigation__example.active").dataset.description; + } + + 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 = + `
    ${data.details}
    `; + + 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 +87,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); } diff --git a/app/helpers/fetch-metadata.js b/app/helpers/fetch-metadata.js index 13c714b..c520bf5 100644 --- a/app/helpers/fetch-metadata.js +++ b/app/helpers/fetch-metadata.js @@ -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"]); @@ -92,8 +92,6 @@ module.exports = exports = (data, socket) => { body.file_path = uploadResponse.filename; return publishMeme(body).then(publishResponse => { - let explorerNotice = ""; - if (publishResponse.error) { socket.send(JSON.stringify({ "details": "Meme publish failed", @@ -112,21 +110,13 @@ module.exports = exports = (data, socket) => { return; } - if ( - publishResponse.result && - publishResponse.result.txid - ) explorerNotice = ` -

    If you want proof of the tip you just gave, check it out on our blockchain explorer!

    - `; - const renderedCode = prism.highlight(stringifyObject(publishResponse, { indent: " ", singleQuotes: false }), prism.languages.json, "json"); return socket.send(JSON.stringify({ + "example": data.example, "html": raw(`

    Response

    - ${explorerNotice}
    ${renderedCode}
    - `), "message": "updated html", "selector": `#example${data.example}-result` @@ -141,6 +131,30 @@ module.exports = exports = (data, socket) => { } if (resolveMethod === "wallet_send") { + const approvedIds = [ + "3db81c073f82fd1bb670c65f526faea3b8546720", + "173412f5b1b7aa63a752e8832406aafd9f1ecb4e", + "d81bac6d49b1f92e58c37a5f633a27a45b43405e", + "b4668c0bd096317b44c40738c099b6618095e75f", + "007789cc45cbb4255cf02ba77cbf84ca8e3d7561", + "1ac47b8b3def40a25850dc726a09ce23d09e7009", + "784b3c215a6f06b663fc1aa292bcb19f29c489bb", + "758dd6497cdfc401ae1f25984738d024d47b50af", + "8a7401b88d5ed0376d98f16808194d4dcb05b284" + ]; + + if (!approvedIds.includes(claimAddress)) { + return socket.send(JSON.stringify({ + "example": data.example, + "html": raw(` +

    Response

    +
    Tipping creators not in the whitelist for this example is not allowed.
    + `), + "message": "updated html", + "selector": `#example${data.example}-result` + })); + } + apiRequestMethod = "POST"; body.amount = "0.01"; // Hardcoded tip amount @@ -148,8 +162,6 @@ module.exports = exports = (data, socket) => { } return new Promise((resolve, reject) => { // eslint-disable-line - let explorerNotice = ""; - request({ body: body, json: true, @@ -180,22 +192,23 @@ module.exports = exports = (data, socket) => { return resolve(body.error); } + /* if ( body.result && body.result.txid ) explorerNotice = `

    If you want proof of the tip you just gave on behalf of LBRY, check it out on our blockchain explorer!

    `; + */ if (socket) { const renderedCode = prism.highlight(stringifyObject(body, { indent: " ", singleQuotes: false }), prism.languages.json, "json"); return socket.send(JSON.stringify({ + "example": data.example, "html": raw(`

    Response

    - ${explorerNotice}
    ${renderedCode}
    - `), "message": "updated html", "selector": `#example${data.example}-result` diff --git a/app/helpers/github.js b/app/helpers/github.js index ab9e2dd..da4f721 100644 --- a/app/helpers/github.js +++ b/app/helpers/github.js @@ -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(` +
    + + + + +

    + ${generateEvent(event)} + ${event.repo.name} + ${relativeDate(new Date(event.created_at))} +

    +
    + `); + } + + updateGithubFeed(); // TODO: Update `.last-updated` every minute + + displayGitHubFeed(` +

    GitHub

    +
    Last updated: ${new Date().format("YYYY-MM-DD").replace(/-/g, "·")} at ${new Date().add(-4, "hours").format("UTC:H:mm:ss A").toLowerCase()} EST
    + + ${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 }; diff --git a/app/index.js b/app/index.js index 138becd..c3c0aec 100755 --- a/app/index.js +++ b/app/index.js @@ -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"); diff --git a/app/sass/bundle.scss b/app/sass/bundle.scss index cb36abf..0d14ce4 100755 --- a/app/sass/bundle.scss +++ b/app/sass/bundle.scss @@ -21,6 +21,7 @@ "partials/navigation", "partials/mission-statement", "partials/modal", + "partials/pre", "layout", diff --git a/app/sass/init/_extends.scss b/app/sass/init/_extends.scss index af6cb4d..d27d41e 100644 --- a/app/sass/init/_extends.scss +++ b/app/sass/init/_extends.scss @@ -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; diff --git a/app/sass/init/_markdown.scss b/app/sass/init/_markdown.scss index 9d8b174..9b12971 100644 --- a/app/sass/init/_markdown.scss +++ b/app/sass/init/_markdown.scss @@ -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 { diff --git a/app/sass/pages/_api.scss b/app/sass/pages/_api.scss index 1769906..e57ca6e 100644 --- a/app/sass/pages/_api.scss +++ b/app/sass/pages/_api.scss @@ -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 { diff --git a/app/sass/pages/_tour.scss b/app/sass/pages/_tour.scss index f009ce0..12246df 100644 --- a/app/sass/pages/_tour.scss +++ b/app/sass/pages/_tour.scss @@ -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,15 @@ content: "Processing request"; } } + + h3 { + font-size: 1.5rem; + margin-bottom: 1rem; + } + + pre { + font-size: 1rem; + } } .tour__content__meme { @@ -167,6 +188,7 @@ .tour__content__meme__canvas { float: left; margin-right: 2%; + position: relative; width: 48%; canvas { @@ -202,7 +224,7 @@ .tour__content__meme__editor { float: right; - width: 48%; + width: 50%; h2.__metadata { margin-top: 3rem; @@ -315,10 +337,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 +355,7 @@ .tour__content__trend { img { - width: 100%; height: 175px; + width: 100%; height: 213px; cursor: pointer; display: block; @@ -428,3 +451,32 @@ width: 3.5rem; } } + + + +/** + * Tour | Description + * + * @class .tour__description + */ + +.tour__description { + cursor: default; + font-size: 1rem; + line-height: 1.33; + padding: 1rem; + + &:not(.success) { + background-color: rgba($black, 0.05); + text-align: center; + } + + &.success { + background-color: rgba($teal, 0.05); + + strong { + display: block; + text-transform: uppercase; + } + } +} diff --git a/app/sass/partials/_github-feed.scss b/app/sass/partials/_github-feed.scss index 70fe8bc..46d1dbd 100644 --- a/app/sass/partials/_github-feed.scss +++ b/app/sass/partials/_github-feed.scss @@ -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) { diff --git a/app/sass/partials/_pre.scss b/app/sass/partials/_pre.scss new file mode 100644 index 0000000..41bb560 --- /dev/null +++ b/app/sass/partials/_pre.scss @@ -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; + } + } +} diff --git a/app/sockets.js b/app/sockets.js new file mode 100644 index 0000000..0de1585 --- /dev/null +++ b/app/sockets.js @@ -0,0 +1,400 @@ +"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({ + // "example": 1, + "html": result, + "message": "updated html", + "selector": "#tour-loader" + })); + }); + break; + + case (action.message === "request for tour, example 1"): + generateContent(1, result => { + socket.send(JSON.stringify({ + // "example": 1, + "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({ + // "example": 3, + "html": result, + "message": "updated html", + "selector": "#tour-loader" + })); + }); + break; + + case (action.message === "subscribe"): + newsletterSubscribe(action, socket); + break; + + default: + console.log(action); // eslint-disable-line + break; + } +}; + + + +// H E L P E R S + +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(` +
    + ${part.name} + +
    + ${part.value.stream.metadata.title} + ${part.channel_name} +
    +
    + `); + } catch (err) { + return; // TODO: Return nice error message + } + } + + renderedContentCollection.push(` + + `); + + displayTrendingContent(renderedContentCollection.join("")); + }); + }); + } + + if (exampleNumber === 3) { + const approvedUrls = [ + "LBRY#3db81c073f82fd1bb670c65f526faea3b8546720", + "correlation-can-imply-causation#173412f5b1b7aa63a752e8832406aafd9f1ecb4e", + "thanos-is-the-protagonist-how-infinity#2a7f5db2678177435b1dee6c9e38e035ead450b6", + "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 })); + } + + return Promise.all(rawContentCollection).then(collection => { + for (const part of collection) { + if ( + part && + part.value && + part.value.stream.metadata.thumbnail && + part.channel_name + ) { + renderedContentCollection.push(` +
    + ${part.name} +
    + ${part.value.stream.metadata.title} + ${part.channel_name} +
    +
    + `); + } + } + + renderedContentCollection.push(` + + `); + + displayTrendingContent(renderedContentCollection.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(`${image.alt}`); + } + + const memeCreator = html` +
    + + Unfortunately, it looks like canvas is not supported in your browser + + ${renderedImages} +
    + +
    +

    Image Text

    + +
    + + +
    + +
    + + +
    + +

    Metadata

    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + +
    + +
    +
    + + + `; + + return socket.send(JSON.stringify({ + // "example": 2, + "html": memeCreator, + "message": "updated html", + "selector": "#tour-loader" + })); +} + +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)); +} diff --git a/app/views/home.js b/app/views/home.js index 8cd4b97..cd60c28 100644 --- a/app/views/home.js +++ b/app/views/home.js @@ -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?" }, { diff --git a/app/views/redirect.js b/app/views/redirect.js index 0fa3857..b7ddce3 100644 --- a/app/views/redirect.js +++ b/app/views/redirect.js @@ -85,7 +85,7 @@ module.exports = exports = (state, emit) => { // eslint-disable-line let pageScript = ""; if (path === "glossary") pageScript = ""; if (path === "overview") pageScript = ""; - if (path === "tour") pageScript = ""; + if (path === "playground") pageScript = ""; return html`
    diff --git a/documents/playground.md b/documents/playground.md new file mode 100644 index 0000000..40dc49d --- /dev/null +++ b/documents/playground.md @@ -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. + + diff --git a/server.js b/server.js index b175bba..909eb0a 100755 --- a/server.js +++ b/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(` -
    - - - - -

    - ${github.generateEvent(event)} - ${event.repo.name} - ${relativeDate(new Date(event.created_at))} -

    -
    - `); - } - - updateGithubFeed(); // TODO: Update `.last-updated` every minute - - displayGitHubFeed(dedent` -

    GitHub

    -
    Last updated: ${new Date().format("YYYY-MM-DD").replace(/-/g, "·")} at ${new Date().add(-4, "hours").format("UTC:H:mm:ss A").toLowerCase()} EST
    - - ${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(`${image.alt}`); - } - - const memeCreator = html` -
    - - Unfortunately, it looks like canvas is not supported in your browser - - ${renderedImages} -
    - -
    -

    Image Text

    - -
    - - -
    - -
    - - -
    - - - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - -
    - -
    - -
    -
    - - - `; - - 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(` -
    - ${part.name} - -
    - ${part.value.stream.metadata.title} - ${part.channel_name} -
    -
    - `); - } - } - - 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(` -
    - ${part.name} - -
    - ${part.value.stream.metadata.title} - ${part.channel_name} -
    -
    - `); - } - } - - 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)); -}