diff options
| author | Jan Tuomi <jans.tuomi@gmail.com> | 2022-02-05 16:48:16 +0200 |
|---|---|---|
| committer | Jan Tuomi <jans.tuomi@gmail.com> | 2022-02-05 16:48:16 +0200 |
| commit | a96d162f649704df5d9b567158366015cbe3172c (patch) | |
| tree | 656f946eeb6748fe01447004399e08a45ff0b5f5 | |
| parent | 7d78bf4d2111af87636069ee2d35d6323ffc96be (diff) | |
Get basic stuff working
| -rw-r--r-- | .eslintrc.js | 1 | ||||
| -rw-r--r-- | package-lock.json | 78 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | src/App.module.css | 12 | ||||
| -rw-r--r-- | src/App.tsx | 38 | ||||
| -rw-r--r-- | src/Editor.module.css | 7 | ||||
| -rw-r--r-- | src/Editor.tsx | 68 | ||||
| -rw-r--r-- | src/Tutorial.tsx | 37 | ||||
| -rw-r--r-- | src/api.ts | 43 | ||||
| -rw-r--r-- | src/index.css | 20 |
10 files changed, 273 insertions, 33 deletions
diff --git a/.eslintrc.js b/.eslintrc.js index d7ef9a0..86970be 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,6 +26,7 @@ module.exports = { "indent": [ "error", 2, + { "SwitchCase": 1 }, ], "linebreak-style": [ "error", diff --git a/package-lock.json b/package-lock.json index f060c92..1b4d260 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "slam-frontend", "version": "0.0.0", "dependencies": { + "ace-builds": "1.4.14", "react": "^17.0.2", + "react-ace": "9.5.0", "react-dom": "^17.0.2", "swr": "1.2.1" }, @@ -861,6 +863,11 @@ "node": ">=12.0.0" } }, + "node_modules/ace-builds": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.14.tgz", + "integrity": "sha512-NBOQlm9+7RBqRqZwimpgquaLeTJFayqb9UEPtTkpC3TkkwDnlsT/TwsCC0svjt9kEZ6G9mH5AEOHSz6Q/HrzQQ==" + }, "node_modules/acorn": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", @@ -1207,6 +1214,11 @@ "node": ">= 0.4" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2521,6 +2533,16 @@ "node": ">= 0.8.0" } }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2856,7 +2878,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -2904,6 +2925,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-ace": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-9.5.0.tgz", + "integrity": "sha512-4l5FgwGh6K7A0yWVMQlPIXDItM4Q9zzXRqOae8KkCl6MkOob7sC1CzHxZdOGvV+QioKWbX2p5HcdOVUv6cAdSg==", + "dependencies": { + "ace-builds": "^1.4.13", + "diff-match-patch": "^1.0.5", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0", + "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -2920,8 +2957,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-refresh": { "version": "0.11.0", @@ -4068,6 +4104,11 @@ "resolve": "^1.20.0" } }, + "ace-builds": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.14.tgz", + "integrity": "sha512-NBOQlm9+7RBqRqZwimpgquaLeTJFayqb9UEPtTkpC3TkkwDnlsT/TwsCC0svjt9kEZ6G9mH5AEOHSz6Q/HrzQQ==" + }, "acorn": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", @@ -4324,6 +4365,11 @@ "object-keys": "^1.0.12" } }, + "diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -5246,6 +5292,16 @@ "type-check": "~0.4.0" } }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5493,7 +5549,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -5521,6 +5576,18 @@ "object-assign": "^4.1.1" } }, + "react-ace": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-9.5.0.tgz", + "integrity": "sha512-4l5FgwGh6K7A0yWVMQlPIXDItM4Q9zzXRqOae8KkCl6MkOob7sC1CzHxZdOGvV+QioKWbX2p5HcdOVUv6cAdSg==", + "requires": { + "ace-builds": "^1.4.13", + "diff-match-patch": "^1.0.5", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "prop-types": "^15.7.2" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -5534,8 +5601,7 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-refresh": { "version": "0.11.0", diff --git a/package.json b/package.json index 5ea82b4..c484f3d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "preview": "vite preview" }, "dependencies": { + "ace-builds": "1.4.14", "react": "^17.0.2", + "react-ace": "9.5.0", "react-dom": "^17.0.2", "swr": "1.2.1" }, diff --git a/src/App.module.css b/src/App.module.css index 0bf5f42..3b75e8c 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -32,4 +32,14 @@ .error { color: var(--red) -}
\ No newline at end of file +} + +.examples { + margin-bottom: 20px; +} + +.editor { + margin-bottom: 40px; +} + +.tutorial {} diff --git a/src/App.tsx b/src/App.tsx index 5645dcc..905149e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,26 @@ -import { useSnippets } from "./api" +import { useExamples } from "./api" import styles from "./App.module.css" +import Editor from "./Editor" +import Tutorial from "./Tutorial" import icon from "./favicon.png" const App = () => { - const snippets = useSnippets() + const examples = useExamples() - const renderSnippets = () => { - if (snippets.type === "failed") { - return <div className={styles.error}> - Unexpected error loading snippets. See console for details. - </div> - } - - if (snippets.type === "loading") { - return <div>Loading snippets...</div> + const renderExamples = () => { + switch (examples.type) { + case "not_yet_requested": + case "loading": + return <div>Loading examples...</div> + case "failed": + return <div className={styles.error}> + An unexpected error occurred while loading examples. See console for details. + </div> + case "success": + return examples.data.map((example) => + <div key={example.id}>{example.title}</div>, + ) } - - return snippets.data.map((snippet) => - <div key={snippet.id}>{snippet.title}</div>, - ) } return ( @@ -32,9 +34,11 @@ const App = () => { <small>A Mere Stack Language</small> </div> </header> - <div> - {renderSnippets()} + <div className={styles.examples}> + {renderExamples()} </div> + <div className={styles.editor}><Editor /></div> + <div className={styles.tutorial}><Tutorial /></div> </div> ) } diff --git a/src/Editor.module.css b/src/Editor.module.css new file mode 100644 index 0000000..2116903 --- /dev/null +++ b/src/Editor.module.css @@ -0,0 +1,7 @@ +.runButton { + margin-top: 10px; +} + +.error { + color: var(--red) +} diff --git a/src/Editor.tsx b/src/Editor.tsx new file mode 100644 index 0000000..d179b83 --- /dev/null +++ b/src/Editor.tsx @@ -0,0 +1,68 @@ +import AceEditor from "react-ace" +import "ace-builds/src-noconflict/mode-java" +import "ace-builds/src-noconflict/theme-github" +import { useState } from "react" +import styles from "./Editor.module.css" +import { APIResult, submitSource } from "./api" + +const Editor = () => { + const [sourceText, setSourceText] = useState("") + const [result, setResult] = useState<APIResult<string, Error>>({ type: "not_yet_requested" }) + const onChange = setSourceText + + const executeCode = async (text: string) => { + const result = await submitSource(text) + setResult(result) + } + + const renderResults = () => { + switch (result.type) { + case "not_yet_requested": + case "loading": + return null + case "failed": + return ( + <div className={styles.error}> + Failed to run code on the server. See console for details. + </div> + ) + case "success": + return ( + <AceEditor + mode="" + theme="github" + onChange={() => undefined} + value={result.data} + name="results" + editorProps={{ $blockScrolling: true }} + width="100%" + fontSize={16} + readOnly + /> + ) + } + } + + return ( + <> + <AceEditor + mode="" + theme="github" + onChange={onChange} + value={sourceText} + name="editor" + editorProps={{ $blockScrolling: true }} + width="100%" + fontSize={16} + /> + <button className={styles.runButton} onClick={() => executeCode(sourceText)}> + ▶️ Run code on server + </button> + <div className={styles.results}> + {renderResults()} + </div> + </> + ) +} + +export default Editor diff --git a/src/Tutorial.tsx b/src/Tutorial.tsx new file mode 100644 index 0000000..917142b --- /dev/null +++ b/src/Tutorial.tsx @@ -0,0 +1,37 @@ +import { useState } from "react" + +const Tutorial = () => { + const [collapsed, setCollapsed] = useState(true) + + if (collapsed) { + return <button onClick={() => setCollapsed(false)}>SLAM tutorial ▼</button> + } + + return ( + <> + <button onClick={() => setCollapsed(true)}>SLAM tutorial ▲</button> + + <h3> + <strong>SLAM</strong> is a procedural, stack-based programming language + inspired by Forth and Lisp. + </h3> + + <p> + In the core of every SLAM program, there is an implied, global stack of + strongly and dynamically typed values. A SLAM program consists of a sequence + of <i>words</i> that when evaluated, can consume and produce values from/to + the stack. + </p> + + <p> + <i>Phrases</i>, roughly equal to Lisp's quoted S-expressions, are a sequence + of words that are not immediately evaluated when encountered, but instead put on the stack for later + manipulation. Phrases can be used to implement strings or homogenous list structures. Phrases + are also the core of conditional expressions, where branches of code are only evaluated + when a condition is met. + </p> + </> + ) +} + +export default Tutorial
\ No newline at end of file @@ -10,27 +10,31 @@ const fetcher = (path: string) => fetch(`${API_URL}${path}`) throw err }) -interface Snippet { +interface Example { id: string title: string content: string } -interface APISuccess<D> { +export interface APISuccess<D> { readonly type: "success" data: D } -interface APILoading { +export interface APILoading { readonly type: "loading" } -interface APIFailed<E> { +export interface APIFailed<E> { readonly type: "failed" error: E } -type APIResult<D, E> = APISuccess<D> | APILoading | APIFailed<E> +export interface APINotYetRequested { + readonly type: "not_yet_requested" +} + +export type APIResult<D, E> = APISuccess<D> | APILoading | APIFailed<E> | APINotYetRequested const mapSWRResult = <D, E>({ data, error }: SWRResponse<D, E>): APIResult<D, E> => { if (error !== undefined) { @@ -45,6 +49,31 @@ const mapSWRResult = <D, E>({ data, error }: SWRResponse<D, E>): APIResult<D, E> return { type: "success", data } } -export const useSnippets = () => mapSWRResult<Snippet[], Error>( - useSWR("/snippets", fetcher), +const postRequest = async <D, E>(path: string, body: any, opts?: any): Promise<APIResult<D, E>> => { + const url = `${API_URL}${path}` + const body_ = typeof body === "string" ? body : JSON.stringify(body) + try { + const resp = await fetch(url, { method: "POST", body: body_, headers: { + "Content-Type": "application/json", + }, ...opts }) + + if (resp.status >= 400) { + throw new Error(`${url} responded with ${resp.status}: ${resp.text}`) + } + const data = await resp.json() + return { type: "success", data } + } catch (err: any) { + console.error(`An error occurred when POSTing to API path ${path}`) + console.error(err) + return { type: "failed", error: err } + } +} + +export const useExamples = () => mapSWRResult<Example[], Error>( + useSWR("/examples", fetcher), +) + +export const submitSource = async (text: string) => postRequest<string, Error>( + "/submit", + text, ) diff --git a/src/index.css b/src/index.css index 910f8af..6f181ab 100644 --- a/src/index.css +++ b/src/index.css @@ -23,8 +23,24 @@ body { padding: 0 10px; max-width: 900px; text-align: left; + font-size: var(--font-s); } -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; +button { + border-radius: 0; + border: none; + outline: none; + padding: 5px; + margin: 0 -5px; + background: var(--dark-grey); + color: var(--white); + font-size: var(--font-s); + cursor: pointer; + display: flex; + flex-flow: row nowrap; + align-items: center; +} + +button:hover { + text-decoration: underline; }
\ No newline at end of file |
