trying in vain to buildmy own json editor

This commit is contained in:
Shoofle 2024-11-01 12:27:39 -04:00
parent 261fe25549
commit eb75c859f2
14 changed files with 465 additions and 137 deletions

View File

@ -12,6 +12,7 @@
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@libsql/client": "^0.11.0", "@libsql/client": "^0.11.0",
"@mdxeditor/editor": "^3.14.0", "@mdxeditor/editor": "^3.14.0",
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5", "@mui/material": "^6.1.5",
"@react-sigma/core": "^4.0.3", "@react-sigma/core": "^4.0.3",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
@ -23,6 +24,7 @@
"graphology-layout": "^0.6.1", "graphology-layout": "^0.6.1",
"graphology-layout-force": "^0.2.4", "graphology-layout-force": "^0.2.4",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"json-edit-react": "^1.17.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.53.0",
@ -1994,6 +1996,31 @@
"url": "https://opencollective.com/mui-org" "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": { "node_modules/@mui/material": {
"version": "6.1.5", "version": "6.1.5",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.5.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.5.tgz",
@ -5515,6 +5542,19 @@
"node": ">=4" "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": { "node_modules/json-parse-even-better-errors": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "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": ">= 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": { "node_modules/object.assign": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",

View File

@ -7,6 +7,7 @@
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@libsql/client": "^0.11.0", "@libsql/client": "^0.11.0",
"@mdxeditor/editor": "^3.14.0", "@mdxeditor/editor": "^3.14.0",
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5", "@mui/material": "^6.1.5",
"@react-sigma/core": "^4.0.3", "@react-sigma/core": "^4.0.3",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
@ -18,6 +19,7 @@
"graphology-layout": "^0.6.1", "graphology-layout": "^0.6.1",
"graphology-layout-force": "^0.2.4", "graphology-layout-force": "^0.2.4",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"json-edit-react": "^1.17.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.53.0",

View File

@ -1,28 +1,22 @@
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { useForm } from "react-hook-form"; import { Fab } from '@mui/material';
import './CommandEntry.css'; import './CommandEntry.css';
const CommandEntry = forwardRef(({ setCommand, onSubmitCommand, command }, ref) => { const CommandEntry = forwardRef(({ setCommand, onSubmitCommand, command }, ref) => {
const { register, handleSubmit, setValue } = useForm();
return ( return (
<div className="commandline" style={{display: "none"}}> <div className="commandline" >
<form onSubmit={ <input
handleSubmit((data) => { onSubmitCommand(data.command); }) ref={ref}
}> onChange={(e) => setCommand(e.target.value)}
<input {...register("command")} autoFocus
ref={ref} type="text"
onChange={(e) => { placeholder="Enter a command!"
setCommand(e.target.value); value={command} />
setValue("command", e.target.value); <Fab onClick={ () => onSubmitCommand(command) } >
}} Send
autoFocus </Fab>
type="text"
placeholder="Enter a command!"
value={command} />
<button type="submit">Do</button>
</form>
</div> </div>
); );
}); });

View File

@ -9,7 +9,7 @@ import useWebSocket, { ReadyState } from 'react-use-websocket';
import Page from '../page/Page.jsx'; import Page from '../page/Page.jsx';
import ExtendedAttributes from '../page/ExtendedAttributes.jsx'; import ExtendedAttributes from '../page/ExtendedAttributes.jsx';
import MessageFeed from './MessageFeed.jsx'; import MessageFeed from './MessageFeed.jsx';
import Sidebar from './Sidebar.jsx'; import WordSidebar from './WordSidebar.jsx';
import CommandEntry from './CommandEntry.jsx'; import CommandEntry from './CommandEntry.jsx';
function Live({...props}) { function Live({...props}) {
@ -23,6 +23,7 @@ function Live({...props}) {
const [ editing, setEditing ] = useState(false); const [ editing, setEditing ] = useState(false);
const [ connecting, setConnecting ] = useState(true); const [ connecting, setConnecting ] = useState(true);
const editorRef = useRef(null);
const commandEntryRef = useRef(null); const commandEntryRef = useRef(null);
//setting up the websocket and using its data! //setting up the websocket and using its data!
@ -148,6 +149,7 @@ function Live({...props}) {
onChangeTitle={(e) => setTitle(e.target.value)} onChangeTitle={(e) => setTitle(e.target.value)}
onChangeText={setText} onChangeText={setText}
onChangeType={() => setType(!type)} onChangeType={() => setType(!type)}
ref={editorRef}
{...props} /> {...props} />
<ExtendedAttributes <ExtendedAttributes
attributes={fetchAttributesQuery.data} /> attributes={fetchAttributesQuery.data} />
@ -163,12 +165,12 @@ function Live({...props}) {
}[readyState]}</button> }[readyState]}</button>
<button onClick={()=> setMessageHistory([])}>Clear History</button> <button onClick={()=> setMessageHistory([])}>Clear History</button>
</MessageFeed> </MessageFeed>
<Sidebar verbs={verbs} sendWord={(word) => { <WordSidebar verbs={verbs} sendWord={(word) => {
setCommand((command + " " + word).trim()); setCommand((command + " " + word).trim());
commandEntryRef.current.focus(); commandEntryRef.current.focus();
// maybe set focus to the command entry? // maybe set focus to the command entry?
}}> }}>
</Sidebar> </WordSidebar>
<CommandEntry <CommandEntry
ref={commandEntryRef} ref={commandEntryRef}
setCommand={(val) => {setCommand(val)}} setCommand={(val) => {setCommand(val)}}

View File

@ -1,6 +1,10 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from 'react-router-dom'; 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'; import './MessageFeed.css';
@ -8,11 +12,34 @@ function MessageFeed({messages=[], children}) {
const [ open, setOpen ] = useState(false); const [ open, setOpen ] = useState(false);
const listContainer = useRef(null); 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 (<>
<div className={`messages-tray ${!open?'messages-hidden':''}`}> <IconButton
<button style={{float: 'right'}} onClick={() => setOpen(!open)}>{open?'Hide':'Show'}</button> 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> <h2>Message history:</h2>
<ol ref={listContainer}> <ol ref={listContainer}>
{messages.map((message, idx) => {messages.map((message, idx) =>
@ -22,8 +49,8 @@ function MessageFeed({messages=[], children}) {
)} )}
</ol> </ol>
{children} {children}
</div> </SwipeableDrawer>
); </>);
} }
export default MessageFeed; export default MessageFeed;

View File

@ -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;

View 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;

View File

@ -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 <> return <>
<section className="page-contents"> <section className="page-contents">
{attributes.parent ? {parent ?
`The parent of this object is #${attributes.parent}.` `The parent of this object is #${parent}.`
: :
"This object has no parent."} "This object has no parent."}
</section> </section>
{attributes.location && <section className="page-contents"> <section className="page-contents">
This object lives in #{JSON.stringify(attributes.location)}. {location ?
</section>} `This object lives in #${location}`
:
"This object has no location."}
</section>
{attributes.contents && typeof attributes.contents.map === 'function' && <section className="page-contents"> <AttrList
<h4>Contents of this location:</h4> list={contents}
<ul> title="Objects in this location:" />
{attributes.contents.map((objectNum) => <li key={objectNum}>{objectNum}</li>)}
</ul> <AttrList
</section>} list={verbs}
{attributes.verbs && <section className="page-contents"> title="Verbs defined on this object:" />
<h4>Verbs defined on this object:</h4>
<ul> <AttrList
{attributes.verbs.map((verbNum) => <li key={verbNum}>{verbNum}</li>)} list={prepositions}
</ul> title="Prepositions this verb expects:" />
</section>}
{attributes.prepositions && <section className="page-contents"> <JsonDisplay obj={newAttributes} />
<h4>Prepositions this verb expects:</h4>
<ul>
{attributes.prepositions.map((prep) => <li key={prep}>{prep}</li>)}
</ul>
</section>}
</> </>
} }

View 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;

View File

@ -1,10 +1,14 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { apiUrl, fetchPage, fetchPageAttributes, postPage } from '../apiTools.jsx'; import { apiUrl, fetchPage, fetchPageAttributes, postPage } from '../apiTools.jsx';
import { useLoggedIn } from '../AuthProvider.jsx'; import { useLoggedIn } from '../AuthProvider.jsx';
import Page from './Page.jsx'; import Page from './Page.jsx';
import ExtendedAttributes from './ExtendedAttributes.jsx'; import ExtendedAttributes from './ExtendedAttributes.jsx';
//import ExtendedAttributesEdit from './ExtendedAttributesEdit.jsx';
import { JsonEditor } from 'json-edit-react';
import './Pages.css'; import './Pages.css';
@ -14,68 +18,74 @@ function GhostPage({editing, ...props}) {
const loggedIn = useLoggedIn(); const loggedIn = useLoggedIn();
const queryClient = useQueryClient(); 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({ const fetchPageQuery = useQuery({
queryKey: ['page', pagenumber, null], queryKey: ['page', pagenumber, null],
queryFn: () => fetchPage(pagenumber), 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({ const fetchAttributesQuery = useQuery({
queryKey: ['attributes', pagenumber], queryKey: ['attributes', pagenumber],
queryFn: () => fetchPageAttributes(pagenumber) queryFn: () => fetchPageAttributes(pagenumber)
}); });
const [title, setTitle] = useState(null); const [attributes, setAttributes] = useState(null);
useEffect(() => { useEffect(() => setAttributes(fetchAttributesQuery.data), [fetchAttributesQuery.data]);
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 postMutation = useMutation({ // for changing the value when we're done with it const postMutation = useMutation({ // for changing the value when we're done with it
mutationFn: postPage, mutationFn: postPage,
onSettled: async (data, error, variables) => { onSettled: async (data, error, variables) => {
// Invalidate and refetch // Invalidate and refetch
await queryClient.invalidateQueries(['page', variables.number, null]) await queryClient.invalidateQueries(['page', variables.number, null]);
console.log("shoulda just invalidated the thing");
navigate(`/${pagenumber}`); navigate(`/${pagenumber}`);
}, },
}); });
function submitChanges(e) { function submitChanges(e) {
postMutation.mutate({ if (editorRef.current) {
number: pagenumber, postMutation.mutate({
title: title, number: pagenumber,
description: text, title: title,
type: type ? 1 : 0, 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"> return <div className="main-column">
<Page <Page
page={{...fetchPageQuery.data, description: text, title: title, type: type }} page={{...fetchPageQuery.data, title: title, type: type }}
editing={editing} editing={editing}
linkClick={linkClick} linkClick={navigateLinkClick}
onChangeTitle={(e) => setTitle(e.target.value)} onChangeTitle={(e) => setTitle(e.target.value)}
onChangeText={setText}
onChangeType={() => setType(!type)} onChangeType={() => setType(!type)}
ref={editorRef}
{...props}/> {...props}/>
<ExtendedAttributes { !editing ?
attributes={fetchAttributesQuery.data} /> <ExtendedAttributes
attributes={attributes} />
:
<ExtendedAttributesEdit
attributes={attributes}
setAttributes={setAttributes} />
}
<button <button
onClick={() => navigate(`/${pagenumber}/history`)}> onClick={() => navigate(`/${pagenumber}/history`)}>
History History
@ -86,10 +96,15 @@ function GhostPage({editing, ...props}) {
onClick={submitChanges}> onClick={submitChanges}>
{postMutation.isPending ? "Updating..." : "Update"} {postMutation.isPending ? "Updating..." : "Update"}
</button>)} </button>)}
{editing && (
<button
onClick={() => navigate(`/${pagenumber}`)}>
Return without saving
</button>)}
{!editing && !editid && ( {!editing && !editid && (
<button <button
disabled={!loggedIn} disabled={!loggedIn}
onClick={() => navigate(`/${pagenumber}/edit`, {replace: true})}> onClick={() => navigate(`/${pagenumber}/edit`)}>
Edit Page Edit Page
</button>)} </button>)}
</div>; </div>;

View File

@ -1,8 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, forwardRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiUrl, fetchPage, fetchPageAtEdit, postPage } from '../apiTools.jsx'; import { apiUrl, fetchPage, fetchPageAtEdit, postPage } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx'; import { useFixLinks } from '../clientStuff.jsx';
import { Switch } from '@mui/material';
import { MDXEditor, import { MDXEditor,
headingsPlugin, quotePlugin, listsPlugin, headingsPlugin, quotePlugin, listsPlugin,
thematicBreakPlugin, linkPlugin, diffSourcePlugin } from '@mdxeditor/editor'; thematicBreakPlugin, linkPlugin, diffSourcePlugin } from '@mdxeditor/editor';
@ -11,27 +13,27 @@ import './Pages.css';
import 'highlight.js/styles/a11y-dark.min.css'; import 'highlight.js/styles/a11y-dark.min.css';
import '@mdxeditor/editor/style.css'; import '@mdxeditor/editor/style.css';
function Page({ const Page = forwardRef(({
page=null, page=null,
editing, historical, editing, historical,
linkClick=()=>{}, linkClick=()=>{},
onChangeTitle=()=>{}, onChangeTitle=()=>{},
onChangeType=()=>{}, onChangeType=()=>{},
onChangeText=()=>{}, }, ref) => {
}) {
let {title, description, html, lua, time, author, type} = page || {}; 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 (<> return (<>
<header> <header>
<h1> <h1>
<a href="/" onClick={linkClick}>🌳</a> <a href="/" onClick={linkClick}>🌳</a>
{page?.number}.&nbsp; {page?.number}.&nbsp;
{ editing ? { editing ?
<input disabled={!editing} value={title} onChange={onChangeTitle}/> <input disabled={!editing} value={title || ""} onChange={onChangeTitle}/>
: :
<span>{title}</span> <span>{title}</span>
} }
@ -39,8 +41,11 @@ function Page({
{ historical && <p>saved at {time} by user #{author}</p> } { historical && <p>saved at {time} by user #{author}</p> }
{ editing ? { editing ?
<label> <label>
<input type="checkbox" checked={type} onChange={onChangeType}/> noun
{type ? "verb" : "noun"} <Switch
checked={type}
onChange={onChangeType}/>
verb
</label> </label>
: :
<br/> <br/>
@ -50,7 +55,7 @@ function Page({
<section className="page-contents"> <section className="page-contents">
{ editing ? { editing ?
<MDXEditor <MDXEditor
markdown={description} markdown="nothin here"
plugins={[ plugins={[
headingsPlugin(), headingsPlugin(),
quotePlugin(), quotePlugin(),
@ -59,7 +64,7 @@ function Page({
linkPlugin(), linkPlugin(),
diffSourcePlugin({ diffMarkdown: 'ahhhh do not look upon me!', viewMode: 'source' }), diffSourcePlugin({ diffMarkdown: 'ahhhh do not look upon me!', viewMode: 'source' }),
]} ]}
onChange={onChangeText} ref={ref}
/> />
: :
( type ? ( type ?
@ -73,6 +78,6 @@ function Page({
</section> </section>
</> </>
); );
} });
export default Page; export default Page;

View 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;

View 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;

View 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;