fixing up live display and mobile, making custom swipe drawer

This commit is contained in:
Shoofle 2024-12-03 10:31:50 -05:00
parent 9575d333ad
commit 90d8ec5da9
25 changed files with 2120 additions and 528 deletions

1661
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,8 @@
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview" "preview": "vite preview",
"test": "vitest"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -53,5 +54,9 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"jsdom": "^25.0.1",
"vitest": "^2.1.5"
} }
} }

View File

@ -21,7 +21,6 @@ a:visited {
max-width: 76ch; max-width: 76ch;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-top: 3rem;
color: white; color: white;
} }
header { header {

View File

@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -0,0 +1,36 @@
import { useRef, useEffect } from 'react';
function Shim({height, width, duplicateOf}) {
const shimDiv = useRef(null);
let target = duplicateOf.current;
let shim = shimDiv.current;
useEffect(()=>{
if (target && shim) {
const rect = target.getBoundingClientRect();
const compStyle= window.getComputedStyle(target)
const margins = {
top: compStyle.getPropertyValue('margin-top'),
bottom: compStyle.getPropertyValue('margin-bottom'),
left: compStyle.getPropertyValue('margin-left'),
right: compStyle.getPropertyValue('margin-right'),
}
if (width) {
shim.style.width = `calc(${margins.left} + ${margins.right} + ${target.offsetWidth}px)`;
}
if (height) {
shim.style.height = `calc(${margins.top} + ${margins.bottom} + ${target.offsetHeight}px)`;
}
console.log('set shimdiv style', rect, margins);
}
}, [ target, shim,
width, height,
target && target.offsetWidth,
target && target.offsetHeight ]
);
return <div ref={shimDiv}><h4>I'M A SHIM</h4></div>
}
export default Shim;

View File

@ -0,0 +1,43 @@
.swipeTargetStyle {
width: 30vw;
height: 100vh;
position: fixed;
background: transparent;
}
.swipeTargetStyle-left {
top: 0;
left: 0;
}
.swipeTargetStyle-right {
top: 0;
right: 0;
}
.drawerStyle {
transition: all 0.3s ease-in-out;
width: 30vw;
height: 100vh;
position: fixed;
background: lightgreen;
}
.drawerStyle-left {
top: 0;
left: 0;
}
.drawerStyle-left-closed {
transform: translateX(-30vw);
background: green;
}
.drawerStyle-right {
top: 0;
right: 0;
}
.drawerStyle-right-closed {
transform: translateX(30vw);
background: green;
}

View File

@ -0,0 +1,115 @@
import { useState, useRef } from 'react';
import { Button, IconButton } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import CloseIcon from '@mui/icons-material/Close';
import './SwipeableDrawer.css';
function SwipeableDrawer({children, anchor="right", hidden=true, className, ...props}) {
const [ open, setOpen ] = useState(!hidden);
const [ distance, setDistance ] = useState(0);
const [ origin, setOrigin ] = useState(0);
const [ inTouch, setInTouch ] = useState(false);
const swipeTargetRef = useRef(null);
function onTouchStart(e) {
setInTouch(true);
setOrigin(e.targetTouches[0].clientX);
}
function onTouchMove(e) {
setDistance(e.targetTouches[0].clientX - origin);
}
function onTouchEnd(e) {
let threshhold = 100;
if (swipeTargetRef.current) {
threshhold = swipeTargetRef.current.getBoundingClientRect().width * 0.5;
}
if (anchor == "right") {
if (distance < -threshhold) {
setOpen(true);
}
if (distance > threshhold) {
setOpen(false);
}
}
if (anchor == "left") {
if (distance > threshhold) {
setOpen(true);
}
if (distance < -threshhold) {
setOpen(false);
}
}
setOrigin(0);
setDistance(0);
setInTouch(false);
}
const dragStyle = {};
if (inTouch) {
dragStyle.transition = "none";
if (open) {
dragStyle.transform = `translateX(${distance}px)`;
} else {
if (anchor == "right") dragStyle.transform = `translateX(calc(30vw + ${distance}px))`;
if (anchor == "left") dragStyle.transform = `translateX(calc(-30vw + ${distance}px))`;
}
}
const drawerClosedClass = open ? "" : `drawerStyle-${anchor}-closed`;
return (<>
<div
ref={swipeTargetRef}
className={`swipeTargetStyle swipeTargetStyle-${anchor}`}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}>
<IconButton
size="large"
variant="contained"
style={{
visible: open,
padding: "2rem",
position: "absolute",
right: anchor == "right" ? 0 : "auto",
left: anchor == "left" ? 0 : "auto",
top: 0
}}
onClick={() => setOpen(true)} >
<MenuIcon />
</IconButton>
</div>
<div
className={`drawerStyle drawerStyle-${anchor} ${drawerClosedClass} ${className}`}
style={dragStyle}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}>
<IconButton
size="large"
variant="contained"
style={{
visible: open,
padding: "2rem",
position: "absolute",
right: anchor == "left" ? 0 : "auto",
left: anchor == "right" ? 0 : "auto",
top: 0
}}
onClick={() => setOpen(false)} >
<CloseIcon />
</IconButton>
{children}
</div>
</>);
}
export default SwipeableDrawer;

View File

@ -0,0 +1,15 @@
import { describe, expect, test, assert } from 'vitest';
import { render, screen } from '@testing-library/react';
import SwipeableDrawer from './SwipeableDrawer.jsx';
//TODO: i was going to write tests for things but then i felt silly writing a single test file
// so i guess i'm not going too write tests at the moment
// sorry for my sin against good software development practices
describe('swipeable drawer tests', () => {
test("test that it renders its children", () => {
render(<SwipeableDrawer><h4>Content</h4></SwipeableDrawer>);
expect(screen.getByText(/Content/i)).toBeDefined()
})
})

View File

@ -6,25 +6,27 @@
padding: 0; padding: 0;
background: lightgreen; background: lightgreen;
position: fixed; position: fixed;
bottom: 0; bottom: 1rem;
left: 20%; left: 20%;
right: 20%; right: 20%;
border-radius: 1rem; border-radius: 1rem;
border: 1px solid gray; border: 1px solid gray;
}
.commandline form {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; align-items: center;
} }
.commandline input { .commandline input {
flex: 10; flex: 1;
font-size: 16pt; font-size: 16pt;
border: none; border: none;
padding: 2rem; padding: 2rem;
border-radius: 1rem; border-radius: 1rem;
background: transparent; background: transparent;
width: 100%;
} }
.commandline button { .commandline button {
border-radius: 4rem;
width: 4rem;
} }

View File

@ -1,21 +1,21 @@
import { forwardRef } from 'react'; import { forwardRef } from 'react';
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) => {
return ( return (
<div className="commandline" > <div className="commandline" ref={ref} >
<input <input
ref={ref} id="command_input"
onChange={(e) => setCommand(e.target.value)} onChange={(e) => setCommand(e.target.value)}
autoFocus autoFocus
type="text" type="text"
placeholder="Enter a command!" placeholder="Enter a command!"
value={command} /> value={command} />
<Fab onClick={ () => { console.log("clicked"); onSubmitCommand(command); }} > <button
onClick={ () => onSubmitCommand(command) } >
Send Send
</Fab> </button>
</div> </div>
); );
}); });

View File

@ -11,6 +11,7 @@ import { JsonEditor } from 'json-edit-react';
import MessageFeed from './MessageFeed.jsx'; import MessageFeed from './MessageFeed.jsx';
import WordSidebar from './WordSidebar.jsx'; import WordSidebar from './WordSidebar.jsx';
import CommandEntry from './CommandEntry.jsx'; import CommandEntry from './CommandEntry.jsx';
import Shim from '../components/Shim.jsx';
function Live({...props}) { function Live({...props}) {
const navigate = useNavigate(); const navigate = useNavigate();
@ -22,7 +23,6 @@ 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!
@ -61,6 +61,8 @@ function Live({...props}) {
setType(fetchPageQuery.data?.type == 1); setType(fetchPageQuery.data?.type == 1);
}, [fetchPageQuery.data?.type]); }, [fetchPageQuery.data?.type]);
const editorRef = useRef(null);
// extended attribute store! // extended attribute store!
const fetchAttributesQuery = useQuery({ const fetchAttributesQuery = useQuery({
queryKey: ['attributes', currentNumber], queryKey: ['attributes', currentNumber],
@ -72,7 +74,6 @@ function Live({...props}) {
setAttributes(fetchAttributesQuery.data); setAttributes(fetchAttributesQuery.data);
}, [fetchAttributesQuery.data]); }, [fetchAttributesQuery.data]);
console.log("is live reloading wrking");
// verbs available to us // verbs available to us
const fetchVerbsQuery = useQuery({ const fetchVerbsQuery = useQuery({
queryKey: ['my verbs'], queryKey: ['my verbs'],
@ -169,23 +170,27 @@ function Live({...props}) {
<ExtendedAttributes <ExtendedAttributes
attributes={attributes} /> attributes={attributes} />
} }
<Shim height={true} width={false} duplicateOf={commandEntryRef}/>
</div> </div>
<MessageFeed messages={messageHistory}> <MessageFeed messages={messageHistory}>
<button disabled={connecting} onClick={() => setConnecting(true)}> <p>
{{ {{
[ReadyState.CONNECTING]: 'Connecting', [ReadyState.CONNECTING]: 'Connecting',
[ReadyState.OPEN]: 'Open', [ReadyState.OPEN]: 'Open',
[ReadyState.CLOSING]: 'Closing', [ReadyState.CLOSING]: 'Closing',
[ReadyState.CLOSED]: 'Closed', [ReadyState.CLOSED]: 'Closed',
[ReadyState.UNINSTANTIATED]: 'Uninstantiated', [ReadyState.UNINSTANTIATED]: 'Uninstantiated',
}[readyState]}</button> }[readyState]}
</p>
<button onClick={()=> setMessageHistory([])}>Clear History</button> <button onClick={()=> setMessageHistory([])}>Clear History</button>
</MessageFeed> </MessageFeed>
<WordSidebar verbs={verbs} sendWord={(word) => { <WordSidebar
setCommand((command + " " + word).trim()); verbs={verbs}
commandEntryRef.current.focus(); sendWord={(word) => {
// maybe set focus to the command entry? setCommand((command + " " + word).trim());
}}> // diong this outside of react so shim can... ugh
document.getElementById("command_input").focus();
}}>
</WordSidebar> </WordSidebar>
<CommandEntry <CommandEntry
ref={commandEntryRef} ref={commandEntryRef}

View File

@ -1,28 +1,19 @@
.messages-tray { .messages-tray {
display: none;/*flex;*/ display: flex;/*flex;*/
flex-direction: column; flex-direction: column;
transition: all 0.1s linear;
margin: 0;
background: lightgreen;
position: fixed;
width: 30ch;
height: 100%;
left: 0;
top: 0;
overflow: hidden; overflow: hidden;
} }
.messages-hidden {
transform: translateX(-20ch);
}
.messages-tray ol { .messages-tray ol {
flex: 10; flex: 10;
padding-left: 1ch; padding-left: 1ch;
overflow: auto; overflow: scroll;
} }
.messages-tray li { .messages-tray li {
list-style: none; list-style: none;
} }
.messages-tray li:nth-child(even) { .messages-tray li:nth-child(even) {
background: lightgray; background: lightgray;
} }

View File

@ -1,45 +1,21 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useForm } from "react-hook-form"; import SwipeableDrawer from '../components/SwipeableDrawer.jsx';
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';
function MessageFeed({messages=[], children}) { function MessageFeed({messages=[], hidden=true, children}) {
const [ open, setOpen ] = useState(false);
const listContainer = useRef(null); const listContainer = useRef(null);
useEffect(() => { useEffect(() => {
if (listContainer.current) if (listContainer.current)
listContainer.current.scrollTo(0, listContainer.current.scrollHeight); listContainer.current.scrollTo(0, listContainer.current.scrollHeight);
}, [listContainer, messages]) }, [listContainer.current, messages])
return (<> return (
<IconButton
size="large"
style={{
visible: open,
position: "fixed",
padding: "2rem",
left: 0,
top: 0
}}
onClick={() => setOpen(true)} >
<MenuIcon />
</IconButton>
<SwipeableDrawer <SwipeableDrawer
anchor="left" anchor="left"
open={open} hidden={hidden}
onClose={() => setOpen(false)} className="messages-tray">
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) =>
@ -50,7 +26,7 @@ function MessageFeed({messages=[], children}) {
</ol> </ol>
{children} {children}
</SwipeableDrawer> </SwipeableDrawer>
</>); );
} }
export default MessageFeed; export default MessageFeed;

View File

@ -1,25 +0,0 @@
.sidebar {
transition: all 0.1s linear;
margin: 0;
background: lightgreen;
position: fixed;
width: 30ch;
height: 100%;
right: 0;
top: 0;
}
.sidebar-hidden {
transform: translateX(20ch);
}
.sidebar li {
list-style: none;
}
.sidebar li button {
text-transform: uppercase;
}
.sidebar-hidden li {
display: none;
}

View File

@ -0,0 +1,17 @@
.sidebar {
}
.sidebar-hidden {
transform: translateX(20ch);
}
.sidebar ul {
padding-left: 1ch;
}
.sidebar li {
list-style: none;
}
.sidebar li button {
text-transform: uppercase;
}

View File

@ -1,52 +1,22 @@
import { useState } from 'react'; import SwipeableDrawer from '../components/SwipeableDrawer.jsx';
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 './WordSidebar.css';
import CloseIcon from '@mui/icons-material/Close';
import './Sidebar.css';
function WordSidebar({children, verbs, hidden=true, sendWord=(()=>null)}) { function WordSidebar({children, verbs, hidden=true, sendWord=(()=>null)}) {
const [open, setOpen] = useState(!hidden);
const loggedIn = useLoggedIn();
return (<> return (<>
<IconButton
size="large"
variant="contained"
style={{
visible: open,
position: "fixed",
padding: "2rem",
right: 0,
top: 0
}}
onClick={() => setOpen(true)} >
<MenuIcon />
</IconButton>
<SwipeableDrawer <SwipeableDrawer
hideBackdrop={true}
disableBackdropTransition={true}
anchor="right" anchor="right"
open={open} hidden={hidden}
onClose={() => setOpen(false)} className="sidebar">
onOpen={() => setOpen(true)}>
<IconButton
onClick={() => setOpen(false)} >
<CloseIcon />
</IconButton>
<ul> <ul>
{verbs.map( (name) => ( {verbs.map( (name) => (
<li key={name}> <li key={name}>
<Button onClick={() => { <button onClick={() => {
console.log("clicked button for", name); console.log("clicked button for", name);
sendWord(name); sendWord(name);
}}> }}>
{name} {name}
</Button> </button>
</li> </li>
) )} ) )}
{children} {children}

View File

@ -44,7 +44,7 @@ export default function GraphRender() {
else if (error) return `Error encountered: ${error}`; else if (error) return `Error encountered: ${error}`;
else return ( else return (
<SigmaContainer <SigmaContainer
allowInvalidContainer settings={{allowInvalidContainer: true}}
graph={data} graph={data}
style={{height: "400px", width: "100%", background: 'rgba(0,0,0,0)'}} style={{height: "400px", width: "100%", background: 'rgba(0,0,0,0)'}}
/> />

View File

@ -12,6 +12,7 @@
width: 100%; width: 100%;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
.landing-section { .landing-section {
margin: 1rem; margin: 1rem;
padding: 1rem; padding: 1rem;
@ -24,3 +25,12 @@ ol li {
list-style-type: circle; list-style-type: circle;
} }
@media (max-height: 700px) {
.landing-column {
width: 100%;
}
.landing-container {
display: block;
}
}

View File

@ -35,13 +35,7 @@ function Landing() {
<hr/> <hr/>
</header> </header>
<div className="landing-container"> <div className="landing-container">
<section className="landing-section"> <section className="landing-section" style={{gridColumn: "3", gridRow: "1"}}>
<GraphRender />
</section>
<section className="landing-section">
<PageList />
</section>
<section className="landing-section">
{ loggedIn { loggedIn
? ?
<> <>
@ -58,6 +52,12 @@ function Landing() {
</> </>
} }
</section> </section>
<section className="landing-section" style={{gridColumn: "1", gridRow: "1"}}>
{(typeof WebGL2RenderingContext != 'undefined') && <GraphRender />}
</section>
<section className="landing-section" style={{gridColumn: "2", gridRow: "1"}}>
<PageList />
</section>
</div> </div>
</div> </div>
); );

View File

@ -12,6 +12,7 @@ import './Pages.css';
function GhostPage({editing, ...props}) { function GhostPage({editing, ...props}) {
const { pagenumber, editid } = useParams(); const { pagenumber, editid } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetchPageQuery = useQuery({ const fetchPageQuery = useQuery({
@ -117,7 +118,6 @@ function GhostPage({editing, ...props}) {
</button>)} </button>)}
{!editing && !editid && ( {!editing && !editid && (
<button <button
disabled={!loggedIn}
onClick={() => navigate(`/${pagenumber}/edit`)}> onClick={() => navigate(`/${pagenumber}/edit`)}>
Edit Page Edit Page
</button>)} </button>)}

View File

@ -5,13 +5,13 @@ function AttrList({list, title, onChange, children}) {
if (!list || !list.map || typeof list.map != 'function') if (!list || !list.map || typeof list.map != 'function')
return <section className="page-contents"> return <section className="page-contents">
<h4>{title}</h4> <h4>{title}</h4>
value is not a list: {list} value is not a list: {JSON.stringify(list)}
</section>; </section>;
return <section className="page-contents"> return <section className="page-contents">
<h4>{title}</h4> <h4>{title}</h4>
<ul> <ul>
{list.map((objectNum) => <li key={objectNum}>{objectNum}</li>)} {list.map((objectNum) => <li key={objectNum}>{JSON.stringify(objectNum)}</li>)}
</ul> </ul>
</section> </section>
} }

View File

@ -2,6 +2,7 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import viteTsconfigPaths from 'vite-tsconfig-paths' import viteTsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({ export default defineConfig({
// depending on your application, base can also be "/" // depending on your application, base can also be "/"
base: '/', base: '/',
@ -12,4 +13,9 @@ export default defineConfig({
// this sets a default port to 3000 // this sets a default port to 3000
port: 3000, port: 3000,
}, },
test: {
globals: true,
environment: 'jsdom',
// other options...
}
}) })

View File

@ -273,7 +273,7 @@ function setAttribute(obj, attributeName, value) {
const attributeStore = pullAttribute.get(verifyObjectReference(obj)); const attributeStore = pullAttribute.get(verifyObjectReference(obj));
const contents = JSON.parse(attributeStore.contents); const contents = JSON.parse(attributeStore.contents);
if (isEmptyObject(value) && isArray(contents[attributes])) if (isEmptyObject(value) && isArray(contents[attributeName]))
contents[attributeName] = []; contents[attributeName] = [];
else else
contents[attributeName] = value; contents[attributeName] = value;
@ -284,10 +284,12 @@ function setAttribute(obj, attributeName, value) {
} }
function isArray(obj) { function isArray(obj) {
return Object.prototype.toString.apply(value) === '[object Array]'; return Object.prototype.toString.apply(obj) === '[object Array]';
} }
function isEmptyObject(obj) { function isEmptyObject(obj) {
if (typeof obj !== "obj") return false; if (typeof obj !== "object") return false;
if (isArray(obj)) return false;
for (const prop in obj) { for (const prop in obj) {
if (Object.hasOwn(obj, prop)) return true; if (Object.hasOwn(obj, prop)) return true;

517
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node --watch --env-file=.env server.js", "start": "node --watch --env-file=.env server.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1",
"ver": "node --version"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -15,7 +16,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"argon2": "^0.41.1", "argon2": "^0.41.1",
"better-sqlite3": "^11.3.0", "better-sqlite3": "^11.5.0",
"better-sqlite3-session-store": "^0.1.0", "better-sqlite3-session-store": "^0.1.0",
"client-sessions": "^0.8.0", "client-sessions": "^0.8.0",
"express": "^4.21.0", "express": "^4.21.0",
@ -27,7 +28,7 @@
"graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-forceatlas2": "^0.10.1",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"node": "^22.10.0", "node": "^22.11.0",
"nodemon": "^3.1.5", "nodemon": "^3.1.5",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"wasmoon": "^1.16.0" "wasmoon": "^1.16.0"