lib/comments-api.ts
Rama main · réplica de lectura del repositorio original.
"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" });
}