Ctrl K
ComunidadGuías…

lib/comments-api.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use client";

import { fetchClientRead } from "./client-read";

import type { Attachment, Comment, IssueEvent, ResourceInput } from "./types";
import type { CommentVisibility } from "./feedback/types";
import { trackEvent } from "./analytics";
import { lengthBucket } from "./analytics-sanitize";

async function parseJson<T>(response: Response): Promise<T> {
  const text = await response.text();
  let data: unknown = null;
  try {
    data = text ? JSON.parse(text) : null;
  } catch {
    data = null;
  }
  if (!response.ok) {
    const message =
      (data as { error?: string } | null)?.error || text.trim() || "Request failed";
    throw new Error(message);
  }
  return data as T;
}

export async function fetchCommentsApi(issueId: string, signal?: AbortSignal): Promise<Comment[]> {
  return parseJson<Comment[]>(await fetchClientRead(`/api/issues/${issueId}/comments`, { signal }));
}

export async function fetchEventsApi(issueId: string, signal?: AbortSignal): Promise<IssueEvent[]> {
  return parseJson<IssueEvent[]>(await fetchClientRead(`/api/issues/${issueId}/events`, { signal }));
}

export async function addCommentApi(
  issueId: string,
  body: string,
  mentionedUserIds: string[] = [],
  parentId: string | null = null,
  attachments: ResourceInput[] = [],
  commentId?: string,
): Promise<Comment> {
  // Only metadata: slice length, never text.
  trackEvent("comment_added", {
    target: "issue",
    length_bucket: lengthBucket(body),
    is_reply: parentId !== null,
    mention_count: mentionedUserIds.length,
    attachment_count: attachments.length,
  });
  const saved = await parseJson<Comment & { attachment_error?: string }>(
    await fetch(`/api/issues/${issueId}/comments`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        id: commentId,
        body,
        mentioned_user_ids: mentionedUserIds,
        parent_id: parentId,
        attachments,
      }),
    })
  );
  if (saved.attachment_error) throw new Error(saved.attachment_error);
  return saved;
}

export async function fetchObjectiveCommentsApi(objectiveId: string, signal?: AbortSignal): Promise<Comment[]> {
  return parseJson<Comment[]>(await fetchClientRead(`/api/objectives/${objectiveId}/comments`, { signal }));
}

export async function fetchObjectiveEventsApi(objectiveId: string, signal?: AbortSignal): Promise<IssueEvent[]> {
  return parseJson<IssueEvent[]>(await fetchClientRead(`/api/objectives/${objectiveId}/events`, { signal }));
}

export async function fetchFeedbackEventsApi(
  projectId: string,
  postId: string,
  signal?: AbortSignal
): Promise<IssueEvent[]> {
  return parseJson<IssueEvent[]>(
    await fetchClientRead(`/api/projects/${projectId}/feedback/${postId}/events`, { signal })
  );
}

// ── Feedback internal comments (service-client routes, team-only) ──────────
export async function fetchFeedbackCommentsApi(
  projectId: string,
  postId: string,
  signal?: AbortSignal
): Promise<Comment[]> {
  return parseJson<Comment[]>(
    await fetchClientRead(`/api/projects/${projectId}/feedback/${postId}/comments`, { signal })
  );
}

export async function addFeedbackCommentApi(
  projectId: string,
  postId: string,
  body: string,
  mentionedUserIds: string[] = [],
  parentId: string | null = null,
  attachments: ResourceInput[] = [],
  /** 'public' publishes it on the board as the team's response (MIN-196). */
  visibility: CommentVisibility = "internal"
): Promise<Comment> {
  trackEvent("comment_added", {
    target: "feedback",
    length_bucket: lengthBucket(body),
    is_reply: parentId !== null,
    mention_count: mentionedUserIds.length,
    attachment_count: attachments.length,
    visibility,
  });
  return parseJson<Comment>(
    await fetch(`/api/projects/${projectId}/feedback/${postId}/comments`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        body,
        mentioned_user_ids: mentionedUserIds,
        parent_id: parentId,
        attachments,
        visibility,
      }),
    })
  );
}

export async function updateFeedbackCommentApi(
  projectId: string,
  postId: string,
  commentId: string,
  body: string
): Promise<Comment> {
  return parseJson<Comment>(
    await fetch(
      `/api/projects/${projectId}/feedback/${postId}/comments/${commentId}`,
      {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ body }),
      }
    )
  );
}

export async function deleteFeedbackCommentApi(
  projectId: string,
  postId: string,
  commentId: string
): Promise<void> {
  const response = await fetch(
    `/api/projects/${projectId}/feedback/${postId}/comments/${commentId}`,
    { method: "DELETE" }
  );
  if (!response.ok) {
    const data = await response.json().catch(() => null);
    throw new Error((data as { error?: string } | null)?.error || "Delete failed");
  }
  trackEvent("comment_deleted", { target: "feedback" });
}

export async function addObjectiveCommentApi(
  objectiveId: string,
  body: string,
  mentionedUserIds: string[] = [],
  parentId: string | null = null,
  attachments: ResourceInput[] = []
): Promise<Comment> {
  trackEvent("comment_added", {
    target: "objective",
    length_bucket: lengthBucket(body),
    is_reply: parentId !== null,
    mention_count: mentionedUserIds.length,
    attachment_count: attachments.length,
  });
  return parseJson<Comment>(
    await fetch(`/api/objectives/${objectiveId}/comments`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        body,
        mentioned_user_ids: mentionedUserIds,
        parent_id: parentId,
        attachments,
      }),
    })
  );
}

/** One resource, file or link — the uploader's own only (checked server-side). */
/** Rename one saved file or link (MIN-742); answers the updated row. */
export async function renameResourceApi(resourceId: string, fileName: string): Promise<Attachment> {
  const saved = await parseJson<Attachment>(
    await fetch(`/api/resources/${resourceId}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ file_name: fileName }),
    })
  );
  // Neither the old nor the new name: only that a resource was renamed.
  trackEvent("resource_renamed", { kind: saved.kind === "link" ? "link" : "file" });
  return saved;
}

export async function deleteResourceApi(resourceId: string): Promise<void> {
  const response = await fetch(`/api/resources/${resourceId}`, {
    method: "DELETE",
  });
  if (!response.ok) {
    const data = await response.json().catch(() => null);
    throw new Error(
      (data as { error?: string } | null)?.error || "Delete failed"
    );
  }
  trackEvent("resource_removed", { target: "issue" });
}

export async function updateCommentApi(commentId: string, body: string): Promise<Comment> {
  return parseJson<Comment>(
    await fetch(`/api/comments/${commentId}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ body }),
    })
  );
}

export async function deleteCommentApi(commentId: string, missingOk = false): Promise<void> {
  const response = await fetch(`/api/comments/${commentId}`, { method: "DELETE" });
  if (missingOk && response.status === 404) return;
  if (!response.ok) {
    const data = await response.json().catch(() => null);
    throw new Error(
      (data as { error?: string } | null)?.error || "Delete failed"
    );
  }
  trackEvent("comment_deleted", { target: "issue" });
}