"use client";
import { createContext, use, useCallback, useMemo, useState, type ReactNode } from "react";
import { useToast } from "@unlocalhosted/metalui";
import { createLabelAction } from "@/app/actions/issues";
import { LABEL_COLORS, type IssueLabel, type Member } from "@/lib/issue-meta";
type Tone = "info" | "success" | "error";
type Tracker = {
owner: string;
name: string;
base: string;
members: Member[];
labels: IssueLabel[];
viewerId: string | null;
/** Push access: may set priority, assignee, labels, estimates and reorder. */
canTriage: boolean;
/** Today as YYYY-MM-DD, from the server, for overdue highlighting. */
today: string;
createLabel: (name: string) => Promise<IssueLabel | null>;
/** The result of the person's own action, on MetalUI's toast deck (errors stay until dismissed). */
notify: (message: string, options?: { sub?: string; tone?: Tone }) => void;
};
const TrackerContext = createContext<Tracker | null>(null);
export function useTracker() {
const tracker = use(TrackerContext);
if (!tracker) throw new Error("useTracker must be used inside <TrackerProvider>");
return tracker;
}
export function TrackerProvider({
children,
labels: serverLabels,
...rest
}: Omit<Tracker, "createLabel" | "notify"> & { children: ReactNode }) {
const { owner, name } = rest;
const toast = useToast();
const [created, setCreated] = useState<IssueLabel[]>([]);
const notify = useCallback<Tracker["notify"]>(
(message, options) =>
void toast.show({ title: message, sub: options?.sub, tone: options?.tone === "info" || !options?.tone ? "default" : options.tone }),
[toast],
);
// Labels created from a picker are usable right away, before the page data refreshes.
const labels = useMemo(() => {
const ids = new Set(serverLabels.map((l) => l.id));
return [...serverLabels, ...created.filter((l) => !ids.has(l.id))].sort((a, b) => a.name.localeCompare(b.name));
}, [serverLabels, created]);
const createLabel = useCallback(
async (labelName: string) => {
const color = LABEL_COLORS[labels.length % LABEL_COLORS.length];
const result = await createLabelAction(owner, name, { name: labelName, color });
if (result.error || !result.label) {
notify(result.error ?? "Couldn't create the label.", { tone: "error" });
return null;
}
setCreated((c) => [...c, result.label!]);
return result.label;
},
[owner, name, labels.length, notify],
);
return <TrackerContext value={{ ...rest, labels, createLabel, notify }}>{children}</TrackerContext>;
}