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": {
"start": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest"
},
"eslintConfig": {
"extends": [
@ -53,5 +54,9 @@
"last 1 firefox 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;
margin-left: auto;
margin-right: auto;
padding-top: 3rem;
color: white;
}
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;
background: lightgreen;
position: fixed;
bottom: 0;
bottom: 1rem;
left: 20%;
right: 20%;
border-radius: 1rem;
border: 1px solid gray;
}
.commandline form {
display: flex;
flex-direction: row;
width: 100%;
align-items: center;
}
.commandline input {
flex: 10;
flex: 1;
font-size: 16pt;
border: none;
padding: 2rem;
border-radius: 1rem;
background: transparent;
width: 100%;
}
.commandline button {
border-radius: 4rem;
width: 4rem;
}

View File

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

View File

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

View File

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

View File

@ -1,45 +1,21 @@
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 { useEffect, useRef } from 'react';
import SwipeableDrawer from '../components/SwipeableDrawer.jsx';
import './MessageFeed.css';
function MessageFeed({messages=[], children}) {
const [ open, setOpen ] = useState(false);
function MessageFeed({messages=[], hidden=true, children}) {
const listContainer = useRef(null);
useEffect(() => {
if (listContainer.current)
listContainer.current.scrollTo(0, listContainer.current.scrollHeight);
}, [listContainer, messages])
}, [listContainer.current, messages])
return (<>
<IconButton
size="large"
style={{
visible: open,
position: "fixed",
padding: "2rem",
left: 0,
top: 0
}}
onClick={() => setOpen(true)} >
<MenuIcon />
</IconButton>
return (
<SwipeableDrawer
anchor="left"
open={open}
onClose={() => setOpen(false)}
onOpen={() => setOpen(true)}>
<IconButton
variant="text"
onClick={() => setOpen(false)} >
<CloseIcon />
</IconButton>
hidden={hidden}
className="messages-tray">
<h2>Message history:</h2>
<ol ref={listContainer}>
{messages.map((message, idx) =>
@ -50,7 +26,7 @@ function MessageFeed({messages=[], children}) {
</ol>
{children}
</SwipeableDrawer>
</>);
);
}
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 { useQuery } from '@tanstack/react-query';
import { useLoggedIn } from '../AuthProvider.jsx';
import { fetchCurrentVerbs } from '../apiTools.jsx';
import { Button, IconButton, SwipeableDrawer } from '@mui/material';
import SwipeableDrawer from '../components/SwipeableDrawer.jsx';
import MenuIcon from '@mui/icons-material/Menu';
import CloseIcon from '@mui/icons-material/Close';
import './Sidebar.css';
import './WordSidebar.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>
hidden={hidden}
className="sidebar">
<ul>
{verbs.map( (name) => (
<li key={name}>
<Button onClick={() => {
<button onClick={() => {
console.log("clicked button for", name);
sendWord(name);
}}>
{name}
</Button>
</button>
</li>
) )}
{children}

View File

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

View File

@ -12,6 +12,7 @@
width: 100%;
grid-template-columns: repeat(3, 1fr);
}
.landing-section {
margin: 1rem;
padding: 1rem;
@ -24,3 +25,12 @@ ol li {
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/>
</header>
<div className="landing-container">
<section className="landing-section">
<GraphRender />
</section>
<section className="landing-section">
<PageList />
</section>
<section className="landing-section">
<section className="landing-section" style={{gridColumn: "3", gridRow: "1"}}>
{ loggedIn
?
<>
@ -58,6 +52,12 @@ function Landing() {
</>
}
</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>
);

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import viteTsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
// depending on your application, base can also be "/"
base: '/',
@ -12,4 +13,9 @@ export default defineConfig({
// this sets a default port to 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 contents = JSON.parse(attributeStore.contents);
if (isEmptyObject(value) && isArray(contents[attributes]))
if (isEmptyObject(value) && isArray(contents[attributeName]))
contents[attributeName] = [];
else
contents[attributeName] = value;
@ -284,10 +284,12 @@ function setAttribute(obj, attributeName, value) {
}
function isArray(obj) {
return Object.prototype.toString.apply(value) === '[object Array]';
return Object.prototype.toString.apply(obj) === '[object Array]';
}
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) {
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",
"scripts": {
"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": {
"type": "git",
@ -15,7 +16,7 @@
"license": "ISC",
"dependencies": {
"argon2": "^0.41.1",
"better-sqlite3": "^11.3.0",
"better-sqlite3": "^11.5.0",
"better-sqlite3-session-store": "^0.1.0",
"client-sessions": "^0.8.0",
"express": "^4.21.0",
@ -27,7 +28,7 @@
"graphology-layout-forceatlas2": "^0.10.1",
"highlight.js": "^11.10.0",
"jsdom": "^25.0.0",
"node": "^22.10.0",
"node": "^22.11.0",
"nodemon": "^3.1.5",
"showdown": "^2.1.0",
"wasmoon": "^1.16.0"