impl: autosave to local storage
This commit is contained in:
@@ -103,14 +103,6 @@ function StageItemActions({ stage }: { stage: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row space-x-2 invisible group-hover:visible">
|
<div className="flex flex-row space-x-2 invisible group-hover:visible">
|
||||||
<Button
|
|
||||||
variant="small"
|
|
||||||
onClick={() => {
|
|
||||||
deleteStageInEntry(stage, entry.name)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="small"
|
variant="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@@ -1,17 +1,37 @@
|
|||||||
interface Node {
|
import {
|
||||||
key: string
|
type Infer,
|
||||||
outs: Record<string, Connection>
|
array,
|
||||||
}
|
integer,
|
||||||
|
min,
|
||||||
|
object,
|
||||||
|
record,
|
||||||
|
string,
|
||||||
|
} from "superstruct"
|
||||||
|
|
||||||
interface Entry {
|
const $Connection = object({
|
||||||
name: string
|
nodeKey: string(),
|
||||||
stages: Node["key"][]
|
weight: min(integer(), 0),
|
||||||
}
|
})
|
||||||
|
type Connection = Infer<typeof $Connection>
|
||||||
|
|
||||||
interface Connection {
|
const $Node = object({
|
||||||
nodeKey: Node["key"]
|
key: string(),
|
||||||
weight: number
|
outs: record(string(), $Connection),
|
||||||
}
|
})
|
||||||
|
type Node = Infer<typeof $Node>
|
||||||
|
|
||||||
|
const $Entry = object({
|
||||||
|
name: string(),
|
||||||
|
stages: array(string()),
|
||||||
|
})
|
||||||
|
type Entry = Infer<typeof $Entry>
|
||||||
|
|
||||||
|
const $Graph = object({
|
||||||
|
nodes: record(string(), $Node),
|
||||||
|
starts: array(string()),
|
||||||
|
entries: record(string(), $Entry),
|
||||||
|
})
|
||||||
|
type Graph = Infer<typeof $Graph>
|
||||||
|
|
||||||
const DEFAULT_NODE = {
|
const DEFAULT_NODE = {
|
||||||
applicationSubmittedNode: {
|
applicationSubmittedNode: {
|
||||||
@@ -28,5 +48,5 @@ const DEFAULT_NODE = {
|
|||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export { DEFAULT_NODE }
|
export { $Graph, DEFAULT_NODE }
|
||||||
export type { Node, Entry, Connection }
|
export type { Graph, Node, Entry, Connection }
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
|
import { is } from "superstruct"
|
||||||
import { create } from "zustand/index"
|
import { create } from "zustand/index"
|
||||||
import { immer } from "zustand/middleware/immer"
|
import { immer } from "zustand/middleware/immer"
|
||||||
import { DEFAULT_NODE, type Entry, type Node } from "~/home/graph"
|
import { $Graph, DEFAULT_NODE, type Entry, type Node } from "~/home/graph"
|
||||||
|
|
||||||
interface RootStore {
|
interface RootStore {
|
||||||
nodes: Record<string, Node>
|
nodes: Record<string, Node>
|
||||||
starts: Node["key"][]
|
starts: Node["key"][]
|
||||||
entries: Record<string, Entry>
|
entries: Record<string, Entry>
|
||||||
|
|
||||||
|
loadGraphFromLocalStorage: () => boolean
|
||||||
|
resetGraph: () => void
|
||||||
addEntry: (name: string) => void
|
addEntry: (name: string) => void
|
||||||
hasEntry: (name: string) => boolean
|
hasEntry: (name: string) => boolean
|
||||||
addStageInEntry: (stage: string, entryName: string) => void
|
addStageInEntry: (stage: string, entryName: string) => void
|
||||||
@@ -25,6 +28,38 @@ const useRootStore = create<RootStore>()(
|
|||||||
starts: [DEFAULT_NODE.applicationSubmittedNode.key],
|
starts: [DEFAULT_NODE.applicationSubmittedNode.key],
|
||||||
entries: {},
|
entries: {},
|
||||||
|
|
||||||
|
loadGraphFromLocalStorage: () => {
|
||||||
|
const graphJson = localStorage.getItem("graph")
|
||||||
|
if (!graphJson) {
|
||||||
|
// if there is no saved data, then we simply return
|
||||||
|
// this is not considered to be an error
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const graph = JSON.parse(graphJson)
|
||||||
|
if (!is(graph, $Graph)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
nodes: graph.nodes,
|
||||||
|
starts: graph.starts,
|
||||||
|
entries: graph.entries,
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
resetGraph: () =>
|
||||||
|
set({
|
||||||
|
nodes: {
|
||||||
|
[DEFAULT_NODE.applicationSubmittedNode.key]:
|
||||||
|
DEFAULT_NODE.applicationSubmittedNode,
|
||||||
|
},
|
||||||
|
starts: [DEFAULT_NODE.applicationSubmittedNode.key],
|
||||||
|
entries: {},
|
||||||
|
}),
|
||||||
|
|
||||||
addEntry: (name) =>
|
addEntry: (name) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const currentEntries = state.entries
|
const currentEntries = state.entries
|
||||||
|
@@ -16,6 +16,25 @@ export function meta() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const loadGraphFromLocalStorage = useRootStore(
|
||||||
|
(state) => state.loadGraphFromLocalStorage,
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(function saveGraphToLocalStorage() {
|
||||||
|
const unsub = useRootStore.subscribe(({ nodes, starts, entries }) => {
|
||||||
|
localStorage.setItem("graph", JSON.stringify({ nodes, starts, entries }))
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
unsub()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadGraphFromLocalStorage()) {
|
||||||
|
alert("Unable to load locally saved data due to inconsistency.")
|
||||||
|
}
|
||||||
|
}, [loadGraphFromLocalStorage])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center p-4 md:px-16 md:pt-20 w-full">
|
<div className="flex items-center justify-center p-4 md:px-16 md:pt-20 w-full">
|
||||||
<main className="w-full max-w-xl flex flex-col items-start">
|
<main className="w-full max-w-xl flex flex-col items-start">
|
||||||
@@ -37,26 +56,34 @@ export default function Home() {
|
|||||||
function FlufferChart() {
|
function FlufferChart() {
|
||||||
const nodes = useRootStore((state) => state.nodes)
|
const nodes = useRootStore((state) => state.nodes)
|
||||||
const starts = useRootStore((state) => state.starts)
|
const starts = useRootStore((state) => state.starts)
|
||||||
|
const resetGraph = useRootStore((state) => state.resetGraph)
|
||||||
const uiMode = useUiMode()
|
const uiMode = useUiMode()
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
const rows: [string, string, number][] = []
|
try {
|
||||||
const queue = new Queue<Node>()
|
const rows: [string, string, number][] = []
|
||||||
for (const nodeKey of starts) {
|
const queue = new Queue<Node>()
|
||||||
queue.enqueue(nodes[nodeKey])
|
for (const nodeKey of starts) {
|
||||||
}
|
queue.enqueue(nodes[nodeKey])
|
||||||
while (!queue.isEmpty) {
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: if queue is non empty, then dequeue will always be non null
|
|
||||||
const currentNode = queue.dequeue()!
|
|
||||||
for (const nodeKey in currentNode.outs) {
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
|
||||||
const connection = currentNode.outs[nodeKey]!
|
|
||||||
rows.push([currentNode.key, connection.nodeKey, connection.weight])
|
|
||||||
queue.enqueue(nodes[connection.nodeKey])
|
|
||||||
}
|
}
|
||||||
|
while (!queue.isEmpty) {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: if queue is non empty, then dequeue will always be non null
|
||||||
|
const currentNode = queue.dequeue()!
|
||||||
|
for (const nodeKey in currentNode.outs) {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||||
|
const connection = currentNode.outs[nodeKey]!
|
||||||
|
rows.push([currentNode.key, connection.nodeKey, connection.weight])
|
||||||
|
queue.enqueue(nodes[connection.nodeKey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
} catch {
|
||||||
|
if (confirm("Invalid data detected. Erase data and reset?")) {
|
||||||
|
resetGraph()
|
||||||
|
}
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
return rows
|
}, [starts, nodes, resetGraph])
|
||||||
}, [starts, nodes])
|
|
||||||
|
|
||||||
const hasData = data.length > 0
|
const hasData = data.length > 0
|
||||||
|
|
||||||
|
@@ -18,6 +18,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-google-charts": "^5.2.1",
|
"react-google-charts": "^5.2.1",
|
||||||
"react-router": "^7.1.3",
|
"react-router": "^7.1.3",
|
||||||
|
"superstruct": "^2.0.2",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
Reference in New Issue
Block a user