trying in vain to buildmy own json editor
This commit is contained in:
parent
261fe25549
commit
eb75c859f2
52
client/package-lock.json
generated
52
client/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 (
|
||||
<div className="commandline" style={{display: "none"}}>
|
||||
<form onSubmit={
|
||||
handleSubmit((data) => { onSubmitCommand(data.command); })
|
||||
}>
|
||||
<input {...register("command")}
|
||||
ref={ref}
|
||||
onChange={(e) => {
|
||||
setCommand(e.target.value);
|
||||
setValue("command", e.target.value);
|
||||
}}
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Enter a command!"
|
||||
value={command} />
|
||||
<button type="submit">Do</button>
|
||||
</form>
|
||||
<div className="commandline" >
|
||||
<input
|
||||
ref={ref}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Enter a command!"
|
||||
value={command} />
|
||||
<Fab onClick={ () => onSubmitCommand(command) } >
|
||||
Send
|
||||
</Fab>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -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} />
|
||||
<ExtendedAttributes
|
||||
attributes={fetchAttributesQuery.data} />
|
||||
@ -163,12 +165,12 @@ function Live({...props}) {
|
||||
}[readyState]}</button>
|
||||
<button onClick={()=> setMessageHistory([])}>Clear History</button>
|
||||
</MessageFeed>
|
||||
<Sidebar verbs={verbs} sendWord={(word) => {
|
||||
<WordSidebar verbs={verbs} sendWord={(word) => {
|
||||
setCommand((command + " " + word).trim());
|
||||
commandEntryRef.current.focus();
|
||||
// maybe set focus to the command entry?
|
||||
}}>
|
||||
</Sidebar>
|
||||
</WordSidebar>
|
||||
<CommandEntry
|
||||
ref={commandEntryRef}
|
||||
setCommand={(val) => {setCommand(val)}}
|
||||
|
@ -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 (
|
||||
<div className={`messages-tray ${!open?'messages-hidden':''}`}>
|
||||
<button style={{float: 'right'}} onClick={() => setOpen(!open)}>{open?'Hide':'Show'}</button>
|
||||
return (<>
|
||||
<IconButton
|
||||
size="large"
|
||||
style={{
|
||||
visible: open,
|
||||
position: "fixed",
|
||||
padding: "2rem",
|
||||
left: 0,
|
||||
top: 0
|
||||
}}
|
||||
onClick={() => setOpen(true)} >
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<SwipeableDrawer
|
||||
anchor="left"
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOpen={() => setOpen(true)}>
|
||||
<IconButton
|
||||
variant="text"
|
||||
onClick={() => setOpen(false)} >
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<h2>Message history:</h2>
|
||||
<ol ref={listContainer}>
|
||||
{messages.map((message, idx) =>
|
||||
@ -22,8 +49,8 @@ function MessageFeed({messages=[], children}) {
|
||||
)}
|
||||
</ol>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
</SwipeableDrawer>
|
||||
</>);
|
||||
}
|
||||
|
||||
export default MessageFeed;
|
@ -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 (
|
||||
<SwipeableDrawer
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOpen={() => setOpen(true)}>
|
||||
<ul>
|
||||
{verbs.map( (name) => (
|
||||
<li key={name}>
|
||||
<button onClick={() => { console.log("clicked button for", name); sendWord(name);}}>
|
||||
{name}
|
||||
</button>
|
||||
</li>
|
||||
) )}
|
||||
{children}
|
||||
</ul>
|
||||
</SwipeableDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
58
client/src/embodied/WordSidebar.jsx
Normal file
58
client/src/embodied/WordSidebar.jsx
Normal file
@ -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 (<>
|
||||
<IconButton
|
||||
size="large"
|
||||
variant="contained"
|
||||
style={{
|
||||
visible: open,
|
||||
position: "fixed",
|
||||
padding: "2rem",
|
||||
right: 0,
|
||||
top: 0
|
||||
}}
|
||||
onClick={() => setOpen(true)} >
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<SwipeableDrawer
|
||||
hideBackdrop={true}
|
||||
disableBackdropTransition={true}
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOpen={() => setOpen(true)}>
|
||||
<IconButton
|
||||
onClick={() => setOpen(false)} >
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<ul>
|
||||
{verbs.map( (name) => (
|
||||
<li key={name}>
|
||||
<Button onClick={() => {
|
||||
console.log("clicked button for", name);
|
||||
sendWord(name);
|
||||
}}>
|
||||
{name}
|
||||
</Button>
|
||||
</li>
|
||||
) )}
|
||||
{children}
|
||||
</ul>
|
||||
</SwipeableDrawer>
|
||||
</>);
|
||||
}
|
||||
|
||||
export default WordSidebar;
|
@ -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 <section className="page-contents">This object has no attributes.</section>;
|
||||
|
||||
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 <>
|
||||
<section className="page-contents">
|
||||
{attributes.parent ?
|
||||
`The parent of this object is #${attributes.parent}.`
|
||||
{parent ?
|
||||
`The parent of this object is #${parent}.`
|
||||
:
|
||||
"This object has no parent."}
|
||||
</section>
|
||||
|
||||
{attributes.location && <section className="page-contents">
|
||||
This object lives in #{JSON.stringify(attributes.location)}.
|
||||
</section>}
|
||||
<section className="page-contents">
|
||||
{location ?
|
||||
`This object lives in #${location}`
|
||||
:
|
||||
"This object has no location."}
|
||||
</section>
|
||||
|
||||
{attributes.contents && typeof attributes.contents.map === 'function' && <section className="page-contents">
|
||||
<h4>Contents of this location:</h4>
|
||||
<ul>
|
||||
{attributes.contents.map((objectNum) => <li key={objectNum}>{objectNum}</li>)}
|
||||
</ul>
|
||||
</section>}
|
||||
{attributes.verbs && <section className="page-contents">
|
||||
<h4>Verbs defined on this object:</h4>
|
||||
<ul>
|
||||
{attributes.verbs.map((verbNum) => <li key={verbNum}>{verbNum}</li>)}
|
||||
</ul>
|
||||
</section>}
|
||||
{attributes.prepositions && <section className="page-contents">
|
||||
<h4>Prepositions this verb expects:</h4>
|
||||
<ul>
|
||||
{attributes.prepositions.map((prep) => <li key={prep}>{prep}</li>)}
|
||||
</ul>
|
||||
</section>}
|
||||
<AttrList
|
||||
list={contents}
|
||||
title="Objects in this location:" />
|
||||
|
||||
<AttrList
|
||||
list={verbs}
|
||||
title="Verbs defined on this object:" />
|
||||
|
||||
<AttrList
|
||||
list={prepositions}
|
||||
title="Prepositions this verb expects:" />
|
||||
|
||||
<JsonDisplay obj={newAttributes} />
|
||||
</>
|
||||
}
|
||||
|
||||
|
109
client/src/page/ExtendedAttributesEdit.jsx
Normal file
109
client/src/page/ExtendedAttributesEdit.jsx
Normal file
@ -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 <section className="page-contents">Attributes is falsy!</section>;
|
||||
}
|
||||
|
||||
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 = <section className="page-contents">
|
||||
This object does not have a parent.
|
||||
<button
|
||||
onClick={() => setAttributes((a) => {return {...a, parent: ""}}) }
|
||||
>
|
||||
Set Parent
|
||||
</button>
|
||||
</section>;
|
||||
} else {
|
||||
parentSnip = <section className="page-contents">
|
||||
The parent of this object is #
|
||||
<input
|
||||
type="text"
|
||||
value={`${attributes.parent}`}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setAttributes((a) => {return {...a, parent: Number(val) };});
|
||||
}}
|
||||
/>.
|
||||
<button
|
||||
onClick={() =>setAttributes((a) => {return {...a, parent: undefined};})} >
|
||||
Clear Parent
|
||||
</button>
|
||||
</section>;
|
||||
}
|
||||
|
||||
let locationSnip;
|
||||
if (!attributes.hasOwnProperty("location") || attributes.location === null || attributes.location === undefined) {
|
||||
locationSnip = <section className="page-contents">
|
||||
This object does not have a location.
|
||||
<button
|
||||
onClick={() => setAttributes((a) => {return {...a, location: ""}}) }
|
||||
>
|
||||
Set Location
|
||||
</button>
|
||||
</section>;
|
||||
} else {
|
||||
locationSnip = <section className="page-contents">
|
||||
The location of this object is #
|
||||
<input
|
||||
type="text"
|
||||
value={attributes.location}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setAttributes((a) => {return {...a, location: Number(val) };});
|
||||
}}
|
||||
/>.
|
||||
<button
|
||||
onClick={() => setAttributes((a) => {return {...a, location: undefined};})} >
|
||||
Clear Location
|
||||
</button>
|
||||
</section>;
|
||||
}
|
||||
|
||||
return <>
|
||||
{parentSnip}
|
||||
|
||||
{locationSnip}
|
||||
|
||||
<AttrListEdit
|
||||
title="Contents of this location:"
|
||||
list={attributes.contents}
|
||||
onChange={attributeListSetter('contents')} />
|
||||
|
||||
<AttrListEdit
|
||||
title="Verbs defined on this object:"
|
||||
list={attributes.verbs}
|
||||
onChange={attributeListSetter('verbs')} />
|
||||
|
||||
<AttrListEdit
|
||||
title="Prepositions this verb expects:"
|
||||
list={attributes.prepositions}
|
||||
onChange={attributeListSetter('prepositions')} />
|
||||
|
||||
<JsonDisplay
|
||||
obj={newAttributes} />
|
||||
</>
|
||||
}
|
||||
|
||||
export default ExtendedAttributesEdit;
|
@ -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 <div className="main-column">
|
||||
<Page
|
||||
page={{...fetchPageQuery.data, description: text, title: title, type: type }}
|
||||
page={{...fetchPageQuery.data, title: title, type: type }}
|
||||
editing={editing}
|
||||
linkClick={linkClick}
|
||||
linkClick={navigateLinkClick}
|
||||
onChangeTitle={(e) => setTitle(e.target.value)}
|
||||
onChangeText={setText}
|
||||
onChangeType={() => setType(!type)}
|
||||
ref={editorRef}
|
||||
{...props}/>
|
||||
<ExtendedAttributes
|
||||
attributes={fetchAttributesQuery.data} />
|
||||
{ !editing ?
|
||||
<ExtendedAttributes
|
||||
attributes={attributes} />
|
||||
:
|
||||
<ExtendedAttributesEdit
|
||||
attributes={attributes}
|
||||
setAttributes={setAttributes} />
|
||||
}
|
||||
<button
|
||||
onClick={() => navigate(`/${pagenumber}/history`)}>
|
||||
History
|
||||
@ -86,10 +96,15 @@ function GhostPage({editing, ...props}) {
|
||||
onClick={submitChanges}>
|
||||
{postMutation.isPending ? "Updating..." : "Update"}
|
||||
</button>)}
|
||||
{editing && (
|
||||
<button
|
||||
onClick={() => navigate(`/${pagenumber}`)}>
|
||||
Return without saving
|
||||
</button>)}
|
||||
{!editing && !editid && (
|
||||
<button
|
||||
disabled={!loggedIn}
|
||||
onClick={() => navigate(`/${pagenumber}/edit`, {replace: true})}>
|
||||
onClick={() => navigate(`/${pagenumber}/edit`)}>
|
||||
Edit Page
|
||||
</button>)}
|
||||
</div>;
|
||||
|
@ -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 (<>
|
||||
<header>
|
||||
<h1>
|
||||
<a href="/" onClick={linkClick}>🌳</a>
|
||||
{page?.number}.
|
||||
{ editing ?
|
||||
<input disabled={!editing} value={title} onChange={onChangeTitle}/>
|
||||
<input disabled={!editing} value={title || ""} onChange={onChangeTitle}/>
|
||||
:
|
||||
<span>{title}</span>
|
||||
}
|
||||
@ -39,8 +41,11 @@ function Page({
|
||||
{ historical && <p>saved at {time} by user #{author}</p> }
|
||||
{ editing ?
|
||||
<label>
|
||||
<input type="checkbox" checked={type} onChange={onChangeType}/>
|
||||
{type ? "verb" : "noun"}
|
||||
noun
|
||||
<Switch
|
||||
checked={type}
|
||||
onChange={onChangeType}/>
|
||||
verb
|
||||
</label>
|
||||
:
|
||||
<br/>
|
||||
@ -50,7 +55,7 @@ function Page({
|
||||
<section className="page-contents">
|
||||
{ editing ?
|
||||
<MDXEditor
|
||||
markdown={description}
|
||||
markdown="nothin here"
|
||||
plugins={[
|
||||
headingsPlugin(),
|
||||
quotePlugin(),
|
||||
@ -59,7 +64,7 @@ function Page({
|
||||
linkPlugin(),
|
||||
diffSourcePlugin({ diffMarkdown: 'ahhhh do not look upon me!', viewMode: 'source' }),
|
||||
]}
|
||||
onChange={onChangeText}
|
||||
ref={ref}
|
||||
/>
|
||||
:
|
||||
( type ?
|
||||
@ -73,6 +78,6 @@ function Page({
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default Page;
|
19
client/src/page/components/AttrList.jsx
Normal file
19
client/src/page/components/AttrList.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
function AttrList({list, title, onChange, children}) {
|
||||
if (!list)
|
||||
return null;
|
||||
|
||||
if (!list || !list.map || typeof list.map != 'function')
|
||||
return <section className="page-contents">
|
||||
<h4>{title}</h4>
|
||||
value is not a list: {list}
|
||||
</section>;
|
||||
|
||||
return <section className="page-contents">
|
||||
<h4>{title}</h4>
|
||||
<ul>
|
||||
{list.map((objectNum) => <li key={objectNum}>{objectNum}</li>)}
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
|
||||
export default AttrList;
|
44
client/src/page/components/AttrListEdit.jsx
Normal file
44
client/src/page/components/AttrListEdit.jsx
Normal file
@ -0,0 +1,44 @@
|
||||
function AttrListEdit({list, title, onChange, children}) {
|
||||
if (list === null || list === undefined)
|
||||
return <section className="page-contents">
|
||||
<h4>{title}</h4>
|
||||
Not set.
|
||||
<button onClick={() => onChange([])}>Make empty list</button>
|
||||
</section>;
|
||||
|
||||
if (!list.map || typeof list.map != 'function')
|
||||
return <section className="page-contents">
|
||||
<h4>{title}</h4>
|
||||
value is not a list: {list}
|
||||
<button onClick={() => onChange([])}>Make empty list</button>
|
||||
<button onClick={() => onChange(undefined)}>Clear field</button>
|
||||
</section>;
|
||||
|
||||
return <section className="page-contents">
|
||||
<h4>{title}</h4>
|
||||
<ul>
|
||||
{list.map((objectNum, index) => (
|
||||
<li key={index}>
|
||||
<input
|
||||
type="text"
|
||||
value={objectNum}
|
||||
onChange={(e) => onChange((list) => { list[index] = Number(e.target.value); } )}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
onChange((list) => {
|
||||
delete list[index];
|
||||
return list;
|
||||
})
|
||||
}>
|
||||
clear element
|
||||
</button>
|
||||
</li>)
|
||||
)}
|
||||
<li><input value="" placeholder="add element..."/></li>
|
||||
</ul>
|
||||
<button onClick={() => onChange(undefined)}>Clear field</button>
|
||||
</section>
|
||||
}
|
||||
|
||||
export default AttrListEdit;
|
15
client/src/page/components/JsonDisplay.jsx
Normal file
15
client/src/page/components/JsonDisplay.jsx
Normal file
@ -0,0 +1,15 @@
|
||||
function JsonDisplay({obj}) {
|
||||
if (!obj)
|
||||
return <section className="page-contents">No additional attributes found.</section>;
|
||||
|
||||
let empty = true;
|
||||
for (const prop in obj) {
|
||||
if (Object.hasOwn(obj, prop) && obj[prop] !== undefined) empty = false;
|
||||
}
|
||||
if (empty)
|
||||
return <section className="page-contents">No additional attributes found.</section>;
|
||||
|
||||
return <section className="page-contents">{JSON.stringify(obj)}</section>;
|
||||
}
|
||||
|
||||
export default JsonDisplay;
|
Loading…
Reference in New Issue
Block a user