Skip to content

Commit

Permalink
refactor: Clean up tutorial & REPL, fix ?code query param
Browse files Browse the repository at this point in the history
  • Loading branch information
rschristian committed Jun 30, 2024
1 parent e07646c commit 77e742a
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 107 deletions.
44 changes: 15 additions & 29 deletions src/components/controllers/repl-page.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRoute } from 'preact-iso';
import { useLocation, useRoute } from 'preact-iso';
import { Repl } from './repl';
import { useExample } from './repl/examples';
import { fetchExample } from './repl/examples';
import { useContent, useResource } from '../../lib/use-resource';
import { useTitle, useDescription } from './utils';
import { useLanguage } from '../../lib/i18n';
Expand All @@ -15,7 +15,7 @@ export default function ReplPage() {
useTitle(meta.title);
useDescription(meta.description);

const [code, slug] = initialCode(query);
const code = useResource(() => getInitialCode(query), [query]);

return (
<div class={style.repl}>
Expand All @@ -24,11 +24,8 @@ export default function ReplPage() {
height: 100% !important;
overflow: hidden !important;
}
footer {
display: none !important;
}
`}</style>
<Repl code={code} slug={slug} />
<Repl code={code} />
</div>
);
}
Expand All @@ -38,42 +35,31 @@ export default function ReplPage() {
*
* ?code -> ?example -> localStorage -> simple counter example
*/
function initialCode(query) {
let code, slug;
async function getInitialCode(query) {
const { route } = useLocation();
let code;
if (query.code) {
try {
code = useResource(() => querySafetyCheck(atob(query.code)), [query.code]);
} catch (e) {}
code = querySafetyCheck(atob(query.code));
} else if (query.example) {
code = useExample([query.example]);
if (code) {
slug = query.example;
history.replaceState(
null,
null,
`/repl?example=${encodeURIComponent(slug)}`
);
code = await fetchExample(query.example);
if (!code) {
route('/repl', true);
}
else history.replaceState(null, null, '/repl');
}

if (!code) {
if (typeof window !== 'undefined' && localStorage.getItem('preact-www-repl-code')) {
code = localStorage.getItem('preact-www-repl-code');
} else {
slug = 'counter';
const slug = 'counter';
if (typeof window !== 'undefined') {
history.replaceState(
null,
null,
`/repl?example=${encodeURIComponent(slug)}`
);
route(`/repl?example=${encodeURIComponent(slug)}`, true);
}
code = useExample([slug]);
code = await fetchExample(slug);
}
}

return [code, slug];
return code;
}

async function querySafetyCheck(code) {
Expand Down
13 changes: 3 additions & 10 deletions src/components/controllers/repl/examples.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useResource } from '../../../lib/use-resource';

import simpleCounterExample from './examples/simple-counter.txt?url';
import counterWithHtmExample from './examples/counter-with-htm.txt?url';
import todoExample from './examples/todo-list.txt?url';
Expand Down Expand Up @@ -69,15 +67,10 @@ export function getExample(slug, list = EXAMPLES) {
}

/**
* @param {[ slug: string ]} args
* @returns {string | undefined}
* @param {string} slug
*/
export function useExample([slug]) {
export async function fetchExample(slug) {
const example = getExample(slug);
if (!example) return;
return useResource(() => loadExample(example.url), ['example', slug]);
}

export async function loadExample(exampleUrl) {
return await fetch(exampleUrl).then(r => r.text());
return await fetch(example.url).then(r => r.text());
}
57 changes: 26 additions & 31 deletions src/components/controllers/repl/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'preact/hooks';
import { useLocation, useRoute } from 'preact-iso';
import { Splitter } from '../../splitter';
import { EXAMPLES, getExample, loadExample } from './examples';
import { EXAMPLES, fetchExample } from './examples';
import { ErrorOverlay } from './error-overlay';
import { useStoredValue } from '../../../lib/localstorage';
import { useResource } from '../../../lib/use-resource';
Expand All @@ -13,55 +14,49 @@ import REPL_CSS from './examples.css?raw';
* @param {string} props.code
* @param {string} [props.slug]
*/
export function Repl({ code, slug }) {
export function Repl({ code }) {
const { route } = useLocation();
const { query } = useRoute();
const [editorCode, setEditorCode] = useStoredValue('preact-www-repl-code', code);
const [exampleSlug, setExampleSlug] = useState(slug || '');
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);

// TODO: CodeMirror v5 cannot load in Node, and loading only the runner
// causes some bad jumping/pop-in. For the moment, this is the best option
if (typeof window === 'undefined') return null;

/**
* @type {{ Runner: import('../repl/runner').default, CodeEditor: import('../../code-editor').default }}
*/
const { Runner, CodeEditor } = useResource(() => Promise.all([
import('../../code-editor'),
import('./runner')
]).then(([CodeEditor, Runner]) => ({ CodeEditor: CodeEditor.default, Runner: Runner.default })), ['repl']);

const applyExample = (e) => {
const slug = e.target.value;
loadExample(getExample(slug).url)
fetchExample(slug)
.then(code => {
setEditorCode(code);
setExampleSlug(slug);
history.replaceState(
null,
null,
`/repl?example=${encodeURIComponent(slug)}`
);
route(`/repl?example=${encodeURIComponent(slug)}`, true);
});
};

useEffect(() => {
const example = getExample(exampleSlug);
(async function () {
if (example) {
const code = await loadExample(example.url);
if (location.search && code !== editorCode) {
setExampleSlug('');
history.replaceState(null, null, '/repl');
}
}
})();
}, [editorCode]);
const onEditorInput = (code) => {
setEditorCode(code);

// Clears the (now outdated) example & code query params
// when a user begins to modify the code
if (query.example || query.code) {
route('/repl', true);
}
};

const share = () => {
if (!exampleSlug) {
history.replaceState(
null,
null,
`/repl?code=${encodeURIComponent(btoa(editorCode))}`
);
// No reason to share semi-sketchy btoa'd code if there's
// a perfectly good example we can use instead
if (!query.example) {
route(`/repl?code=${encodeURIComponent(btoa(editorCode))}`, true);
}

try {
Expand Down Expand Up @@ -90,13 +85,13 @@ export function Repl({ code, slug }) {
<header class={style.toolbar}>
<label>
Examples:{' '}
<select value={exampleSlug} onChange={applyExample}>
<select value={query.example || ''} onChange={applyExample}>
<option value="" disabled>
Select Example...
</option>
{EXAMPLES.map(function item(ex) {
const selected =
ex.slug !== undefined && ex.slug === exampleSlug;
ex.slug !== undefined && ex.slug === query.example;
return ex.group ? (
<optgroup label={ex.group}>{ex.items.map(item)}</optgroup>
) : (
Expand Down Expand Up @@ -137,7 +132,7 @@ export function Repl({ code, slug }) {
class={style.code}
value={editorCode}
error={error}
onInput={setEditorCode}
onInput={onEditorInput}
/>
</Splitter>
</div>
Expand Down
73 changes: 36 additions & 37 deletions src/components/controllers/tutorial/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export function Tutorial({ html, meta }) {
// causes some bad jumping/pop-in. For the moment, this is the best option
if (typeof window === 'undefined') return null;

/**
* @type {{ Runner: import('../repl/runner').default, CodeEditor: import('../../code-editor').default }}
*/
const { Runner, CodeEditor } = useResource(() => Promise.all([
import('../../code-editor'),
import('../repl/runner')
Expand All @@ -73,7 +76,7 @@ export function Tutorial({ html, meta }) {
solutionCtx.setSolved(false);
content.current.scrollTo(0, 0);
}
}, [html]);
}, [meta.tutorial?.initial]);


const useResult = fn => {
Expand Down Expand Up @@ -140,42 +143,38 @@ export function Tutorial({ html, meta }) {
orientation="horizontal"
force={!showCode ? '100%' : undefined}
other={
// TODO: CodeMirror v5 cannot load in Node, and loading only the runner
// causes some bad jumping/pop-in. For the moment, this is the best option
typeof window === 'undefined'
? null
: <Splitter
orientation="vertical"
other={
<>
<div class={style.output}>
{error && (
<ErrorOverlay
name={error.name}
message={error.message}
stack={parseStackTrace(error)}
/>
)}
<Runner
ref={runner}
onSuccess={onSuccess}
onRealm={onRealm}
onError={onError}
code={editorCode}
/>
</div>
{hasCode && (
<button
class={style.toggleCode}
title="Toggle Code"
onClick={toggleCode}
>
<span>Toggle Code</span>
</button>
)}
</>
}
>
<Splitter
orientation="vertical"
other={
<>
<div class={style.output}>
{error && (
<ErrorOverlay
name={error.name}
message={error.message}
stack={parseStackTrace(error)}
/>
)}
<Runner
ref={runner}
onSuccess={onSuccess}
onRealm={onRealm}
onError={onError}
code={editorCode}
/>
</div>
{hasCode && (
<button
class={style.toggleCode}
title="Toggle Code"
onClick={toggleCode}
>
<span>Toggle Code</span>
</button>
)}
</>
}
>
<div class={style.codeWindow}>
<CodeEditor
class={style.code}
Expand Down

0 comments on commit 77e742a

Please sign in to comment.