aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Tuomi <jans.tuomi@gmail.com>2022-02-05 16:48:16 +0200
committerJan Tuomi <jans.tuomi@gmail.com>2022-02-05 16:48:16 +0200
commita96d162f649704df5d9b567158366015cbe3172c (patch)
tree656f946eeb6748fe01447004399e08a45ff0b5f5
parent7d78bf4d2111af87636069ee2d35d6323ffc96be (diff)
Get basic stuff working
-rw-r--r--.eslintrc.js1
-rw-r--r--package-lock.json78
-rw-r--r--package.json2
-rw-r--r--src/App.module.css12
-rw-r--r--src/App.tsx38
-rw-r--r--src/Editor.module.css7
-rw-r--r--src/Editor.tsx68
-rw-r--r--src/Tutorial.tsx37
-rw-r--r--src/api.ts43
-rw-r--r--src/index.css20
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&apos;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
diff --git a/src/api.ts b/src/api.ts
index 7fd9e64..725f0d8 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -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