From 8b8dc32d4d74fdd02bdadd83f536afb9044c83cd Mon Sep 17 00:00:00 2001 From: Stefan Werner Date: Sat, 11 Nov 2023 12:36:38 +0100 Subject: [PATCH] improve a bit more with design and usablility --- web/src/App.js | 6 +- web/src/lib/convertToAntdTreeData.js | 97 +++++++++++++++++++ web/src/lib/useMuuriGrid.js | 2 +- web/src/ui/ControlContainer.jsx | 91 ++++++++++++++++++ web/src/ui/ShareModal.jsx | 2 +- web/src/ui/{ => editor}/BibtexViewer.js | 0 web/src/ui/editor/Controls.jsx | 39 ++++---- web/src/ui/editor/Editor.jsx | 1 - web/src/ui/editor/ExperimentsView.jsx | 3 +- web/src/ui/{ => editor}/Puzzle.jsx | 11 ++- web/src/ui/editor/PuzzleView.jsx | 2 +- web/src/ui/editor/TreeView.jsx | 4 +- web/src/ui/editor/TriangleView.jsx | 2 +- web/src/ui/{ => viewer}/Fractal.jsx | 8 +- web/src/ui/{ => viewer}/MobileControls.jsx | 2 +- web/src/ui/{ => viewer}/MuuriComponent.js | 2 +- web/src/ui/{ => viewer}/Pin.jsx | 2 +- web/src/ui/{ => viewer}/Tooltips.jsx | 103 ++------------------- web/src/ui/{ => viewer}/Triangle.jsx | 21 +++-- 19 files changed, 249 insertions(+), 149 deletions(-) create mode 100644 web/src/lib/convertToAntdTreeData.js create mode 100644 web/src/ui/ControlContainer.jsx rename web/src/ui/{ => editor}/BibtexViewer.js (100%) rename web/src/ui/{ => editor}/Puzzle.jsx (96%) rename web/src/ui/{ => viewer}/Fractal.jsx (97%) rename web/src/ui/{ => viewer}/MobileControls.jsx (98%) rename web/src/ui/{ => viewer}/MuuriComponent.js (96%) rename web/src/ui/{ => viewer}/Pin.jsx (98%) rename web/src/ui/{ => viewer}/Tooltips.jsx (50%) rename web/src/ui/{ => viewer}/Triangle.jsx (92%) diff --git a/web/src/App.js b/web/src/App.js index 1cb4014..f3370b0 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -1,9 +1,9 @@ -import React, { useState } from 'react' +import React from 'react' import { QueryClient, QueryClientProvider } from 'react-query' import './App.css' -import Fractal from './ui/Fractal' -import { Puzzle } from './ui/Puzzle' +import Fractal from './ui/viewer/Fractal' +import { Puzzle } from './ui/editor/Puzzle' import { Editor } from './ui/editor/Editor' import { useRoutes } from 'raviger' diff --git a/web/src/lib/convertToAntdTreeData.js b/web/src/lib/convertToAntdTreeData.js new file mode 100644 index 0000000..1e4b0c9 --- /dev/null +++ b/web/src/lib/convertToAntdTreeData.js @@ -0,0 +1,97 @@ +import ReactMarkdown from 'react-markdown' +import React from 'react' +import { postProcessTitle } from './position' + +export const generateExpandedKeys = (path) => { + const segments = path.split('-') + const keys = [] + + for (let i = 1; i <= segments.length; i++) { + keys.push(segments.slice(0, i).join('-')) + } + + return keys +} +const customSort = (a, b) => { + if (a === '.text') return -1 + if (b === '.text') return 1 + if (a === '_text') return 1 + if (b === '_text') return -1 + return Number(a) - Number(b) +} +export const convertToAntdTreeData = (node, prefix = '') => { + const result = [] + + if (!node) return [] + const sortedKeys = Object.keys(node).sort(customSort) + for (let key of sortedKeys) { + if (key === '.' || key === '_') continue + const childNode = node[key] + const currentKey = prefix ? `${prefix}-${key}` : key + const isObject = typeof childNode === 'object' + + let title = key + if (key.includes('text')) { + title = ( +
+ {' '} + {postProcessTitle(node[key])} +
+ ) + } + if (isObject) { + title = postProcessTitle(childNode?.['.']) ?? '' + } + + let treeNode = { + title: title, + key: currentKey, + icon: isObject ? key : key === 'text' ? '◬' : '▰', + } + + // Check if the node has children (ignoring special keys like "." and "_") + if ( + typeof childNode === 'object' && + key !== '.' && + key !== '_' && + !key.includes('text') + ) { + treeNode.children = convertToAntdTreeData(childNode, currentKey) + } + + result.push(treeNode) + } + + return result +} + +export const nestTexts = (path, texts) => { + if (!texts) return {} + if (!path) return texts + + const keys = path.split('/').filter(Boolean) // Split by '/' and filter out empty strings + let currentObject = {} + + keys.reduce((obj, key, index) => { + if (index === keys.length - 1) { + // If we are at the last key in the path + obj[key] = Object.fromEntries( + Object.entries(texts).map(([key, value]) => [ + ['.', '_'].includes(key) ? key + 'text' : key, + ['.', '_'].includes(key) ? value : { text: value }, + ]), + ) + } else { + obj[key] = {} // Else create an empty object for the next level + } + return obj[key] // Return the nested object for the next iteration + }, currentObject) + + return currentObject +} diff --git a/web/src/lib/useMuuriGrid.js b/web/src/lib/useMuuriGrid.js index c876361..0ffc2b0 100644 --- a/web/src/lib/useMuuriGrid.js +++ b/web/src/lib/useMuuriGrid.js @@ -1,6 +1,6 @@ import { useEffect } from 'react' import Muuri from 'muuri' -import { idToSize, idToZIndex, positionToId } from '../ui/Puzzle' +import { idToSize, idToZIndex, positionToId } from '../ui/editor/Puzzle' const useMuuriGrid = (gridRef, options, size, props) => { useEffect(() => { diff --git a/web/src/ui/ControlContainer.jsx b/web/src/ui/ControlContainer.jsx new file mode 100644 index 0000000..5af2c22 --- /dev/null +++ b/web/src/ui/ControlContainer.jsx @@ -0,0 +1,91 @@ +import React, { useEffect, useRef } from 'react' +import Muuri from 'muuri' +import '../../muuri.css' + +const areas = { + leftTriangle: { + vertices: [ + { x: 0, y: window.innerHeight }, // Bottom left + { x: 0, y: 0 }, // Top left + { x: window.innerWidth / 2, y: 0 }, // Middle top + ], + }, + rightTriangle: { + vertices: [ + { x: window.innerWidth, y: window.innerHeight }, // right left + { x: window.innerWidth, y: 0 }, // Top right + { x: window.innerWidth / 2, y: 0 }, // Middle top + ], + }, +} + +function calculatePositionInArea(item, area) { + if (area.vertices) { + // Assuming area is a triangle + const [v1, v2, v3] = area.vertices + + // Randomly choose a point inside the triangle + const r1 = Math.random() + const r2 = Math.random() + const sqrtR1 = Math.sqrt(r1) + + const x = + (1 - sqrtR1) * v1.x + sqrtR1 * (1 - r2) * v2.x + sqrtR1 * r2 * v3.x + const y = + (1 - sqrtR1) * v1.y + sqrtR1 * (1 - r2) * v2.y + sqrtR1 * r2 * v3.y + + return { x, y } + } +} + +function determineAreaForItem(item, areas, index, totalItems) { + // Example: Alternate between left and right triangles + if (index % 2 === 0) { + return areas.leftTriangle + } else { + return areas.rightTriangle + } +} + +function layout(grid) { + const items = grid.getItems() + + items.forEach((item) => { + // Decide in which area the item should be + const area = determineAreaForItem(item, areas) + + // Calculate position based on the area + const position = calculatePositionInArea(item, area) + + // Set the item's left and top CSS properties + item.getElement().style.left = `${position.x}px` + item.getElement().style.top = `${position.y}px` + }) + + // Update the grid after positioning items + grid.refreshItems().layout() +} + +const ControlContainer = ({ children }) => { + const gridRef = useRef(null) + + useEffect(() => { + if (!gridRef.current) return + const grid = new Muuri(gridRef.current, { + dragEnabled: true, + rounding: true, + width: 1000, + layout, + }) + + return () => grid.destroy() + }, [gridRef]) + + return ( +
+ {children} +
+ ) +} + +export { ControlContainer } diff --git a/web/src/ui/ShareModal.jsx b/web/src/ui/ShareModal.jsx index 7d04b20..27b2b6f 100644 --- a/web/src/ui/ShareModal.jsx +++ b/web/src/ui/ShareModal.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' import { Modal, Button, Input } from 'antd' import { ShareAltOutlined, CopyOutlined } from '@ant-design/icons' import { removeMultipleSlashes } from '../lib/nesting' diff --git a/web/src/ui/BibtexViewer.js b/web/src/ui/editor/BibtexViewer.js similarity index 100% rename from web/src/ui/BibtexViewer.js rename to web/src/ui/editor/BibtexViewer.js diff --git a/web/src/ui/editor/Controls.jsx b/web/src/ui/editor/Controls.jsx index 5cf6019..b60ce4e 100644 --- a/web/src/ui/editor/Controls.jsx +++ b/web/src/ui/editor/Controls.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { Button, Menu, Popconfirm, Slider, Space } from 'antd' +import { Button, Menu, Popconfirm, Slider } from 'antd' import { DeleteOutlined } from '@ant-design/icons' const ControlBar = { @@ -146,27 +146,30 @@ const UserInteractionMenu = ({ params, onDeleteAction }) => { position: 'fixed', left: 0, top: '10vh', - height: '30vh', - overflowY: "scroll" + height: '30vh', + overflowY: 'scroll', }} > - {(params?.actions ??[]).map((action, index) => [action, index]).reverse().map(([action, index]) => ( - + {(params?.actions ?? []) + .map((action, index) => [action, index]) + .reverse() + .map(([action, index]) => ( + {index} - onDeleteAction(index)} - okText="Yes" - cancelText="No" - > - - - {action.source}↦{JSON.stringify(action.target)} - - ))} + onDeleteAction(index)} + okText="Yes" + cancelText="No" + > + + + {action.source}↦{JSON.stringify(action.target)} + + ))} ) diff --git a/web/src/ui/editor/Editor.jsx b/web/src/ui/editor/Editor.jsx index 9edc220..5dc4535 100644 --- a/web/src/ui/editor/Editor.jsx +++ b/web/src/ui/editor/Editor.jsx @@ -11,7 +11,6 @@ import { ExperimentsView } from './ExperimentsView' import { PuzzleView } from './PuzzleView' import TextModal from './TextModal' import MetaModal from './MetaModal' -var debounce = require('lodash.debounce') let taskId = null const setTaskId = (tI) => (taskId = tI) diff --git a/web/src/ui/editor/ExperimentsView.jsx b/web/src/ui/editor/ExperimentsView.jsx index e31a7ae..a4aee78 100644 --- a/web/src/ui/editor/ExperimentsView.jsx +++ b/web/src/ui/editor/ExperimentsView.jsx @@ -1,10 +1,9 @@ import React from 'react' import { Button } from 'antd' import { navigate } from 'raviger' -import BibTeXViewer from '../BibtexViewer' +import BibTeXViewer from './BibtexViewer' export function ExperimentsView(props) { - //console.log(props) const [isGood, setIsGood] = React.useState({}) return ( <> diff --git a/web/src/ui/Puzzle.jsx b/web/src/ui/editor/Puzzle.jsx similarity index 96% rename from web/src/ui/Puzzle.jsx rename to web/src/ui/editor/Puzzle.jsx index f4b2f36..66fa45a 100644 --- a/web/src/ui/Puzzle.jsx +++ b/web/src/ui/editor/Puzzle.jsx @@ -1,10 +1,10 @@ import React, { useEffect, useRef, useState } from 'react' import Muuri from 'muuri' -import '../puzzle.css' -import { stringToColour } from '../lib/color' -import { postProcessTitle } from '../lib/position' -import useMuuriGrid from '../lib/useMuuriGrid' -import calculateFontSize from '../lib/FontSize' +import '../../puzzle.css' +import { stringToColour } from '../../lib/color' +import { postProcessTitle } from '../../lib/position' +import useMuuriGrid from '../../lib/useMuuriGrid' +import calculateFontSize from '../../lib/FontSize' // Define the base length for the largest triangle const BASE_LENGTH = window.innerHeight * 0.5 // Modify as needed @@ -168,6 +168,7 @@ const MutableTriangle = ({ overflowWrap: 'break-word', width: size, transform: 'translateX(-25%)', + zIndex: 1000000000 - 1000 * nest, }} title={title} // Full title as a tooltip onMouseDown={(e) => { diff --git a/web/src/ui/editor/PuzzleView.jsx b/web/src/ui/editor/PuzzleView.jsx index 343e275..4734091 100644 --- a/web/src/ui/editor/PuzzleView.jsx +++ b/web/src/ui/editor/PuzzleView.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { Puzzle } from '../Puzzle' +import { Puzzle } from './Puzzle' import PuzzleControls from './Controls' export function PuzzleView(props) { diff --git a/web/src/ui/editor/TreeView.jsx b/web/src/ui/editor/TreeView.jsx index b06b8ca..0debebc 100644 --- a/web/src/ui/editor/TreeView.jsx +++ b/web/src/ui/editor/TreeView.jsx @@ -1,11 +1,10 @@ import React, { useState } from 'react' import { Tree } from 'antd' -import { convertToAntdTreeData } from '../Tooltips' +import { convertToAntdTreeData } from '../../lib/convertToAntdTreeData' export function TreeView(props) { const treeData = convertToAntdTreeData(props.state) const [expandedKeys, setExpandedKeys] = useState([]) - const [openedKeys, setOpenedKeys] = useState(null) const onExpand = (expandedKeysValue) => { console.log('onExpand', expandedKeysValue) setExpandedKeys(expandedKeysValue) @@ -21,7 +20,6 @@ export function TreeView(props) { titleHeight={'10px'} loadData={async (node) => { console.log(node) - setOpenedKeys(node.key.replace(/-/g, '/').slice(0, -2)) }} /> ) diff --git a/web/src/ui/editor/TriangleView.jsx b/web/src/ui/editor/TriangleView.jsx index ce56695..4917b3b 100644 --- a/web/src/ui/editor/TriangleView.jsx +++ b/web/src/ui/editor/TriangleView.jsx @@ -1,5 +1,5 @@ import React from 'react' -import Fractal from '../Fractal' +import Fractal from '../viewer/Fractal' export function TriangleView(props) { return diff --git a/web/src/ui/Fractal.jsx b/web/src/ui/viewer/Fractal.jsx similarity index 97% rename from web/src/ui/Fractal.jsx rename to web/src/ui/viewer/Fractal.jsx index 1e496ba..1926e4f 100644 --- a/web/src/ui/Fractal.jsx +++ b/web/src/ui/viewer/Fractal.jsx @@ -1,14 +1,14 @@ import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react' import { useQuery } from 'react-query' import Triangle from './Triangle' -import { mergeDeep, lookupDeep, shiftIn, slashIt } from '../lib/nesting' -import { parseHash } from '../lib/read_link_params' +import { mergeDeep, lookupDeep, shiftIn, slashIt } from '../../lib/nesting' +import { parseHash } from '../../lib/read_link_params' import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch' -import { go, beamDataTo } from '../lib/navigate' +import { go, beamDataTo } from '../../lib/navigate' import { Tooltips } from './Tooltips' -import { MAX_LEVEL } from '../config/const' +import { MAX_LEVEL } from '../../config/const' import { MobileControls } from './MobileControls' import { MuuriComponent } from './MuuriComponent' diff --git a/web/src/ui/MobileControls.jsx b/web/src/ui/viewer/MobileControls.jsx similarity index 98% rename from web/src/ui/MobileControls.jsx rename to web/src/ui/viewer/MobileControls.jsx index 4a25faa..373e8ec 100644 --- a/web/src/ui/MobileControls.jsx +++ b/web/src/ui/viewer/MobileControls.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import ShareModal from './ShareModal' +import ShareModal from '../ShareModal' import { Button, Input } from 'antd' import { SearchOutlined } from '@ant-design/icons' import { useNavigate } from 'raviger' diff --git a/web/src/ui/MuuriComponent.js b/web/src/ui/viewer/MuuriComponent.js similarity index 96% rename from web/src/ui/MuuriComponent.js rename to web/src/ui/viewer/MuuriComponent.js index ad916f9..031bc9b 100644 --- a/web/src/ui/MuuriComponent.js +++ b/web/src/ui/viewer/MuuriComponent.js @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react' import Muuri from 'muuri' -import '../muuri.css' +import '../../muuri.css' import { Pin } from './Pin' const MuuriComponent = ({ labels, setHiddenId }) => { diff --git a/web/src/ui/Pin.jsx b/web/src/ui/viewer/Pin.jsx similarity index 98% rename from web/src/ui/Pin.jsx rename to web/src/ui/viewer/Pin.jsx index 2a54a2e..afcb6eb 100644 --- a/web/src/ui/Pin.jsx +++ b/web/src/ui/viewer/Pin.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import LeaderLine from 'leader-line' -import useLinkedElementsStore from '../lib/PinnedElements' +import useLinkedElementsStore from '../../lib/PinnedElements' function findElementById(id) { let currentId = id diff --git a/web/src/ui/Tooltips.jsx b/web/src/ui/viewer/Tooltips.jsx similarity index 50% rename from web/src/ui/Tooltips.jsx rename to web/src/ui/viewer/Tooltips.jsx index 3e7fdc6..103b692 100644 --- a/web/src/ui/Tooltips.jsx +++ b/web/src/ui/viewer/Tooltips.jsx @@ -1,103 +1,12 @@ -import { postProcessTitle } from '../lib/position' import React, { useEffect, useMemo, useState } from 'react' import { useQuery } from 'react-query' import { Tree } from 'antd' -import { mergeDeep } from '../lib/nesting' -import ReactMarkdown from 'react-markdown' - -const generateExpandedKeys = (path) => { - const segments = path.split('-') - const keys = [] - - for (let i = 1; i <= segments.length; i++) { - keys.push(segments.slice(0, i).join('-')) - } - - return keys -} -const customSort = (a, b) => { - if (a === '.text') return -1 - if (b === '.text') return 1 - if (a === '_text') return 1 - if (b === '_text') return -1 - return Number(a) - Number(b) -} -export const convertToAntdTreeData = (node, prefix = '') => { - const result = [] - - if (!node) return [] - const sortedKeys = Object.keys(node).sort(customSort) - for (let key of sortedKeys) { - if (key === '.' || key === '_') continue - const childNode = node[key] - const currentKey = prefix ? `${prefix}-${key}` : key - const isObject = typeof childNode === 'object' - - let title = key - if (key.includes('text')) { - title = ( -
- {' '} - {postProcessTitle(node[key])} -
- ) - } - if (isObject) { - title = postProcessTitle(childNode?.['.']) ?? '' - } - - let treeNode = { - title: title, - key: currentKey, - icon: isObject ? key : key === 'text' ? '◬' : '▰', - } - - // Check if the node has children (ignoring special keys like "." and "_") - if ( - typeof childNode === 'object' && - key !== '.' && - key !== '_' && - !key.includes('text') - ) { - treeNode.children = convertToAntdTreeData(childNode, currentKey) - } - - result.push(treeNode) - } - - return result -} - -const nestTexts = (path, texts) => { - if (!texts) return {} - if (!path) return texts - - const keys = path.split('/').filter(Boolean) // Split by '/' and filter out empty strings - let currentObject = {} - - keys.reduce((obj, key, index) => { - if (index === keys.length - 1) { - // If we are at the last key in the path - obj[key] = Object.fromEntries( - Object.entries(texts).map(([key, value]) => [ - ['.', '_'].includes(key) ? key + 'text' : key, - ['.', '_'].includes(key) ? value : { text: value }, - ]), - ) - } else { - obj[key] = {} // Else create an empty object for the next level - } - return obj[key] // Return the nested object for the next iteration - }, currentObject) - - return currentObject -} +import { mergeDeep } from '../../lib/nesting' +import { + convertToAntdTreeData, + generateExpandedKeys, + nestTexts, +} from '../../lib/convertToAntdTreeData' export const Tooltips = ({ tree: _tree, diff --git a/web/src/ui/Triangle.jsx b/web/src/ui/viewer/Triangle.jsx similarity index 92% rename from web/src/ui/Triangle.jsx rename to web/src/ui/viewer/Triangle.jsx index 0dcadb7..5069bcb 100644 --- a/web/src/ui/Triangle.jsx +++ b/web/src/ui/viewer/Triangle.jsx @@ -4,12 +4,16 @@ import { getTopPosition, isElementInViewportAndBigAndNoChildren, postProcessTitle, -} from '../lib/position' -import { addHoverObject, hoverObjects, removeHoverObject } from '../lib/hover' -import { MAX_LEVEL } from '../config/const' -import { stringToColour } from '../lib/color' -import { trim } from '../lib/string' -import calculateFontSize from '../lib/FontSize' +} from '../../lib/position' +import { + addHoverObject, + hoverObjects, + removeHoverObject, +} from '../../lib/hover' +import { MAX_LEVEL } from '../../config/const' +import { stringToColour } from '../../lib/color' +import { trim } from '../../lib/string' +import calculateFontSize from '../../lib/FontSize' function getRandomElement(arr) { const randomIndex = Math.floor(Math.random() * arr.length) @@ -69,8 +73,6 @@ function Triangle({ }, [transformState, scale, fullId]) if (!data) return null - //const { linkedElements, linkedElementsHas } = useLinkedElementsStore() - const hover = hoverObjects.has(fullId) if (hover) { setHoverId(fullId) @@ -78,7 +80,7 @@ function Triangle({ const title = data?.['.'] const anto = data?.['_'] - const { shortTitle, fontSize } = calculateFontSize(size, title) + const { shortTitle, fontSize } = calculateFontSize(size, title, 0.7) return (
{word}