Files
trackmyapp/app/home/application-list.tsx

250 lines
6.2 KiB
TypeScript

import {
type Ref,
createContext,
memo,
useContext,
useEffect,
useRef,
} from "react"
import { createStore, useStore } from "zustand"
import { useShallow } from "zustand/react/shallow"
import { Button } from "~/components/button"
import { DEFAULT_NODE, type Entry } from "~/home/graph"
import { useRootStore } from "./store"
function ApplicationList() {
const entries = useRootStore(useShallow((state) => state.entries))
return Object.values(entries).map((entry) => (
<ApplicationListItem key={entry.name} entry={entry} />
))
}
interface ListItemState {
isAddingStage: boolean
newStageValue: string
setNewStageValue: (newStageValue: string) => void
setIsAddingStage: (isAddingStage: boolean) => void
addStage: (entryName: string) => void
}
function createListItemStore() {
return createStore<ListItemState>()((set, get) => ({
isAddingStage: false,
newStageValue: "",
setNewStageValue: (newStageValue: string) => set({ newStageValue }),
setIsAddingStage: (isAddingStage: boolean) => set({ isAddingStage }),
addStage: (entryName) => {
const store = useRootStore.getState()
let stage = get().newStageValue
if (stage) {
if (stage === DEFAULT_NODE.acceptedNode.key.toLowerCase()) {
stage = DEFAULT_NODE.acceptedNode.key
} else if (stage === DEFAULT_NODE.rejectedNode.key.toLowerCase()) {
stage = DEFAULT_NODE.rejectedNode.key
}
store.addStageInEntry(stage, entryName)
set({ isAddingStage: false, newStageValue: "" })
}
},
}))
}
type ListItemStore = ReturnType<typeof createListItemStore>
const ListItemStoreContext = createContext<ListItemStore>(
null as unknown as ListItemStore,
)
function useListItemStore<T>(selector: (state: ListItemState) => T): T {
const store = useContext(ListItemStoreContext)
return useStore(store, selector)
}
const EntryContext = createContext<Entry>(null as unknown as Entry)
const ApplicationListItem = memo(({ entry }: { entry: Entry }) => {
const store = useRef<ListItemStore | null>(null)
if (!store.current) {
store.current = createListItemStore()
}
return (
<ListItemStoreContext value={store.current}>
<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} stage={step} />
))}
<NewStageInput />
</ol>
<ApplicationActions />
</details>
</EntryContext>
</ListItemStoreContext>
)
})
const StageItem = memo(({ stage }: { stage: string }) => (
<li key={stage} className="w-full group justify-between px-1">
<div className="w-[90%] inline-flex flex-row flex-wrap items-center justify-between">
{stage}
{stage !== DEFAULT_NODE.applicationSubmittedNode.key ? (
<StageItemActions stage={stage} />
) : null}
</div>
</li>
))
function StageItemActions({ stage }: { stage: string }) {
const entry = useContext(EntryContext)
const deleteStageInEntry = useRootStore((state) => state.deleteStageInEntry)
return (
<div className="flex flex-row space-x-2 invisible group-hover:visible">
<Button
variant="small"
onClick={() => {
deleteStageInEntry(stage, entry.name)
}}
>
Delete
</Button>
</div>
)
}
function NewStageInput() {
const isAddingStage = useListItemStore((state) => state.isAddingStage)
const inputRef = useRef<HTMLInputElement | null>(null)
useEffect(() => {
if (isAddingStage) {
inputRef.current?.focus()
}
}, [isAddingStage])
if (isAddingStage) {
return (
<li className="px-1">
<ActualStageInput ref={inputRef} />
</li>
)
}
return null
}
function ActualStageInput({ ref }: { ref: Ref<HTMLInputElement> }) {
const entry = useContext(EntryContext)
const newStageValue = useListItemStore((state) => state.newStageValue)
const setNewStageValue = useListItemStore((state) => state.setNewStageValue)
const addStage = useListItemStore((state) => state.addStage)
return (
<input
ref={ref}
value={newStageValue}
onChange={(event) => {
setNewStageValue(event.currentTarget.value)
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
addStage(entry.name)
}
}}
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 addStageToEntry = useRootStore((state) => state.addStageInEntry)
const deleteEntry = useRootStore((state) => state.deleteEntry)
const isApplicationFinalized =
entry.stages.at(-1) === DEFAULT_NODE.acceptedNode.key ||
entry.stages.at(-1) === DEFAULT_NODE.rejectedNode.key
function onDeleteApplication() {
if (confirm("Are you sure you want to delete this application?")) {
deleteEntry(entry.name)
}
}
function onAccepted() {
addStageToEntry(DEFAULT_NODE.acceptedNode.key, entry.name)
}
function onRejected() {
addStageToEntry(DEFAULT_NODE.rejectedNode.key, entry.name)
}
return (
<div className="flex flex-row flex-wrap gap-2 pl-4 mt-2">
<Button
variant="small"
disabled={isApplicationFinalized}
onClick={() => {
setIsAddingStage(true)
}}
>
New stage
</Button>
<Button
variant="small"
disabled={isApplicationFinalized}
onClick={onAccepted}
>
Accepted
</Button>
<Button
variant="small"
disabled={isApplicationFinalized}
onClick={onRejected}
>
Rejected
</Button>
<Button variant="small" onClick={onDeleteApplication}>
Delete application
</Button>
</div>
)
})
const AddStageActions = memo(() => {
const entry = useContext(EntryContext)
const setIsAddingStage = useListItemStore((state) => state.setIsAddingStage)
const addStage = useListItemStore((state) => state.addStage)
function onOk() {
addStage(entry.name)
}
function onCancel() {
setIsAddingStage(false)
}
return (
<div className="flex flex-row space-x-2 pl-4 mt-2">
<Button variant="small" onClick={onOk}>
Ok
</Button>
<Button variant="small" onClick={onCancel}>
Cancel
</Button>
</div>
)
})
export { ApplicationList }