Vectrum
Sign In
120 lines · 4.8 KB · TypeScript
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createList, deleteList, findList, findListById, ListError, listPath, mayManageLists, setInList, setLiked, updateList, type RepoList } from "@/lib/lists";
import { findRepoById, repoAccess } from "@/lib/repos";
import { getUserByUsername } from "@/lib/users";
import { ActionError, repoForAction, viewerOrThrow } from "./guard";
import type { FormState } from "./types";

// Anyone signed in likes the repositories they can read, and saves them to their own lists and to
// those of organizations they can push in.

async function attempt(fn: () => Promise<FormState | void>): Promise<FormState> {
  try {
    return (await fn()) ?? undefined;
  } catch (e) {
    if (e instanceof ListError || e instanceof ActionError) return { error: e.message };
    throw e;
  }
}

function listInput(formData: FormData) {
  return {
    name: String(formData.get("name") ?? ""),
    description: String(formData.get("description") ?? ""),
    visibility: formData.get("visibility") === "private" ? ("private" as const) : ("public" as const),
  };
}

/** A list the viewer may change. */
async function listForAction(list: RepoList | null) {
  const viewer = await viewerOrThrow();
  if (!list || !(await mayManageLists(list.owner, viewer.id))) throw new ActionError("List not found.");
  return { list, viewer };
}

/** A repository the viewer can read, by id. */
async function readableRepo(repoId: string, viewerId: string) {
  const repo = await findRepoById(String(repoId));
  if (!repo || !(await repoAccess(repo, viewerId))) throw new ActionError("Repository not found.");
  return repo;
}

const refreshList = (list: RepoList) => {
  revalidatePath(listPath(list));
  revalidatePath(`/${list.owner.username}`);
};

export async function setLikedAction(owner: string, name: string, liked: boolean): Promise<FormState> {
  return attempt(async () => {
    const { repo, viewer } = await repoForAction(owner, name, "read");
    await setLiked(repo, viewer, liked === true);
    revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
    revalidatePath(`/${viewer.username}`);
  });
}

/** Puts a repository in one of the viewer's lists (or an organization's), or takes it out. */
export async function setSavedAction(listId: string, repoId: string, saved: boolean): Promise<FormState> {
  return attempt(async () => {
    const { list, viewer } = await listForAction(await findListById(String(listId)));
    const repo = await readableRepo(repoId, viewer.id);
    await setInList(list, repo.id, saved === true);
    refreshList(list);
    revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
    return { success: saved ? `Saved to ${list.name}.` : `Removed from ${list.name}.` };
  });
}

/**
 * Creates a list. From a repository's Save menu (with `repo`, a repository id) the repository goes
 * in it at once and the menu stays open; otherwise the new list's page opens.
 */
export async function createListAction(_prev: FormState, formData: FormData): Promise<FormState> {
  let path = "";
  const repoId = String(formData.get("repo") ?? "");
  const result = await attempt(async () => {
    const viewer = await viewerOrThrow();
    const owner = await getUserByUsername(String(formData.get("owner") ?? viewer.username));
    if (!owner || !(await mayManageLists(owner, viewer.id))) throw new ActionError("You can’t make lists for that account.");
    const repo = repoId ? await readableRepo(repoId, viewer.id) : null;
    const list = await createList(owner, listInput(formData));
    if (repo) {
      await setInList(list, repo.id, true);
      revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
    }
    refreshList(list);
    path = listPath(list);
    return repo ? { success: `Saved to ${list.name}.` } : undefined;
  });
  if (result?.error || repoId) return result;
  redirect(path);
}

export async function updateListAction(owner: string, slug: string, _prev: FormState, formData: FormData): Promise<FormState> {
  let path = "";
  const result = await attempt(async () => {
    const { list } = await listForAction(await findList(owner, slug));
    const updated = await updateList(list, listInput(formData));
    refreshList(list);
    refreshList(updated);
    path = listPath(updated);
  });
  if (result?.error) return result;
  redirect(path);
}

export async function deleteListAction(owner: string, slug: string, _prev: FormState, formData: FormData): Promise<FormState> {
  let path = "";
  const result = await attempt(async () => {
    const { list } = await listForAction(await findList(owner, slug));
    if (formData.get("confirm") !== list.name) return { error: `Type ${list.name} to confirm.` };
    await deleteList(list);
    refreshList(list);
    path = `/${list.owner.username}?tab=lists`;
  });
  if (result?.error) return result;
  redirect(path);
}