typescript! yay! (pain.)

This commit is contained in:
2024-12-31 03:47:10 -07:00
parent a837266dca
commit 6733a3be8e
39 changed files with 1188 additions and 335 deletions

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect } from 'react';
import React, { useCallback, useContext, useEffect } from "react";
import {
ReactFlow,
Controls,
@@ -8,55 +8,57 @@ import {
addEdge,
useReactFlow,
MarkerType,
} from '@xyflow/react';
import Dagre from '@dagrejs/dagre';
BackgroundVariant,
ConnectionLineType,
type DefaultEdgeOptions,
type ProOptions,
type ReactFlowInstance,
type Node,
type Edge,
type OnConnect,
} from "@xyflow/react";
import Dagre, { type GraphLabel } from "@dagrejs/dagre";
import '@xyflow/react/dist/style.css';
import styles from './Graph.module.css';
import "@xyflow/react/dist/style.css";
import styles from "./Graph.module.css";
import { showErrorToastNotification, showInfoToastNotification } from '../../components/ToastNotification';
import {
showErrorToastNotification,
showInfoToastNotification,
} from "../../components/ToastNotification";
import { apiFetchGraph } from '../../api/operations';
import { apiFetchGraph } from "../../api/operations";
import { RefreshAuthContext } from "../../App";
import Button from '../../components/Button';
import Button from "../../components/Button";
const initialNodes = [];
// const initialNodes = [
// { id: '1', position: { x: 0, y: 0 }, data: { label: '1' } },
// { id: '2', position: { x: 0, y: 100 }, data: { label: '2' } },
// { id: '3', position: { x: 50, y: 50 }, data: { label: '3' } },
// ];
const initialEdges = [];
// const initialEdges = [
// { id: 'e1-2', source: '1', target: '2' }
// ];
const initialNodes: any[] = [];
const initialEdges: any[] = [];
const nodeOffsets = {
connected: {
origin: {
x: 0,
y: 0
y: 0,
},
scaling: {
x: 240,
y: 80
}
y: 80,
},
},
unconnected: {
origin: {
x: 800,
y: 0
y: 0,
},
scaling: {
x: 160,
y: 40
}
}
}
y: 40,
},
},
};
/** @type {import('@xyflow/react').DefaultEdgeOptions} */
const edgeOptions = {
const edgeOptions: DefaultEdgeOptions = {
animated: true,
style: {
stroke: "white",
@@ -67,10 +69,10 @@ const edgeOptions = {
color: "white",
width: 40,
height: 40,
}
},
};
const proOptions = { hideAttribution: true };
const proOptions: ProOptions = { hideAttribution: true };
const Graph = () => {
const refreshAuth = useContext(RefreshAuthContext);
@@ -78,39 +80,65 @@ const Graph = () => {
const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onFlowInit = (instance) => {
const onFlowInit = (instance: ReactFlowInstance) => {
console.debug("flow loaded");
}
};
const onConnect = useCallback((params) => {
setEdges((eds) => addEdge(params, eds));
console.debug("new connection");
console.debug(params);
}, [setEdges]);
const onConnect: OnConnect = useCallback(
(params) => {
setEdges((eds) => addEdge(params, eds));
console.debug("new connection");
console.debug(params);
},
[setEdges]
);
const getLayoutedElements = (nodes, edges, options = { direction: "TB" }) => {
const g = new Dagre.graphlib.Graph()
type getLayoutedElementsOpts = {
direction: GraphLabel["rankdir"];
};
const getLayoutedElements = (
nodes: Node[],
edges: Edge[],
options: getLayoutedElementsOpts = { direction: "TB" }
) => {
const g = new Dagre.graphlib.Graph();
g.setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: options.direction, nodesep: 200, edgesep: 200, ranksep: 200 });
g.setGraph({
rankdir: options.direction,
nodesep: 200,
edgesep: 200,
ranksep: 200,
});
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
const connectedNodesID = new Set(edges.flatMap(edge => [edge.source, edge.target]));
const connectedNodes = nodes.filter(node => connectedNodesID.has(node.id));
const unconnectedNodes = nodes.filter(node => !connectedNodesID.has(node.id));
const connectedNodesID = new Set(
edges.flatMap((edge) => [edge.source, edge.target])
);
const connectedNodes = nodes.filter((node) =>
connectedNodesID.has(node.id)
);
const unconnectedNodes = nodes.filter(
(node) => !connectedNodesID.has(node.id)
);
nodes.forEach((node) => {
g.setNode(node.id, {
...node,
width: node.measured?.width ?? 0,
height: node.measured?.height ?? 0,
})
});
});
Dagre.layout(g);
let finalLayout = { edges };
finalLayout.nodes = [
let finalLayout: { nodes: Node[]; edges: Edge[] } = {
nodes: [],
edges: [],
};
finalLayout.edges = edges;
finalLayout.nodes.push(
...connectedNodes.map((node) => {
const position = g.node(node.id);
// We are shifting the dagre node position (anchor=center center) to the top left
@@ -119,32 +147,41 @@ const Graph = () => {
const y = position.y - (node.measured?.height ?? 0) / 2;
return { ...node, position: { x, y } };
}),
})
);
finalLayout.nodes.push(
...unconnectedNodes.map((node, idx) => {
const position = {
x: nodeOffsets.unconnected.origin.x + Math.floor(idx / 20) * nodeOffsets.unconnected.scaling.x,
y: nodeOffsets.unconnected.origin.y + Math.floor(idx % 20) * nodeOffsets.unconnected.scaling.y,
x:
nodeOffsets.unconnected.origin.x +
Math.floor(idx / 20) * nodeOffsets.unconnected.scaling.x,
y:
nodeOffsets.unconnected.origin.y +
Math.floor(idx % 20) * nodeOffsets.unconnected.scaling.y,
};
const x = position.x - (node.measured?.width ?? 0) / 2;
const y = position.y - (node.measured?.height ?? 0) / 2;
return { ...node, position: { x, y } };
})
];
);
console.debug("layout generated");
return finalLayout;
};
const arrangeLayout = (direction) => {
const layouted = getLayoutedElements(playlistNodes, linkEdges, { direction });
const arrangeLayout = (direction: GraphLabel["rankdir"]) => {
const layouted = getLayoutedElements(playlistNodes, linkEdges, {
direction,
});
setNodes([...layouted.nodes]);
setEdges([...layouted.edges]);
setTimeout(flowInstance.fitView);
console.debug("layout applied");
}
};
useEffect(() => {
const fetchGraph = async () => {
@@ -155,29 +192,37 @@ const Graph = () => {
}
if (resp.status === 200) {
// place playlist nodes
setNodes(resp.data.playlists.map((pl, idx) => {
return {
id: `${pl.playlistID}`,
position: {
x: nodeOffsets.unconnected.origin.x + Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x,
y: nodeOffsets.unconnected.origin.y + Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y,
},
data: {
label: pl.playlistName,
metadata: {
pl
}
}
}
}));
setNodes(
resp.data.playlists?.map((pl, idx) => {
return {
id: `${pl.playlistID}`,
position: {
x:
nodeOffsets.unconnected.origin.x +
Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x,
y:
nodeOffsets.unconnected.origin.y +
Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y,
},
data: {
label: pl.playlistName,
metadata: {
pl,
},
},
};
}) ?? []
);
// connect links
setEdges(resp.data.links.map((link, idx) => {
return {
id: `${idx}`,
source: link.from,
target: link.to
}
}));
setEdges(
resp.data.links?.map((link, idx) => {
return {
id: `${idx}`,
source: link.from,
target: link.to,
};
}) ?? []
);
showInfoToastNotification("Graph updated.");
return;
}
@@ -186,11 +231,11 @@ const Graph = () => {
return;
}
if (resp.status === 401) {
await refreshAuth();
refreshAuth();
}
showErrorToastNotification(resp.data.message);
return;
}
};
fetchGraph();
}, [refreshAuth, setEdges, setNodes]);
@@ -200,7 +245,7 @@ const Graph = () => {
nodes={playlistNodes}
edges={linkEdges}
defaultEdgeOptions={edgeOptions}
connectionLineType="smoothstep"
connectionLineType={ConnectionLineType.SmoothStep}
fitView
proOptions={proOptions}
colorMode={"light"}
@@ -210,17 +255,17 @@ const Graph = () => {
onConnect={onConnect}
>
<Controls />
<Background variant='dots' gap={36} size={3} />
<Background variant={BackgroundVariant.Dots} gap={36} size={3} />
{/* <Panel position="top-right"> */}
{/* <button onClick={() => arrangeLayout('TB')}>Arrange vertically</button> */}
{/* <button onClick={() => arrangeLayout('LR')}>Arrange horizontally</button> */}
{/* </Panel> */}
</ReactFlow>
<div className={styles.operations_wrapper}>
<Button onClickMethod={() => arrangeLayout('TB')}>Arrange</Button>
<Button onClickMethod={() => arrangeLayout("TB")}>Arrange</Button>
</div>
</div>
)
}
);
};
export default Graph;

View File

@@ -1,13 +1,14 @@
import React, { useEffect } from "react"
import React, { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import styles from "./Landing.module.css"
import { showInfoToastNotification, showSuccessToastNotification } from "../../components/ToastNotification";
import styles from "./Landing.module.css";
import {
showInfoToastNotification,
showSuccessToastNotification,
} from "../../components/ToastNotification";
import AnimatedSVG from "../../components/AnimatedSVG";
const Landing = () => {
// eslint-disable-next-line no-unused-vars
const [searchParams, setSearchParams] = useSearchParams();
const [searchParams] = useSearchParams();
useEffect(() => {
if (searchParams.get("login") === "success") {
showSuccessToastNotification("Logged in!");
@@ -27,7 +28,7 @@ const Landing = () => {
<li>Periodic syncing</li>
</ul>
</>
)
}
);
};
export default Landing
export default Landing;

View File

@@ -1,21 +0,0 @@
import React, { useEffect } from 'react';
import styles from './Login.module.css';
import { authLoginURL } from '../../api/paths';
// auth through backend
const Login = () => {
useEffect(() => {
const timeoutID = setTimeout(() => {
window.open(authLoginURL, "_self")
}, 1000);
return () => clearTimeout(timeoutID);
}, []);
return (
<div className={styles.login_wrapper}>
Redirecting to Spotify...
</div>
)
}
export default Login;

17
src/pages/Login/index.tsx Normal file
View File

@@ -0,0 +1,17 @@
import React, { useEffect } from "react";
import styles from "./Login.module.css";
import { authLoginURL } from "../../api/paths";
// auth through backend
const Login = () => {
useEffect(() => {
const timeoutID = setTimeout(() => {
window.open(authLoginURL, "_self");
}, 1000);
return () => clearTimeout(timeoutID);
}, []);
return <div className={styles.login_wrapper}>Redirecting to Spotify...</div>;
};
export default Login;

View File

@@ -1,20 +0,0 @@
import React, { useEffect } from 'react';
import styles from './Logout.module.css';
import { authLogoutURL } from '../../api/paths';
const Logout = () => {
useEffect(() => {
const timeoutID = setTimeout(() => {
window.open(authLogoutURL, "_self")
}, 1000);
return () => clearTimeout(timeoutID);
}, []);
return (
<div className={styles.logout_wrapper}>
See you soon!
</div>
)
}
export default Logout;

View File

@@ -0,0 +1,16 @@
import React, { useEffect } from "react";
import styles from "./Logout.module.css";
import { authLogoutURL } from "../../api/paths";
const Logout = () => {
useEffect(() => {
const timeoutID = setTimeout(() => {
window.open(authLogoutURL, "_self");
}, 1000);
return () => clearTimeout(timeoutID);
}, []);
return <div className={styles.logout_wrapper}>See you soon!</div>;
};
export default Logout;

View File

@@ -1,17 +0,0 @@
import React, { useEffect } from 'react';
import styles from "./PageNotFound.module.css";
import { showWarnToastNotification } from '../../components/ToastNotification';
const PageNotFound = () => {
useEffect(() => {
showWarnToastNotification("Oops!")
}, []);
return (
<div className={styles.pnf_wrapper}>
PageNotFound
</div>
)
}
export default PageNotFound

View File

@@ -0,0 +1,13 @@
import React, { useEffect } from "react";
import styles from "./PageNotFound.module.css";
import { showWarnToastNotification } from "../../components/ToastNotification";
const PageNotFound = () => {
useEffect(() => {
showWarnToastNotification("Oops!");
}, []);
return <div className={styles.pnf_wrapper}>Page Not Found</div>;
};
export default PageNotFound;