refactor: reorganize code + optimization

This commit is contained in:
2025-01-26 17:17:37 +00:00
parent 672917ac92
commit 1bc4206e39
11 changed files with 378 additions and 228 deletions

View File

@@ -0,0 +1,163 @@
import { createContext, memo, useContext } from "react"
import { create } from "zustand"
import { useShallow } from "zustand/react/shallow"
import { Button } from "~/components/button"
import { DEFAULT_NODE, type Entry } from "~/home/graph"
import { useStore } from "./store"
function ApplicationList() {
const entries = useStore(useShallow((state) => state.entries))
return Object.values(entries).map((entry) => (
<ApplicationListItem key={entry.name} entry={entry} />
))
}
interface ListItemStore {
isAddingStage: boolean
newStageValue: string
setNewStageValue: (newStageValue: string) => void
setIsAddingStage: (isAddingStage: boolean) => void
}
const useListItemStore = create<ListItemStore>()((set) => ({
isAddingStage: false,
newStageValue: "",
setNewStageValue: (newStageValue: string) => set({ newStageValue }),
setIsAddingStage: (isAddingStage: boolean) => set({ isAddingStage }),
}))
const EntryContext = createContext<Entry>(null as unknown as Entry)
const ApplicationListItem = memo(({ entry }: { entry: Entry }) => (
<EntryContext value={entry}>
<details className="w-full px-2 pb-2 -mx-2">
<summary className="cursor-pointer">{entry.name}</summary>
<ol className="pl-3 list-decimal list-inside text-sm">
{entry.stages.map((step) => (
<StageItem key={step} step={step} />
))}
<NewStageInput />
</ol>
<ApplicationActions />
</details>
</EntryContext>
))
const StageItem = memo(({ step }: { step: string }) => {
const entry = useContext(EntryContext)
const deleteStageInEntry = useStore((state) => state.deleteStageInEntry)
return (
<li key={step} className="w-full group justify-between px-1">
<div className="w-[90%] inline-flex flex-row items-center justify-between">
{step}
{step !== DEFAULT_NODE.applicationSubmittedNode.key ? (
<Button
onClick={() => {
deleteStageInEntry(step, entry.name)
}}
className="text-xs py-0 invisible group-hover:visible"
>
Delete
</Button>
) : null}
</div>
</li>
)
})
function NewStageInput() {
const isAddingStage = useListItemStore((state) => state.isAddingStage)
if (isAddingStage) {
return (
<li className="px-1">
<ActualStageInput />
</li>
)
}
return null
}
function ActualStageInput() {
const newStageValue = useListItemStore((state) => state.newStageValue)
const setNewStageValue = useListItemStore((state) => state.setNewStageValue)
return (
<input
value={newStageValue}
onChange={(event) => {
setNewStageValue(event.currentTarget.value)
}}
className="bg-transparent"
/>
)
}
function ApplicationActions() {
const isAddingStage = useListItemStore((state) => state.isAddingStage)
if (isAddingStage) {
return <AddStageActions />
}
return <DefaultActions />
}
const DefaultActions = memo(() => {
const entry = useContext(EntryContext)
const setIsAddingStage = useListItemStore((state) => state.setIsAddingStage)
const deleteEntry = useStore((state) => state.deleteEntry)
function onDeleteApplication() {
if (confirm("Are you sure you want to delete this application?")) {
deleteEntry(entry.name)
}
}
return (
<div className="flex flex-row flex-wrap gap-2 pl-4 mt-2">
<Button
onClick={() => {
setIsAddingStage(true)
}}
className="text-xs py-0"
>
New stage
</Button>
<Button className="text-xs py-0">Accepted</Button>
<Button className="text-xs py-0">Rejected</Button>
<Button onClick={onDeleteApplication} className="text-xs py-0">
Delete application
</Button>
</div>
)
})
const AddStageActions = memo(() => {
const entry = useContext(EntryContext)
const setIsAddingStage = useListItemStore((state) => state.setIsAddingStage)
const setNewStageValue = useListItemStore((state) => state.setNewStageValue)
const addStageToEntry = useStore((state) => state.addStageInEntry)
function onOk() {
const stage = useListItemStore.getState().newStageValue
if (stage) {
addStageToEntry(stage, entry.name)
setIsAddingStage(false)
setNewStageValue("")
}
}
function onCancel() {
setIsAddingStage(false)
}
return (
<div className="flex flex-row space-x-2 pl-4 mt-2">
<Button onClick={onOk} className="text-xs py-0">
Ok
</Button>
<Button onClick={onCancel} className="text-xs py-0">
Cancel
</Button>
</div>
)
})
export { ApplicationList }

24
app/home/graph.ts Normal file
View File

@@ -0,0 +1,24 @@
interface Node {
key: string
outs: Record<string, Connection>
}
interface Entry {
name: string
stages: Node["key"][]
}
interface Connection {
nodeKey: Node["key"]
weight: number
}
const DEFAULT_NODE = {
applicationSubmittedNode: {
key: "Application submitted",
outs: {},
},
} as const
export { DEFAULT_NODE }
export type { Node, Entry, Connection }

118
app/home/store.ts Normal file
View File

@@ -0,0 +1,118 @@
import { create } from "zustand/index"
import { immer } from "zustand/middleware/immer"
import { DEFAULT_NODE, type Entry, type Node } from "~/home/graph"
interface Store {
nodes: Record<string, Node>
starts: Node["key"][]
entries: Record<string, Entry>
addEntry: (name: string) => void
hasEntry: (name: string) => boolean
addStageInEntry: (stage: string, entryName: string) => void
deleteStageInEntry: (stage: string, entryName: string) => void
deleteEntry: (entryName: string) => void
}
const useStore = create<Store>()(
immer((set, get) => ({
isAddingEntry: false,
nodes: {
[DEFAULT_NODE.applicationSubmittedNode.key]:
DEFAULT_NODE.applicationSubmittedNode,
},
starts: [DEFAULT_NODE.applicationSubmittedNode.key],
entries: {},
addEntry: (name) =>
set((state) => {
const currentEntries = state.entries
if (!(name in currentEntries)) {
state.entries[name] = {
name,
stages: [DEFAULT_NODE.applicationSubmittedNode.key],
}
}
}),
hasEntry: (name) => {
return name in get().entries
},
addStageInEntry: (stage, entryName) =>
set((state) => {
const entry = state.entries[entryName]
if (entry) {
let node = state.nodes[stage]
if (!node) {
node = {
key: stage,
outs: {},
}
state.nodes[stage] = node
}
const lastStageNodeKey = entry.stages.at(-1) ?? state.starts[0]
if (lastStageNodeKey === stage) {
return
}
const lastStageNode = state.nodes[lastStageNodeKey]
const conn = lastStageNode.outs[node.key]
if (conn) {
conn.weight++
} else {
lastStageNode.outs[node.key] = { nodeKey: node.key, weight: 1 }
}
entry.stages.push(node.key)
}
}),
deleteStageInEntry: (stage, entryName) =>
set((state) => {
if (stage === DEFAULT_NODE.applicationSubmittedNode.key) {
return
}
const entry = state.entries[entryName]
const node = state.nodes[stage]
if (entry && node) {
const lastStageNodeKey = entry.stages.at(-2)
if (!lastStageNodeKey) {
return
}
const lastStageNode = state.nodes[lastStageNodeKey]
const conn = lastStageNode.outs[node.key]
if (conn) {
if (conn.weight === 1) {
delete lastStageNode.outs[node.key]
} else {
conn.weight--
}
}
entry.stages = entry.stages.filter((step) => step !== stage)
}
}),
deleteEntry: (entryName) =>
set((state) => {
const entry = state.entries[entryName]
for (let i = 1; i < entry.stages.length; ++i) {
const lastStageNode = state.nodes[entry.stages[i - 1]]
const node = state.nodes[entry.stages[i]]
const conn = lastStageNode.outs[node.key]
if (conn) {
if (conn.weight === 1) {
delete lastStageNode.outs[node.key]
} else {
conn.weight--
}
}
}
delete state.entries[entryName]
}),
})),
)
export { useStore }

View File

@@ -1,149 +1,20 @@
import type { Route } from "./+types/home"
import { create } from "zustand"
import Chart from "react-google-charts"
import { memo, useMemo, useRef, useState } from "react"
import { Button } from "~/components/button"
import { useShallow } from "zustand/react/shallow"
import { immer } from "zustand/middleware/immer"
import { Queue } from "~/queue"
import clsx from "clsx"
import { useMemo, useRef, useState } from "react"
import Chart from "react-google-charts"
import { Button } from "~/components/button"
import { ApplicationList } from "~/home/application-list"
import type { Node } from "~/home/graph"
import { useStore } from "~/home/store"
import { Queue } from "~/queue"
import { useUiMode } from "~/use-ui-mode"
export function meta({}: Route.MetaArgs) {
export function meta() {
return [
{ title: "TrackMyApp" },
{ name: "description", content: "Track and visualize your applications" },
]
}
interface Node {
key: string
outs: Record<string, Connection>
}
interface Entry {
name: string
stages: Node["key"][]
}
interface Connection {
nodeKey: Node["key"]
weight: number
}
interface Store {
nodes: Record<string, Node>
starts: Node["key"][]
entries: Record<string, Entry>
addEntry: (name: string) => void
hasEntry: (name: string) => boolean
addStageInEntry: (stage: string, entryName: string) => void
deleteStageInEntry: (stage: string, entryName: string) => void
deleteEntry: (entryName: string) => void
}
const applicationSubmittedNode: Node = {
key: "Application submitted",
outs: {},
}
const useStore = create<Store>()(
immer((set, get) => ({
isAddingEntry: false,
nodes: {
[applicationSubmittedNode.key]: applicationSubmittedNode,
},
starts: [applicationSubmittedNode.key],
entries: {},
addEntry: (name) =>
set((state) => {
const currentEntries = state.entries
if (!(name in currentEntries)) {
state.entries[name] = {
name,
stages: [applicationSubmittedNode.key],
}
}
}),
hasEntry: (name) => {
return name in get().entries
},
addStageInEntry: (stage, entryName) =>
set((state) => {
const entry = state.entries[entryName]
if (entry) {
let node = state.nodes[stage]
if (!node) {
node = {
key: stage,
outs: {},
}
state.nodes[stage] = node
}
const lastStageNodeKey = entry.stages.at(-1) ?? state.starts[0]
if (lastStageNodeKey === stage) {
return
}
const lastStageNode = state.nodes[lastStageNodeKey]
const conn = lastStageNode.outs[node.key]
if (conn) {
conn.weight++
} else {
lastStageNode.outs[node.key] = { nodeKey: node.key, weight: 1 }
}
entry.stages.push(node.key)
}
}),
deleteStageInEntry: (stage, entryName) =>
set((state) => {
if (stage === applicationSubmittedNode.key) {
return
}
const entry = state.entries[entryName]
const node = state.nodes[stage]
if (entry && node) {
const lastStageNodeKey = entry.stages.at(-2)!
const lastStageNode = state.nodes[lastStageNodeKey]
const conn = lastStageNode.outs[node.key]
if (conn) {
if (conn.weight === 1) {
delete lastStageNode.outs[node.key]
} else {
conn.weight--
}
}
entry.stages = entry.stages.filter((step) => step !== stage)
}
}),
deleteEntry: (entryName) =>
set((state) => {
const entry = state.entries[entryName]
for (let i = 1; i < entry.stages.length; ++i) {
const lastStageNode = state.nodes[entry.stages[i - 1]]
const node = state.nodes[entry.stages[i]]
const conn = lastStageNode.outs[node.key]
if (conn) {
if (conn.weight === 1) {
delete lastStageNode.outs[node.key]
} else {
conn.weight--
}
}
}
delete state.entries[entryName]
}),
})),
)
export default function Home() {
return (
<div className="flex items-center justify-center p-4 md:px-16 md:pt-20 w-full">
@@ -152,7 +23,7 @@ export default function Home() {
<h1 className="text-2xl font-bold">TrackMyApp</h1>
</header>
<FluffChart />
<FlufferChart />
<h2 className="text-lg font-bold my-4">My Applications</h2>
@@ -163,7 +34,7 @@ export default function Home() {
)
}
function FluffChart() {
function FlufferChart() {
const nodes = useStore((state) => state.nodes)
const starts = useStore((state) => state.starts)
const uiMode = useUiMode()
@@ -213,95 +84,6 @@ function FluffChart() {
)
}
function ApplicationList() {
const entries = useStore(useShallow((state) => state.entries))
return Object.values(entries).map((entry) => (
<ApplicationListItem key={entry.name} entry={entry} />
))
}
const ApplicationListItem = memo(({ entry }: { entry: Entry }) => {
const [isAddingStage, setIsAddingStage] = useState(false)
const inputRef = useRef<HTMLInputElement | null>(null)
const addStageInEntry = useStore((state) => state.addStageInEntry)
const deleteStageInEntry = useStore((state) => state.deleteStageInEntry)
const deleteEntry = useStore((state) => state.deleteEntry)
function onOk() {
if (inputRef.current && inputRef.current.value) {
addStageInEntry(inputRef.current.value, entry.name)
setIsAddingStage(false)
}
}
function onCancel() {
setIsAddingStage(false)
}
function onDeleteApplication() {
if (confirm("Are you sure you want to delete this application?")) {
deleteEntry(entry.name)
}
}
return (
<details className="w-full px-2 pb-2 -mx-2">
<summary className="cursor-pointer">{entry.name}</summary>
<ol className="pl-3 list-decimal list-inside text-sm">
{entry.stages.map((step) => (
<li key={step} className="w-full group justify-between px-1">
<div className="w-[90%] inline-flex flex-row items-center justify-between">
{step}
{step !== applicationSubmittedNode.key ? (
<Button
onClick={() => {
deleteStageInEntry(step, entry.name)
}}
className="text-xs py-0 invisible group-hover:visible"
>
Delete
</Button>
) : null}
</div>
</li>
))}
{isAddingStage ? (
<li className="px-1">
<input required ref={inputRef} className="bg-transparent" />
</li>
) : null}
</ol>
{!isAddingStage ? (
<div className="flex flex-row flex-wrap gap-2 pl-4 mt-2">
<Button
onClick={() => {
setIsAddingStage(true)
}}
className="text-xs py-0"
>
New stage
</Button>
<Button className="text-xs py-0">Accepted</Button>
<Button className="text-xs py-0">Rejected</Button>
<Button onClick={onDeleteApplication} className="text-xs py-0">
Delete application
</Button>
</div>
) : (
<div className="flex flex-row space-x-2 pl-4 mt-2">
<Button onClick={onOk} className="text-xs py-0">
Ok
</Button>
<Button onClick={onCancel} className="text-xs py-0">
Cancel
</Button>
</div>
)}
</details>
)
})
function AddApplicationForm() {
const [isAddingEntry, setIsAddingEntry] = useState(false)
const addEntry = useStore((state) => state.addEntry)