diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/biome.xml b/.idea/biome.xml new file mode 100644 index 0000000..2422780 --- /dev/null +++ b/.idea/biome.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..fc29074 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..30bab2a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e22baf4 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/track-my-app.iml b/.idea/track-my-app.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/track-my-app.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/home/application-list.tsx b/app/home/application-list.tsx new file mode 100644 index 0000000..fc76da7 --- /dev/null +++ b/app/home/application-list.tsx @@ -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) => ( + + )) +} + +interface ListItemStore { + isAddingStage: boolean + newStageValue: string + + setNewStageValue: (newStageValue: string) => void + setIsAddingStage: (isAddingStage: boolean) => void +} + +const useListItemStore = create()((set) => ({ + isAddingStage: false, + newStageValue: "", + setNewStageValue: (newStageValue: string) => set({ newStageValue }), + setIsAddingStage: (isAddingStage: boolean) => set({ isAddingStage }), +})) + +const EntryContext = createContext(null as unknown as Entry) + +const ApplicationListItem = memo(({ entry }: { entry: Entry }) => ( + +
+ {entry.name} +
    + {entry.stages.map((step) => ( + + ))} + +
+ +
+
+)) + +const StageItem = memo(({ step }: { step: string }) => { + const entry = useContext(EntryContext) + const deleteStageInEntry = useStore((state) => state.deleteStageInEntry) + return ( +
  • +
    + {step} + {step !== DEFAULT_NODE.applicationSubmittedNode.key ? ( + + ) : null} +
    +
  • + ) +}) + +function NewStageInput() { + const isAddingStage = useListItemStore((state) => state.isAddingStage) + if (isAddingStage) { + return ( +
  • + +
  • + ) + } + return null +} +function ActualStageInput() { + const newStageValue = useListItemStore((state) => state.newStageValue) + const setNewStageValue = useListItemStore((state) => state.setNewStageValue) + return ( + { + setNewStageValue(event.currentTarget.value) + }} + className="bg-transparent" + /> + ) +} + +function ApplicationActions() { + const isAddingStage = useListItemStore((state) => state.isAddingStage) + if (isAddingStage) { + return + } + return +} + +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 ( +
    + + + + +
    + ) +}) + +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 ( +
    + + +
    + ) +}) + +export { ApplicationList } diff --git a/app/home/graph.ts b/app/home/graph.ts new file mode 100644 index 0000000..8380e0a --- /dev/null +++ b/app/home/graph.ts @@ -0,0 +1,24 @@ +interface Node { + key: string + outs: Record +} + +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 } diff --git a/app/home/store.ts b/app/home/store.ts new file mode 100644 index 0000000..28cc702 --- /dev/null +++ b/app/home/store.ts @@ -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 + starts: Node["key"][] + entries: Record + + 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()( + 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 } diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 830eea9..339854b 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -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 -} - -interface Entry { - name: string - stages: Node["key"][] -} - -interface Connection { - nodeKey: Node["key"] - weight: number -} - -interface Store { - nodes: Record - starts: Node["key"][] - entries: Record - - 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()( - 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 (
    @@ -152,7 +23,7 @@ export default function Home() {

    TrackMyApp

    - +

    My Applications

    @@ -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) => ( - - )) -} - -const ApplicationListItem = memo(({ entry }: { entry: Entry }) => { - const [isAddingStage, setIsAddingStage] = useState(false) - const inputRef = useRef(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 ( -
    - {entry.name} -
      - {entry.stages.map((step) => ( -
    1. -
      - {step} - {step !== applicationSubmittedNode.key ? ( - - ) : null} -
      -
    2. - ))} - {isAddingStage ? ( -
    3. - -
    4. - ) : null} -
    - {!isAddingStage ? ( -
    - - - - -
    - ) : ( -
    - - -
    - )} -
    - ) -}) - function AddApplicationForm() { const [isAddingEntry, setIsAddingEntry] = useState(false) const addEntry = useStore((state) => state.addEntry)