Skip to content

Commit

Permalink
Make draggable and resizable + hooks + types
Browse files Browse the repository at this point in the history
  • Loading branch information
cpbotha committed Jun 3, 2023
1 parent 8668ffa commit b128175
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 51 deletions.
37 changes: 36 additions & 1 deletion fe/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion fe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"htmr": "^1.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-embed": "^3.6.0"
"react-draggable": "^4.4.5",
"react-embed": "^3.6.0",
"react-resizable": "^3.0.5"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^4.29.9",
Expand Down
2 changes: 1 addition & 1 deletion fe/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function getCanvases() {
return axios.get("/api/canvases").then((res) => res.data);
}

function App() {
function App(): JSX.Element {
// TODO: fix initial canvas selection later
const [canvasId, setCanvasId] = useState(1);
const [newOrgNodeId, setNewOrgNodeId] = useState<string | null>(null);
Expand Down
8 changes: 4 additions & 4 deletions fe/src/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";

import { useMutation, useQuery } from "@tanstack/react-query";

import axios from "axios";
import { INode, Node } from "./Node";
import { useEffect } from "react";

import { Node } from "./Node";
import { INode } from "./types";

function getNodes(canvasId: number): Promise<INode[]> {
return axios.get(`/api/canvases/${canvasId}/nodes`).then((res) => res.data);
Expand Down
32 changes: 25 additions & 7 deletions fe/src/Node.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@

.node {
/* all nodes need position absolute with 0,0 so we can transform them */
position: absolute;
top: 0px;
left: 0px;
border: 2px solid steelblue;
}
/* all nodes need position absolute with 0,0 so we can transform them */
position: absolute;
top: 0px;
left: 0px;
border: 2px solid steelblue;
}

.react-resizable {
/* position: relative; */
}

.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=");
background-position: bottom right;
padding: 0 3px 3px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: se-resize;
}
92 changes: 56 additions & 36 deletions fe/src/Node.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
// TODO: oembed, react-iframe, etc.
// react-embed is quite out of date :(

import Embed, { route } from "react-embed";

import { useMutation, useQuery } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";

import htmr from "htmr";
import "./Node.css";
import { useState } from "react";
import Draggable from "react-draggable";
import Embed from "react-embed";
import { ResizableBox } from "react-resizable";

export interface INode {
title: string;
contents?: string;
link?: string;
x: number;
y: number;
width?: number;
height?: number;
colour?: string | null;
canvas_id?: number | null;
id?: number | null;
created_at?: Date | null;
}
import "./Node.css";
import { INode } from "./types";

interface IOrgNodeDetails {
file: string;
Expand All @@ -33,7 +22,7 @@ function getOrgNodeDetails(orgId: string): Promise<IOrgNodeDetails> {
return axios.get(`/api/or-nodes/${orgId}`).then((res) => res.data);
}

function RenderOrgNode(props: { orgId: string }) {
function RenderOrgNode(props: { orgId: string }): JSX.Element {
const { orgId } = props;

const { data: orgNodeDetails } = useQuery({
Expand Down Expand Up @@ -78,28 +67,59 @@ function RenderNodeNonEmbed(props: { node: INode }) {
export function Node(props: { node: INode }) {
const { node } = props;

const [width, setWidth] = useState(node.width);
const [height, setHeight] = useState(node.height);

// Embed only does the sites and filetypes it explicitly supports
// for other websites, we should try a fallback
//node.link = "https://soundcloud.com/kink/mechtaya";

// manual transform
// style.transform: `translate(${node.x}px, ${node.y}px)`,
// with react-draggable, CSS transform of child is updated
return (
<div
className="node"
style={{
width: `${node.width}px`,
height: `${node.height}px`,
transform: `translate(${node.x}px, ${node.y}px)`,
}}
<Draggable
defaultClassName="node"
defaultPosition={{ x: node.x, y: node.y }}
handle=".handle"
cancel={".react-resizable-handle"}
>
{node.title}
{node.link && (
<Embed
url={node.link}
renderVoid={(props, state, error) => (
<RenderNodeNonEmbed node={node} />
)}
/>
)}
</div>
<ResizableBox
width={width}
height={height}
onResize={(event, { node, size, handle }) => {
setWidth(size.width);
setHeight(size.height);
}}
>
<div
className="node-NOT"
style={{
width: `${width}px`,
height: `${height}px`,
}}
>
<div className="handle" style={{ background: "lightgrey" }}>
{node.title}
</div>
<div
style={{
overflow: "auto",
height: "calc(100% - 2em)",
width: "100%",
}}
>
{node.link && (
<Embed
url={node.link}
renderVoid={(props, state, error) => (
<RenderNodeNonEmbed node={node} />
)}
/>
)}
</div>
</div>
</ResizableBox>
</Draggable>
);
}
10 changes: 10 additions & 0 deletions fe/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import axios from "axios";
import { useMutation, useQuery } from "@tanstack/react-query";

import { INode } from "./types";

const mutation = useMutation({
mutationFn: (node: INode) => {
return axios.put(`/api/nodes/${node.id}`, node);
},
});
6 changes: 5 additions & 1 deletion fe/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
import App from "./App.tsx";
import "./index.css";

const queryClient = new QueryClient();
// set default staleTime to 20s
// https://tkdodo.eu/blog/react-query-as-a-state-manager
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 1000 * 20 } },
});

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
Expand Down
13 changes: 13 additions & 0 deletions fe/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface INode {
title: string;
contents?: string;
link?: string;
x: number;
y: number;
width?: number;
height?: number;
colour?: string | null;
canvas_id?: number | null;
id?: number | null;
created_at?: Date | null;
}

0 comments on commit b128175

Please sign in to comment.