From a0ae4125c5bf8e14d30b72a52f4d3247565056c3 Mon Sep 17 00:00:00 2001 From: M Sarmad Qadeer Date: Fri, 30 Jun 2023 15:12:05 +0500 Subject: [PATCH] website: add cli demo to cli docs page (#2622) --- website/src/css/demo.css | 227 +++++++++++++++++++++++++ website/src/js/demo.js | 347 +++++++++++++++++++++++++++++++++++++++ website/src/js/docs.js | 47 ++++++ 3 files changed, 621 insertions(+) create mode 100644 website/src/css/demo.css create mode 100644 website/src/js/demo.js diff --git a/website/src/css/demo.css b/website/src/css/demo.css new file mode 100644 index 000000000..4b1a2b1fc --- /dev/null +++ b/website/src/css/demo.css @@ -0,0 +1,227 @@ +#demo { + padding-top: 3.5rem; + padding-bottom: 2.5rem; + text-align: center; +} + +#demo .all-users { + position: relative; + height: 640px; +} + +#demo .user { + position: absolute; +} + +#demo h3 { + position: absolute; + top: -60px; + text-align: left; + padding: 0 0 5px 7px; + color: #ffffbb; + mix-blend-mode: difference; + font-size: 1.5rem; + margin-bottom: 2rem; +} + +#demo .user .terminal { + text-align: left; + position: relative; + background-color: rgba(0, 0, 0, 0.8); + border: 1px solid white; + border-radius: 10px; + box-sizing: border-box; + padding: 10px 10px; + color: white; + overflow-y: hidden; +} + +#demo .user .terminal:before { + content: ""; + display: block; + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 20px; + background: #666 url(/img/topbar.svg) no-repeat 8px center; + border-radius: 10px 10px 0 0; + z-index: 2; +} + +#demo .user .terminal input, +#demo .user .terminal .input, +#demo .user .terminal .display { + font-family: Menlo, "Lucida Console", Monaco, monospace; + font-size: 14px; + letter-spacing: 0.27px; + line-height: 28px; + position: absolute; + left: 10px; + right: 10px; +} + +#demo .user .terminal input, +#demo .user .terminal .input { + color: white; + display: block; + height: 30px; + bottom: 0; +} + +#demo .user .terminal input { + background-color: transparent; + display: none; + left: 25px; + width: calc(100% - 30px); +} + +#demo .user .terminal .display { + color: white; + text-align: left; + bottom: 30px; + z-index: 1; + overflow-x: hidden; + word-wrap: break-word; +} + +#demo .user .terminal .display .info { + color: white; +} + +#demo .user .terminal .display .error { + color: #ff6347; +} + +#demo .user .terminal .display .sent span.group { + color: cyan; +} + +#demo .user .terminal .display .received span.group { + color: yellow; +} + +#demo .user .terminal .display span { + font-family: Menlo, Monaco, "Lucida Console", monospace; + text-align: left; + margin: 0 0; +} + +#demo .user .terminal .display span.recipient { + color: cyan; +} + +#demo .user .terminal .display span.sender { + color: yellow; +} + +#demo .user .terminal .display span.secret { + color: rgba(0, 0, 0, 0.4); + background-color: rgba(0, 0, 0, 0.4); +} + +#demo .alice { + left: -30px; + top: 102px; +} + +#demo .bob { + left: 425px; + top: 0px; +} + +#demo .tom { + left: 425px; + top: 378px; +} + +#demo .all-users .terminal { + width: 410px; +} + +#demo .alice .terminal { + height: 340px; +} + +#demo .bob .terminal { + height: 305px; +} + +#demo .tom .terminal { + height: 280px; +} + + +/* Demo buttons */ +#demo button { + position: absolute; + width: 140px; + height: 40px; + bottom: 85px; + font-size: 16px; + border-radius: 34px; + color: white; + font-weight: 500; + letter-spacing: 0.02em; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + background-color: #0053d0; +} + +.dark #demo button { + color: black; + background-color: #70f0f9; +} + +#demo button:disabled { + filter: brightness(75%); +} + +#demo .run-demo { + left: 20px; +} + +#demo .run-faster { + left: 20px; + display: none; +} + +#demo .try-it { + left: 180px; +} + +@media (max-width: 1240px) { + #demo .all-users { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + } + + #demo .alice { + position: relative !important; + top: 0; + left: 0; + } + + #demo button, + #demo .bob, + #demo .tom { + display: none; + } +} + +@media (max-width: 570px) { + #demo .alice .terminal { + width: 400px; + } +} + +@media (max-width: 440px) { + #demo .alice .terminal { + width: 330px; + height: 420px; + } +} \ No newline at end of file diff --git a/website/src/js/demo.js b/website/src/js/demo.js new file mode 100644 index 000000000..21beec97f --- /dev/null +++ b/website/src/js/demo.js @@ -0,0 +1,347 @@ +(async function () { + let DELAY = 0; + const DISTR = 0.33; + + class User { + constructor(name) { + this.userWindow = document.querySelector(`#demo .user.${name}`); + this.terminal = this.userWindow.querySelector(`.terminal`); + this.input = this.terminal.querySelector(".input"); + this.demoInput = this.terminal.querySelector("input"); + this.resetInput(); + this.setupDemo(); + this.group = []; + this.display = this.terminal.querySelector(".display"); + this.setupMoveWindow(); + this.name = name; + } + + reset() { + this.resetInput(); + this.display.innerHTML = ""; + } + + setGroup(groupName, users) { + this.users = users; + this.group = users.filter((u) => u !== this); + this.groupName = groupName; + } + + tryDemo() { + this.reset(); + show(this.demoInput); + this.demoInput.value = ""; + } + + async send(to, message, typeTo, paste, secret) { + await this._sendMsg(`@${to.name}`, message, typeTo, paste, secret); + await to.receive(this, toSecret(secret, message)); + await delay(20); + } + + async sendGroup(message, typeTo, paste) { + await this._sendMsg(`#${this.groupName}`, message, typeTo, paste); + await this.receiveGroup(message); + await delay(10); + } + + async _sendMsg(toStr, message, typeTo, paste, secret) { + await this.type(`${toStr} `, !typeTo); + if (secret) await this.type("#"); + await this.type(message, paste); + if (secret) await this.type("#"); + await delay(10); + this.resetInput(); + this.show("sent", `${toStr} ${toSecret(secret, message)}`); + } + + async type(str, paste) { + if (paste) { + await delay(10); + this.input.insertAdjacentHTML("beforeend", str); + } else { + for (const char of str) { + await delay(isAlpha(char) ? 1 : 2); + this.input.insertAdjacentHTML("beforeend", char); + } + } + await delay(2); + } + + resetInput() { + this.input.innerHTML = "> "; + show(this.demoInput, false); + } + + async receive(from, message, edit, group) { + await delay(10); + let g = group ? `#${this.groupName} ` : ""; + this.show("received", `${g}${from.name}> ${message}`, edit); + } + + async receiveGroup(message, edit) { + await Promise.all(this.group.map((u) => u.receive(this, message, edit, true))); + } + + show(mode, str, edit) { + if (edit && this.lastMessage) { + this.lastMessage.innerHTML = highlight(str); + return; + } + this.display.insertAdjacentHTML("beforeend", `
${highlight(str)}
`); + } + + setupDemo() { + if (!this.demoInput) return; + let editMode = false; + + on("keypress", this.demoInput, async ({ key }) => { + if (key === "Enter") { + const edit = editMode; + editMode = false; + const [to, ...words] = this.demoInput.value.trim().split(" "); + const message = words.join(" "); + switch (to[0]) { + case undefined: + if (message !== "") { + this.show("error", "Message should start with @user or #group"); + } + break; + case "@": + await this.sendInput(to.slice(1), message, edit); + break; + case "#": + await this.sendInputGroup(to.slice(1), message, edit); + break; + default: + this.show("error", "Message should start with @user or #group"); + } + } else if (this.demoInput.value === "" && key !== "@" && key !== "#") { + const channel = this.currentChannel(); + if (channel) this.demoInput.value = channel + " "; + } + }); + on("keydown", this.demoInput, async (e) => { + switch (e.key) { + case "ArrowUp": + if (this.demoInput.value === "" && this.lastMessage) { + const str = (this.demoInput.value = this.lastMessage.innerText); + editMode = true; + await delay(0); + this.demoInput.selectionStart = str.length; + } + break; + case "Tab": { + e.preventDefault(); + const userIndex = this.users.indexOf(this); + const nextIndex = (userIndex + 1) % this.users.length; + this.users[nextIndex].demoInput.focus(); + } + } + }); + } + + async sendInput(name, message, edit) { + if (name === this.name) { + this.show("error", "Can't send message to yourself"); + return; + } + const recipient = this.group.find((u) => u.name === name); + if (recipient === undefined) { + const knownNames = this.group.map((u) => `@${u.name}`).join(", ") + ` or @${this.name}`; + this.show("error", `Unknown recipient @${name} (try ${knownNames})`); + return; + } + this.show("sent", `@${name} ${message}`, edit); + this.demoInput.value = ""; + await recipient.receive(this, message, edit); + } + + async sendInputGroup(name, message, edit) { + if (name !== this.groupName) { + this.show("error", `Unknown group #${name} (try #team)`); + return; + } + this.show("sent", `#${name} ${message}`, edit); + this.demoInput.value = ""; + await this.receiveGroup(message, edit); + } + + get lastMessage() { + const messages = this.display.childNodes; + return messages[messages.length - 1]; + } + + currentChannel() { + return this.lastMessage && toContact(this.lastMessage.childNodes[0].innerHTML); + } + + setupMoveWindow() { + let moving = false; + let startX, startY; + const user = this.userWindow; + const parent = user.parentNode; + + on("mousedown", this.terminal, (e) => { + if (e.clientY - this.terminal.getBoundingClientRect().top > 20) return; + moving = true; + startX = user.offsetLeft - e.clientX; + startY = user.offsetTop - e.clientY; + parent.removeChild(user); + parent.appendChild(user); + }); + on("mouseup", this.terminal, () => (moving = false)); + on("mouseleave", this.terminal, () => (moving = false)); + on("mousemove", this.terminal, (e) => { + if (!moving) return; + user.style.left = e.clientX + startX + "px"; + user.style.top = e.clientY + startY + "px"; + }); + } + } + + function toContact(str) { + return str.endsWith(">") ? "@" + str.slice(0, -4) : str; + } + + function setGroup(groupName, users) { + users.forEach((u) => u.setGroup(groupName, users)); + } + + const alice = new User("alice"); + const bob = new User("bob"); + const tom = new User("tom"); + const team = [alice, bob, tom]; + setGroup("team", team); + + async function chatDemo() { + team.forEach((u) => u.reset()); + await alice.sendGroup("please review my PR project/site#72", true); + await tom.sendGroup("anybody got application key 🔑?"); + await bob.sendGroup("looking at it now @alice 👀"); + await alice.sendGroup("thanks @bob!"); + await alice.sendGroup("will DM @tom"); + await alice.send(tom, "w3@o6CewoZx#%$SQETXbWnus", true, true, true); + await tom.send(alice, "you're the savior 🙏!"); + await alice.send(bob, "please check the tests too", true); + await bob.send(alice, "all looks good 👍"); + await alice.send(bob, "thank you!"); + DELAY = 80; + } + + const invitation = + "smp::example.com:5223#1XNE1m2E1m0lm92​WG​Ket9CL6+lO742Vy5​G6nsrkvgs8=::St9hPY+k6nfrbaXj::rsa:MII​BoTANBgkqhkiG9w0B​AQEFAAOCAY4AMIIBiQKCAQEA03XGpEqh3faDN​Gl06pPhaT=="; + + async function establishConnection() { + team.forEach((u) => u.reset()); + await alice.type("/add bob"); + await delay(10); + alice.resetInput(); + // alice.show("/add bob"); + alice.show("sent", "pass this invitation to your contact (via any channel):"); + alice.show("sent", " "); + alice.show("sent", invitation); + alice.show("sent", " "); + alice.show("sent", "and ask them to connect:"); + alice.show("sent", "/c name_for_you invitation_above"); + await delay(20); + await bob.type("/connect alice "); + await bob.type(invitation, true); + await delay(20); + bob.resetInput(); + await bob.show("received", "/connect alice " + invitation); + await delay(10); + bob.show("received", "@alice connected"); + await delay(2); + alice.show("received", "@bob connected"); + await alice.send(bob, "hello bob"); + await bob.send(alice, "hi alice"); + } + + await chatDemo(); + const RUN_DEMO = "#demo .run-demo"; + const RUN_FASTER = "#demo .run-faster"; + const TRY_IT = "#demo .try-it"; + onClick(RUN_DEMO, runChatDemo); + // onClick(RUN_DEMO, establishConnection); + onClick(RUN_FASTER, () => (DELAY /= 2)); + onClick(TRY_IT, tryChatDemo); + + async function runChatDemo() { + show(RUN_DEMO, false); + show(RUN_FASTER); + enable(TRY_IT, false); + await chatDemo(); + show(RUN_DEMO); + show(RUN_FASTER, false); + enable(TRY_IT); + } + + function tryChatDemo() { + team.forEach((u) => u.tryDemo()); + alice.demoInput.focus(); + } + + async function delay(units) { + // delay is random with `1 +/- DISTR` range + const ms = units * DELAY * (1 - DISTR + 2 * DISTR * Math.random()); + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + function highlight(str) { + return str + .replace(/(@[a-z]+)([^0-9]|$)/gi, `$1$2`) + .replace(/([a-z]+>)([^0-9]|$)/gi, `$1$2`) + .replace(/(#[a-z]+)([^0-9]|$)/gi, `$1$2`) + .replace(/#([^\s]+)#([\s]|$)/gi, `#$1#$2`); + } + + function toSecret(secret, message) { + return secret ? `#${message}#` : message; + } + + function isAlpha(c) { + c = c.toUpperCase(); + return c >= "A" && c <= "Z"; + } + + let flipper = setInterval(flipProblem, 10000); + + onClick("#problem .pagination", () => { + clearInterval(flipper); + flipper = setInterval(flipProblem, 20000); + }); + + function flipProblem() { + if (isElementInViewport(document.getElementById("problem"))) { + window.location.hash = + window.location.hash === "#problem-explained" ? "#problem-intro" : "#problem-explained"; + } + } + + function isElementInViewport(el) { + if (!el) return false; + const r = el.getBoundingClientRect(); + return r.bottom >= 0 && r.top <= window.innerHeight; + } + + function onClick(selector, handler, enable = true) { + const el = document.querySelector(selector); + if (el) on("click", el, handler, enable); + } + + function on(event, el, handler, enable = true) { + const method = enable ? "addEventListener" : "removeEventListener"; + el[method](event, handler); + } + + function show(selector, visible = true) { + const el = typeof selector === "string" ? document.querySelector(selector) : selector; + if (el) el.style.display = visible ? "block" : "none"; + } + + function enable(selector, enabled = true) { + const el = document.querySelector(selector); + el.disabled = enabled ? "" : "true"; + } +})(); diff --git a/website/src/js/docs.js b/website/src/js/docs.js index 4535fb5ea..137923482 100644 --- a/website/src/js/docs.js +++ b/website/src/js/docs.js @@ -1,4 +1,51 @@ document.addEventListener("DOMContentLoaded", function () { + if (window.location.pathname.endsWith('cli.html')) { + const cliHeader = document.querySelector('h1') + const demoSection = document.createElement('section') + demoSection.id = 'demo' + demoSection.innerHTML = ` +
+
+

@alice

+
+
+
+ +
+
+
+

@bob

+
+
+
+ +
+
+
+

@tom

+
+
+
+ +
+
+ + + +
+ ` + cliHeader.parentNode.insertBefore(demoSection, cliHeader.nextSibling) + + const demoScript = document.createElement('script') + demoScript.src = '/js/demo.js' + document.body.appendChild(demoScript) + + const demoStyles = document.createElement('link') + demoStyles.rel = 'stylesheet' + demoStyles.href = '/css/demo.css' + document.head.appendChild(demoStyles) + } + const imgs = document.querySelectorAll('p img') imgs.forEach(img => { console.log(img.height)