"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): Promise { 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 { 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 { 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 { 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 { 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 { 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); }