Skip to content

Commit 151ff68

Browse files
committed
💌 frontend: Add code sharing (#115)
Add code sharing via gzip-base64 encoded content in URL hash e.g. https://evy.dev#content=H4sIAAAAAAAAEysoyswrUVDyyFTiAgDlkfOoCwAAAA== This is just a stop gap for storing content server side and and sharing via shorter ids. Note: sharing does not (yet) show on mobile. In preparation, fix broken Run button for mobile. This merges the following commits: * frontend: Fix broken Run button for mobile * frontend: Add code sharing frontend/img/icon-copy.svg | 4 ++ frontend/index.css | 59 +++++++++++++++-- frontend/index.html | 16 +++-- frontend/index.js | 131 ++++++++++++++++++++++++++++++++++--- 4 files changed, 192 insertions(+), 18 deletions(-) Pull-Request: #115
2 parents e1b648a + bde8ae7 commit 151ff68

File tree

4 files changed

+192
-18
lines changed

4 files changed

+192
-18
lines changed

frontend/img/icon-copy.svg

Lines changed: 4 additions & 0 deletions
Loading

frontend/index.css

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,20 @@ header .logo img {
154154
height: 1.2em;
155155
top: var(--header-img-top);
156156
}
157-
header .share {
157+
#share {
158158
order: 3;
159159
flex: 1;
160160
text-align: right;
161161
color: var(--color);
162+
cursor: pointer;
163+
}
164+
#share:hover {
165+
color: var(--color-hover);
162166
}
163-
header .share > * {
167+
#share > * {
164168
display: var(--display-desktop-only);
165169
}
166-
header .share svg {
170+
#share svg {
167171
position: relative;
168172
top: 0.2em;
169173
}
@@ -512,7 +516,8 @@ main.view-output {
512516
#modal-close:hover {
513517
color: var(--hover-color);
514518
}
515-
#modal .modal-main {
519+
.modal-main {
520+
font-family: var(--ff);
516521
padding: 36px 32px;
517522
overflow-y: auto;
518523
display: flex;
@@ -572,6 +577,52 @@ main.view-output {
572577
background: var(--modal-circle-color);
573578
}
574579

580+
#modal-share {
581+
justify-content: center;
582+
padding-top: 48px;
583+
}
584+
#modal-share label {
585+
line-height: 2rem;
586+
}
587+
#modal-share input,
588+
#modal-share button {
589+
height: 2rem;
590+
}
591+
#modal-share button {
592+
padding: 4px 16px;
593+
margin-left: 16px;
594+
color: var(--btn-color);
595+
background: var(--btn-bg);
596+
font-weight: bold;
597+
border: none;
598+
border-radius: 6px;
599+
}
600+
#modal-share button {
601+
cursor: pointer;
602+
background: var(--btn-bg-hover);
603+
}
604+
#modal-share button.copy {
605+
line-height: 0;
606+
margin: 0;
607+
padding: 0;
608+
width: 2rem;
609+
height: 2rem;
610+
border-top-left-radius: 0;
611+
border-bottom-left-radius: 0;
612+
border-left: 1px solid hsl(0deg 0% 70%);
613+
}
614+
#modal-share button.copy svg {
615+
width: 1rem;
616+
height: 1rem;
617+
}
618+
#modal-share input {
619+
margin-left: 16px;
620+
padding-left: 4px;
621+
padding-right: 4px;
622+
border: none;
623+
outline: none;
624+
}
625+
575626
/* --- Easter egg ---------------------------------------------------- */
576627
.confetti {
577628
height: 7vw;

frontend/index.html

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
<img src="img/logo.svg" alt="Evy logo" class="desktop" />
2727
<img src="img/logo-sm.svg" alt="Small evy logo" class="mobile" width="20" height="20" />
2828
</a>
29-
<div class="share">
29+
<div id="share">
3030
<svg width="1.2em" height="1.2em"><use href="#icon-share" /></svg>
3131
<span>Share</span>
3232
</div>
@@ -55,7 +55,7 @@
5555
</main>
5656
<!-- Add separate mobile button because of position:fixed and output transform -->
5757
<div class="run mobile">
58-
<button id="run-mob" disabled>Run</button>
58+
<button id="run-mobile" disabled>Run</button>
5959
</div>
6060
</div>
6161

@@ -67,7 +67,7 @@
6767
<svg width="1.2em" height="1.2em"><use href="#icon-close" class="close" /></svg>
6868
</button>
6969
</header>
70-
<div class="modal-main">
70+
<div class="modal-main" id="modal-courses">
7171
<div class="item">
7272
<h2>🌱 Getting Started</h2>
7373
<ul>
@@ -76,9 +76,9 @@ <h2>🌱 Getting Started</h2>
7676
</ul>
7777
</div>
7878
</div>
79+
<div class="modal-main hidden" id="modal-share"></div>
7980
</div>
8081
</div>
81-
8282
<!-- icons in hidden svg tag for styling and performance -->
8383
<svg style="display: none" version="2.0" xmlns="http://www.w3.org/2000/svg">
8484
<symbol id="icon-share" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24">
@@ -94,6 +94,14 @@ <h2>🌱 Getting Started</h2>
9494
<path d="M3,21 L21,3" />
9595
</g>
9696
</symbol>
97+
<symbol id="icon-copy" fill="currentColor" stroke-width="1" stroke="currentColor" viewBox="0 0 24 24">
98+
<path
99+
d="M7.024 3.75c0-.966.784-1.75 1.75-1.75H20.25c.966 0 1.75.784 1.75 1.75v11.498a1.75 1.75 0 0 1-1.75 1.75H8.774a1.75 1.75 0 0 1-1.75-1.75Zm1.75-.25a.25.25 0 0 0-.25.25v11.498c0 .139.112.25.25.25H20.25a.25.25 0 0 0 .25-.25V3.75a.25.25 0 0 0-.25-.25Z"
100+
/>
101+
<path
102+
d="M1.995 10.749a1.75 1.75 0 0 1 1.75-1.751H5.25a.75.75 0 1 1 0 1.5H3.745a.25.25 0 0 0-.25.25L3.5 20.25c0 .138.111.25.25.25h9.5a.25.25 0 0 0 .25-.25v-1.51a.75.75 0 1 1 1.5 0v1.51A1.75 1.75 0 0 1 13.25 22h-9.5A1.75 1.75 0 0 1 2 20.25l-.005-9.501Z"
103+
/>
104+
</symbol>
97105
</svg>
98106
</body>
99107
</html>

frontend/index.js

Lines changed: 121 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ let animationStart
1313
let courses
1414
let actions = "fmt,ui,eval"
1515
let editor
16+
let errors = false
1617
// --- Initialise ------------------------------------------------------
1718

1819
initWasm()
@@ -27,7 +28,7 @@ function initWasm() {
2728
.then((obj) => (wasmModule = obj))
2829
.catch((err) => console.error(err))
2930
const runButton = document.querySelector("#run")
30-
const runButtonMob = document.querySelector("#run-mob")
31+
const runButtonMob = document.querySelector("#run-mobile")
3132
runButton.onclick = handleRun
3233
runButton.disabled = false
3334
runButtonMob.onclick = handleMobRun
@@ -110,6 +111,7 @@ function jsRead() {
110111
}
111112

112113
function jsError(ptr, len) {
114+
errors = true
113115
const code = editor.value
114116
const lines = code.split("\n")
115117
const errs = memToString(ptr, len).split("\n")
@@ -185,7 +187,7 @@ async function handleMobRun() {
185187
}
186188
// on output screen
187189
if (stopped) {
188-
const runButtonMob = document.querySelector("#run-mob")
190+
const runButtonMob = document.querySelector("#run-mobile")
189191
runButtonMob.innerText = "Run"
190192
slide()
191193
return
@@ -197,11 +199,12 @@ async function handleMobRun() {
197199
// code and initialises the output ui.
198200
async function start() {
199201
stopped = false
202+
errors = false
200203
wasmInst = await WebAssembly.instantiate(wasmModule, go.importObject)
201204
clearOutput()
202205

203206
const runButton = document.querySelector("#run")
204-
const runButtonMob = document.querySelector("#run-mob")
207+
const runButtonMob = document.querySelector("#run-mobile")
205208
runButton.innerText = "Stop"
206209
runButton.classList.add("running")
207210
runButtonMob.innerText = "Stop"
@@ -212,6 +215,8 @@ async function start() {
212215

213216
// format calls evy wasm/go main() but doesn't evaluate.
214217
async function format() {
218+
stopped = false
219+
errors = false
215220
wasmInst = await WebAssembly.instantiate(wasmModule, go.importObject)
216221
actions = "fmt,ui"
217222
go.run(wasmInst)
@@ -234,7 +239,7 @@ function afterStop() {
234239
wasmInst = undefined
235240

236241
const runButton = document.querySelector("#run")
237-
const runButtonMob = document.querySelector("#run-mob")
242+
const runButtonMob = document.querySelector("#run-mobile")
238243
runButton.classList.remove("running")
239244
runButton.innerText = "Run"
240245
runButtonMob.classList.remove("running")
@@ -276,6 +281,7 @@ async function initUI() {
276281
await fetchCourses()
277282
window.addEventListener("hashchange", handleHashChange)
278283
document.querySelector("#modal-close").onclick = hideModal
284+
document.querySelector("#share").onclick = share
279285
initModal()
280286
handleHashChange()
281287
initEditor()
@@ -305,9 +311,16 @@ async function handleHashChange() {
305311
hideModal()
306312
await stopAndSlide() // go to code screen for new code
307313
let opts = parseHash()
308-
if (!opts.source && !opts.unit) {
314+
if (!opts.source && !opts.unit && !opts.content) {
309315
opts = { unit: "welcome" }
310316
}
317+
if (opts.content) {
318+
console.log("start content update")
319+
const decoded = await decode(opts.content)
320+
editor.update({ value: decoded, errorLines: {} })
321+
console.log("finished content update")
322+
return
323+
}
311324
let crumbs = ["Evy"]
312325
if (opts.unit) {
313326
const unit = courses.units[opts.unit]
@@ -498,7 +511,7 @@ function registerEventHandler(ptr, len) {
498511

499512
function unfocusRunBotton() {
500513
const runButton = document.querySelector("#run")
501-
const runButtonMob = document.querySelector("#run-mob")
514+
const runButtonMob = document.querySelector("#run-mobile")
502515
document.activeElement === runButton && runButton.blur()
503516
document.activeElement === runButtonMob && runButtonMob.blur()
504517
}
@@ -575,9 +588,22 @@ function hideModal() {
575588
el.classList.add("hidden")
576589
}
577590

578-
function showModal() {
579-
const el = document.querySelector("#modal")
580-
el.classList.remove("hidden")
591+
function showCourses() {
592+
const courses = document.querySelector("#modal-courses")
593+
courses.classList.remove("hidden")
594+
const share = document.querySelector("#modal-share")
595+
share.classList.add("hidden")
596+
const modal = document.querySelector("#modal")
597+
modal.classList.remove("hidden")
598+
}
599+
600+
function showSharing() {
601+
const share = document.querySelector("#modal-share")
602+
share.classList.remove("hidden")
603+
const courses = document.querySelector("#modal-courses")
604+
courses.classList.add("hidden")
605+
const modal = document.querySelector("#modal")
606+
modal.classList.remove("hidden")
581607
}
582608

583609
function updateBreadcrumbs(crumbs) {
@@ -589,7 +615,7 @@ function updateBreadcrumbs(crumbs) {
589615
function breadcrumb(s) {
590616
const btn = document.createElement("button")
591617
btn.textContent = s
592-
btn.onclick = () => showModal()
618+
btn.onclick = () => showCourses()
593619
const li = document.createElement("li")
594620
li.appendChild(btn)
595621
return li
@@ -650,6 +676,91 @@ function showConfetti() {
650676
}, 8500)
651677
}
652678

679+
// --- Share / load snippets -------------------------------------------
680+
681+
async function share() {
682+
console.log("share")
683+
await format()
684+
const el = document.querySelector("#modal-share")
685+
686+
if (errors) {
687+
const msg = document.createElement("label")
688+
msg.textContent = "Fix errors first please."
689+
const button = document.createElement("button")
690+
button.innerText = "OK"
691+
button.onclick = hideModal
692+
el.replaceChildren(msg, button)
693+
showSharing()
694+
console.log("Fix errors first please.")
695+
return
696+
}
697+
const encoded = await encode(editor.value)
698+
const msg = document.createElement("label")
699+
msg.textContent = "Share"
700+
const input = document.createElement("input")
701+
input.type = "text"
702+
input.onclick = input.select
703+
const baseurl = window.location.origin + window.location.pathname
704+
input.value = `${baseurl}#content=${encoded}`
705+
const button = document.createElement("button")
706+
button.className = "copy"
707+
button.innerHTML = `<svg><use href="#icon-copy" /></svg>`
708+
button.onclick = () => {
709+
navigator.clipboard.writeText(input.value)
710+
hideModal()
711+
}
712+
el.replaceChildren(msg, input, button)
713+
showSharing()
714+
console.log(encoded)
715+
}
716+
717+
async function encode(input) {
718+
await polyfillCompression()
719+
const buffer = new TextEncoder().encode(input)
720+
const stream = readableStream(buffer).pipeThrough(new CompressionStream("gzip"))
721+
const compressedBuffer = await bufferFromStream(stream)
722+
const encoded = btoa(String.fromCharCode(...compressedBuffer))
723+
return encoded
724+
}
725+
726+
async function decode(encoded) {
727+
await polyfillCompression()
728+
const bytes = atob(encoded).split("")
729+
const buffer = new Uint8Array(bytes.map((b) => b.charCodeAt(0)))
730+
const stream = readableStream(buffer).pipeThrough(new DecompressionStream("gzip"))
731+
const decompressedBuffer = await bufferFromStream(stream)
732+
const decoded = new TextDecoder().decode(decompressedBuffer)
733+
return decoded
734+
}
735+
736+
async function polyfillCompression() {
737+
if (!window.CompressionStream) {
738+
await import("https://unpkg.com/compression-streams-polyfill")
739+
}
740+
}
741+
742+
function readableStream(buffer) {
743+
return new ReadableStream({
744+
start(controller) {
745+
controller.enqueue(buffer)
746+
controller.close()
747+
},
748+
})
749+
}
750+
751+
async function bufferFromStream(stream) {
752+
const reader = stream.getReader()
753+
let buffer = new Uint8Array()
754+
while (true) {
755+
const { done, value } = await reader.read()
756+
if (done) {
757+
break
758+
}
759+
buffer = new Uint8Array([...buffer, ...value])
760+
}
761+
return buffer
762+
}
763+
653764
// --- Utilities -------------------------------------------------------
654765

655766
function getElements(q) {

0 commit comments

Comments
 (0)