diff --git a/client/package-lock.json b/client/package-lock.json
index 2e2c64af..c539c350 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -12,6 +12,7 @@
"@emotion/styled": "^11.13.0",
"@libsql/client": "^0.11.0",
"@mdxeditor/editor": "^3.14.0",
+ "@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5",
"@react-sigma/core": "^4.0.3",
"@tanstack/react-query": "^5.56.2",
@@ -23,6 +24,7 @@
"graphology-layout": "^0.6.1",
"graphology-layout-force": "^0.2.4",
"highlight.js": "^11.10.0",
+ "json-edit-react": "^1.17.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
@@ -1994,6 +1996,31 @@
"url": "https://opencollective.com/mui-org"
}
},
+ "node_modules/@mui/icons-material": {
+ "version": "6.1.5",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.5.tgz",
+ "integrity": "sha512-SbxFtO5I4cXfvhjAMgGib/t2lQUzcEzcDFYiRHRufZUeMMeXuoKaGsptfwAHTepYkv0VqcCwvxtvtWbpZLAbjQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.25.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@mui/material": "^6.1.5",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@mui/material": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.5.tgz",
@@ -5515,6 +5542,19 @@
"node": ">=4"
}
},
+ "node_modules/json-edit-react": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/json-edit-react/-/json-edit-react-1.17.1.tgz",
+ "integrity": "sha512-UDGkBpgPoJplyT2MaPbGbKQhQjgvswaDWNbKCEkorq3r72DVm5qQErvribrc/ioZsqSnzCeDT8EwzMi4L2Az1g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-property-assigner": "^1.3.0",
+ "object-property-extractor": "^1.0.11"
+ },
+ "peerDependencies": {
+ "react": ">=16.0.0"
+ }
+ },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -6669,6 +6709,18 @@
"node": ">= 0.4"
}
},
+ "node_modules/object-property-assigner": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/object-property-assigner/-/object-property-assigner-1.3.1.tgz",
+ "integrity": "sha512-3R6x1l1sQA4jQBOqxkxH3DHw0PMh7K5UY4x1Yc5QYbbkzZpAJ8l9w+Trq/64ZRe2thiOsa8JpB4M7D61hnkjRA==",
+ "license": "MIT"
+ },
+ "node_modules/object-property-extractor": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/object-property-extractor/-/object-property-extractor-1.0.12.tgz",
+ "integrity": "sha512-X6rMK1O6O3EU4O06BtATezWKr+5idX6Yo0PndVqr8NlED3Eoa2YE+VKQgQL4EK0+vh+YA83Sjj9Nsi/PuXXeDQ==",
+ "license": "MIT"
+ },
"node_modules/object.assign": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
diff --git a/client/package.json b/client/package.json
index 745b470f..1f886f31 100644
--- a/client/package.json
+++ b/client/package.json
@@ -7,6 +7,7 @@
"@emotion/styled": "^11.13.0",
"@libsql/client": "^0.11.0",
"@mdxeditor/editor": "^3.14.0",
+ "@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5",
"@react-sigma/core": "^4.0.3",
"@tanstack/react-query": "^5.56.2",
@@ -18,6 +19,7 @@
"graphology-layout": "^0.6.1",
"graphology-layout-force": "^0.2.4",
"highlight.js": "^11.10.0",
+ "json-edit-react": "^1.17.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
diff --git a/client/src/embodied/CommandEntry.jsx b/client/src/embodied/CommandEntry.jsx
index a9c51eb1..ceaabb6b 100644
--- a/client/src/embodied/CommandEntry.jsx
+++ b/client/src/embodied/CommandEntry.jsx
@@ -1,28 +1,22 @@
import { forwardRef } from 'react';
-import { useForm } from "react-hook-form";
+import { Fab } from '@mui/material';
import './CommandEntry.css';
const CommandEntry = forwardRef(({ setCommand, onSubmitCommand, command }, ref) => {
- const { register, handleSubmit, setValue } = useForm();
return (
-
-
+
+ setCommand(e.target.value)}
+ autoFocus
+ type="text"
+ placeholder="Enter a command!"
+ value={command} />
+ onSubmitCommand(command) } >
+ Send
+
);
});
diff --git a/client/src/embodied/Live.jsx b/client/src/embodied/Live.jsx
index 805340f9..21d058b3 100644
--- a/client/src/embodied/Live.jsx
+++ b/client/src/embodied/Live.jsx
@@ -9,7 +9,7 @@ import useWebSocket, { ReadyState } from 'react-use-websocket';
import Page from '../page/Page.jsx';
import ExtendedAttributes from '../page/ExtendedAttributes.jsx';
import MessageFeed from './MessageFeed.jsx';
-import Sidebar from './Sidebar.jsx';
+import WordSidebar from './WordSidebar.jsx';
import CommandEntry from './CommandEntry.jsx';
function Live({...props}) {
@@ -23,6 +23,7 @@ function Live({...props}) {
const [ editing, setEditing ] = useState(false);
const [ connecting, setConnecting ] = useState(true);
+ const editorRef = useRef(null);
const commandEntryRef = useRef(null);
//setting up the websocket and using its data!
@@ -148,6 +149,7 @@ function Live({...props}) {
onChangeTitle={(e) => setTitle(e.target.value)}
onChangeText={setText}
onChangeType={() => setType(!type)}
+ ref={editorRef}
{...props} />
@@ -163,12 +165,12 @@ function Live({...props}) {
}[readyState]}
-
{
+ {
setCommand((command + " " + word).trim());
commandEntryRef.current.focus();
// maybe set focus to the command entry?
}}>
-
+
{setCommand(val)}}
diff --git a/client/src/embodied/MessageFeed.jsx b/client/src/embodied/MessageFeed.jsx
index c5d25f8a..16c550cb 100644
--- a/client/src/embodied/MessageFeed.jsx
+++ b/client/src/embodied/MessageFeed.jsx
@@ -1,6 +1,10 @@
import { useEffect, useState, useRef } from 'react';
import { useForm } from "react-hook-form";
import { useNavigate } from 'react-router-dom';
+import { IconButton, SwipeableDrawer } from '@mui/material';
+
+import MenuIcon from '@mui/icons-material/Menu';
+import CloseIcon from '@mui/icons-material/Close';
import './MessageFeed.css';
@@ -8,11 +12,34 @@ function MessageFeed({messages=[], children}) {
const [ open, setOpen ] = useState(false);
const listContainer = useRef(null);
- useEffect(() => listContainer.current.scrollTo(0, listContainer.current.scrollHeight), [listContainer, messages])
+ useEffect(() => {
+ if (listContainer.current)
+ listContainer.current.scrollTo(0, listContainer.current.scrollHeight);
+ }, [listContainer, messages])
- return (
-
-
+ return (<>
+
setOpen(true)} >
+
+
+
setOpen(false)}
+ onOpen={() => setOpen(true)}>
+ setOpen(false)} >
+
+
Message history:
{messages.map((message, idx) =>
@@ -22,8 +49,8 @@ function MessageFeed({messages=[], children}) {
)}
{children}
-
- );
+
+ >);
}
export default MessageFeed;
\ No newline at end of file
diff --git a/client/src/embodied/Sidebar.jsx b/client/src/embodied/Sidebar.jsx
deleted file mode 100644
index 90fe7729..00000000
--- a/client/src/embodied/Sidebar.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useState } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { useLoggedIn } from '../AuthProvider.jsx';
-import { fetchCurrentVerbs } from '../apiTools.jsx';
-import { SwipeableDrawer } from '@mui/material';
-
-import './Sidebar.css';
-
-function Sidebar({children, verbs, hidden=true, sendWord=(()=>null)}) {
- const [open, setOpen] = useState(!hidden);
- const loggedIn = useLoggedIn();
-
- return (
- setOpen(false)}
- onOpen={() => setOpen(true)}>
-
- {verbs.map( (name) => (
- -
-
-
- ) )}
- {children}
-
-
- );
-}
-
-export default Sidebar;
\ No newline at end of file
diff --git a/client/src/embodied/WordSidebar.jsx b/client/src/embodied/WordSidebar.jsx
new file mode 100644
index 00000000..4367f1d4
--- /dev/null
+++ b/client/src/embodied/WordSidebar.jsx
@@ -0,0 +1,58 @@
+import { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { useLoggedIn } from '../AuthProvider.jsx';
+import { fetchCurrentVerbs } from '../apiTools.jsx';
+import { Button, IconButton, SwipeableDrawer } from '@mui/material';
+
+import MenuIcon from '@mui/icons-material/Menu';
+import CloseIcon from '@mui/icons-material/Close';
+
+import './Sidebar.css';
+
+function WordSidebar({children, verbs, hidden=true, sendWord=(()=>null)}) {
+ const [open, setOpen] = useState(!hidden);
+ const loggedIn = useLoggedIn();
+
+ return (<>
+ setOpen(true)} >
+
+
+ setOpen(false)}
+ onOpen={() => setOpen(true)}>
+ setOpen(false)} >
+
+
+
+ {verbs.map( (name) => (
+ -
+
+
+ ) )}
+ {children}
+
+
+ >);
+}
+
+export default WordSidebar;
\ No newline at end of file
diff --git a/client/src/page/ExtendedAttributes.jsx b/client/src/page/ExtendedAttributes.jsx
index b7d9fbeb..d078a65c 100644
--- a/client/src/page/ExtendedAttributes.jsx
+++ b/client/src/page/ExtendedAttributes.jsx
@@ -1,36 +1,55 @@
-function ExtendedAttributes({attributes, ...props}) {
+import AttrList from './components/AttrList.jsx';
+import JsonDisplay from './components/JsonDisplay.jsx';
+
+function ExtendedAttributes({attributes, ...props}) {
+ console.log(attributes);
+
+ if (!attributes)
+ return This object has no attributes.;
+
+ const {parent, location, contents, verbs, prepositions} = attributes;
+
+ let newAttributes = {}
+ for (const prop in attributes) {
+ if (Object.hasOwn(attributes, prop)) {
+ newAttributes[prop] = attributes[prop]
+ }
+ }
+
+ delete newAttributes["parent"];
+ delete newAttributes["location"];
+ delete newAttributes["contents"];
+ delete newAttributes["verbs"];
+ delete newAttributes["prepositions"];
- if (!attributes) return;
return <>
- {attributes.parent ?
- `The parent of this object is #${attributes.parent}.`
+ {parent ?
+ `The parent of this object is #${parent}.`
:
"This object has no parent."}
-
- {attributes.location &&
- This object lives in #{JSON.stringify(attributes.location)}.
- }
- {attributes.contents && typeof attributes.contents.map === 'function' &&
- Contents of this location:
-
- {attributes.contents.map((objectNum) => - {objectNum}
)}
-
- }
- {attributes.verbs &&
- Verbs defined on this object:
-
- {attributes.verbs.map((verbNum) => - {verbNum}
)}
-
- }
- {attributes.prepositions &&
- Prepositions this verb expects:
-
- {attributes.prepositions.map((prep) => - {prep}
)}
-
- }
+
+ {location ?
+ `This object lives in #${location}`
+ :
+ "This object has no location."}
+
+
+
+
+
+
+
+
+
>
}
diff --git a/client/src/page/ExtendedAttributesEdit.jsx b/client/src/page/ExtendedAttributesEdit.jsx
new file mode 100644
index 00000000..4029ac88
--- /dev/null
+++ b/client/src/page/ExtendedAttributesEdit.jsx
@@ -0,0 +1,109 @@
+import AttrListEdit from './components/AttrListEdit.jsx';
+import JsonDisplay from './components/JsonDisplay.jsx';
+
+function ExtendedAttributesEdit({attributes, setAttributes, ...props}) {
+ console.log("rendering with attributes: ", attributes);
+
+ if (!attributes) {
+ return ;
+ }
+
+ function attributeListSetter(attributeName) {
+ return (newValue) => setAttributes((a) => {
+ if (a === null) return a;
+ let newList = {...a}
+ newList[attributeName] = newValue;
+ return newList;
+ });
+ }
+
+ let newAttributes = {...attributes,
+ parent: undefined,
+ location: undefined,
+ contents: undefined,
+ verbs: undefined,
+ prepositions: undefined
+ }
+
+ let parentSnip;
+ if (!attributes.hasOwnProperty("parent") || attributes.parent === null || attributes.parent === undefined) {
+ parentSnip =
+ This object does not have a parent.
+
+ ;
+ } else {
+ parentSnip = ;
+ }
+
+ let locationSnip;
+ if (!attributes.hasOwnProperty("location") || attributes.location === null || attributes.location === undefined) {
+ locationSnip =
+ This object does not have a location.
+
+ ;
+ } else {
+ locationSnip = ;
+ }
+
+ return <>
+ {parentSnip}
+
+ {locationSnip}
+
+
+
+
+
+
+
+
+ >
+}
+
+export default ExtendedAttributesEdit;
\ No newline at end of file
diff --git a/client/src/page/GhostPage.jsx b/client/src/page/GhostPage.jsx
index ac613854..c9e8a8c8 100644
--- a/client/src/page/GhostPage.jsx
+++ b/client/src/page/GhostPage.jsx
@@ -1,10 +1,14 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import { apiUrl, fetchPage, fetchPageAttributes, postPage } from '../apiTools.jsx';
+
import { useLoggedIn } from '../AuthProvider.jsx';
+
import Page from './Page.jsx';
import ExtendedAttributes from './ExtendedAttributes.jsx';
+//import ExtendedAttributesEdit from './ExtendedAttributesEdit.jsx';
+import { JsonEditor } from 'json-edit-react';
import './Pages.css';
@@ -14,68 +18,74 @@ function GhostPage({editing, ...props}) {
const loggedIn = useLoggedIn();
const queryClient = useQueryClient();
-
- const linkClick = (e) => {
- if (e.target.tagName != "A") { return; }
- if (!e.target.href.includes(window.location.origin)) return;
- e.preventDefault();
- navigate(e.target.href.replace(window.location.origin, ""));
- };
-
const fetchPageQuery = useQuery({
queryKey: ['page', pagenumber, null],
queryFn: () => fetchPage(pagenumber),
});
+
+ const [title, setTitle] = useState(null);
+ useEffect(() => setTitle(fetchPageQuery.data?.title), [fetchPageQuery.data?.title]);
+
+ const [type, setType] = useState(false);
+ useEffect(() => setType(fetchPageQuery.data?.type == 1), [fetchPageQuery.data?.type]);
+
+ const editorRef = useRef(null);
+
const fetchAttributesQuery = useQuery({
queryKey: ['attributes', pagenumber],
queryFn: () => fetchPageAttributes(pagenumber)
});
- const [title, setTitle] = useState(null);
- useEffect(() => {
- setTitle(fetchPageQuery.data?.title);
- }, [fetchPageQuery.data?.title]);
-
- const [text, setText] = useState(null);
- useEffect(() => {
- setText(fetchPageQuery.data?.description);
- }, [fetchPageQuery.data?.description]);
-
- const [type, setType] = useState(false);
- useEffect(() => {
- setType(fetchPageQuery.data?.type == 1);
- }, [fetchPageQuery.data?.type]);
+ const [attributes, setAttributes] = useState(null);
+ useEffect(() => setAttributes(fetchAttributesQuery.data), [fetchAttributesQuery.data]);
const postMutation = useMutation({ // for changing the value when we're done with it
mutationFn: postPage,
onSettled: async (data, error, variables) => {
// Invalidate and refetch
- await queryClient.invalidateQueries(['page', variables.number, null])
- console.log("shoulda just invalidated the thing");
+ await queryClient.invalidateQueries(['page', variables.number, null]);
navigate(`/${pagenumber}`);
},
});
function submitChanges(e) {
- postMutation.mutate({
- number: pagenumber,
- title: title,
- description: text,
- type: type ? 1 : 0,
- });
+ if (editorRef.current) {
+ postMutation.mutate({
+ number: pagenumber,
+ title: title,
+ description: editorRef.current.getMarkdown(),
+ type: type ? 1 : 0,
+ });
+ }
}
+ const navigateLinkClick = (e) => {
+ if (e.target.tagName != "A") return;
+ if (!e.target.href.includes(window.location.origin)) return;
+ if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
+
+ e.preventDefault();
+
+ navigate(e.target.href.replace(window.location.origin, ""));
+ };
+
return
setTitle(e.target.value)}
- onChangeText={setText}
onChangeType={() => setType(!type)}
+ ref={editorRef}
{...props}/>
-
+ { !editing ?
+
+ :
+
+ }
)}
+ {editing && (
+ )}
{!editing && !editid && (
)}
;
diff --git a/client/src/page/Page.jsx b/client/src/page/Page.jsx
index 85709c06..65c058dd 100644
--- a/client/src/page/Page.jsx
+++ b/client/src/page/Page.jsx
@@ -1,8 +1,10 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, forwardRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiUrl, fetchPage, fetchPageAtEdit, postPage } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx';
+import { Switch } from '@mui/material';
+
import { MDXEditor,
headingsPlugin, quotePlugin, listsPlugin,
thematicBreakPlugin, linkPlugin, diffSourcePlugin } from '@mdxeditor/editor';
@@ -11,27 +13,27 @@ import './Pages.css';
import 'highlight.js/styles/a11y-dark.min.css';
import '@mdxeditor/editor/style.css';
-function Page({
+const Page = forwardRef(({
page=null,
editing, historical,
linkClick=()=>{},
onChangeTitle=()=>{},
onChangeType=()=>{},
- onChangeText=()=>{},
- }) {
+ }, ref) => {
let {title, description, html, lua, time, author, type} = page || {};
- if (!title) title = "";
- if (!html) html = "[body missing]";
- if (!lua) lua = "[no definition]";
- if (!description) description = "[body missing]";
+ useEffect(() => {
+ if (ref.current && description) {
+ ref.current.setMarkdown(description);
+ }
+ }, [description, ref.current, editing])
return (<>