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 ( -
-
{ onSubmitCommand(data.command); }) - }> - { - setCommand(e.target.value); - setValue("command", e.target.value); - }} - autoFocus - type="text" - placeholder="Enter a command!" - value={command} /> - -
+
+ 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
Attributes is falsy!
; + } + + 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 =
+ The parent of this object is # + { + const val = e.target.value; + setAttributes((a) => {return {...a, parent: Number(val) };}); + }} + />. + +
; + } + + let locationSnip; + if (!attributes.hasOwnProperty("location") || attributes.location === null || attributes.location === undefined) { + locationSnip =
+ This object does not have a location. + +
; + } else { + locationSnip =
+ The location of this object is # + { + const val = e.target.value; + setAttributes((a) => {return {...a, location: Number(val) };}); + }} + />. + +
; + } + + 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 (<>

🌳 {page?.number}.  { editing ? - + : {title} } @@ -39,8 +41,11 @@ function Page({ { historical &&

saved at {time} by user #{author}

} { editing ? :
@@ -50,7 +55,7 @@ function Page({
{ editing ? : ( type ? @@ -73,6 +78,6 @@ function Page({
); -} +}); export default Page; \ No newline at end of file diff --git a/client/src/page/components/AttrList.jsx b/client/src/page/components/AttrList.jsx new file mode 100644 index 00000000..e1483038 --- /dev/null +++ b/client/src/page/components/AttrList.jsx @@ -0,0 +1,19 @@ +function AttrList({list, title, onChange, children}) { + if (!list) + return null; + + if (!list || !list.map || typeof list.map != 'function') + return
+

{title}

+ value is not a list: {list} +
; + + return
+

{title}

+
    + {list.map((objectNum) =>
  • {objectNum}
  • )} +
+
+} + +export default AttrList; \ No newline at end of file diff --git a/client/src/page/components/AttrListEdit.jsx b/client/src/page/components/AttrListEdit.jsx new file mode 100644 index 00000000..fbb35f3c --- /dev/null +++ b/client/src/page/components/AttrListEdit.jsx @@ -0,0 +1,44 @@ +function AttrListEdit({list, title, onChange, children}) { + if (list === null || list === undefined) + return
+

{title}

+ Not set. + +
; + + if (!list.map || typeof list.map != 'function') + return
+

{title}

+ value is not a list: {list} + + +
; + + return
+

{title}

+
    + {list.map((objectNum, index) => ( +
  • + onChange((list) => { list[index] = Number(e.target.value); } )} + /> + +
  • ) + )} +
  • +
+ +
+} + +export default AttrListEdit; \ No newline at end of file diff --git a/client/src/page/components/JsonDisplay.jsx b/client/src/page/components/JsonDisplay.jsx new file mode 100644 index 00000000..cadb52b1 --- /dev/null +++ b/client/src/page/components/JsonDisplay.jsx @@ -0,0 +1,15 @@ +function JsonDisplay({obj}) { + if (!obj) + return
No additional attributes found.
; + + let empty = true; + for (const prop in obj) { + if (Object.hasOwn(obj, prop) && obj[prop] !== undefined) empty = false; + } + if (empty) + return
No additional attributes found.
; + + return
{JSON.stringify(obj)}
; +} + +export default JsonDisplay; \ No newline at end of file