"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(response: Response): Promise { 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 { return parseJson(await fetchClientRead(`/api/issues/${issueId}/comments`, { signal })); } export async function fetchEventsApi(issueId: string, signal?: AbortSignal): Promise { return parseJson(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 { // 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( 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 { return parseJson(await fetchClientRead(`/api/objectives/${objectiveId}/comments`, { signal })); } export async function fetchObjectiveEventsApi(objectiveId: string, signal?: AbortSignal): Promise { return parseJson(await fetchClientRead(`/api/objectives/${objectiveId}/events`, { signal })); } export async function fetchFeedbackEventsApi( projectId: string, postId: string, signal?: AbortSignal ): Promise { return parseJson( 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 { return parseJson( 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 { trackEvent("comment_added", { target: "feedback", length_bucket: lengthBucket(body), is_reply: parentId !== null, mention_count: mentionedUserIds.length, attachment_count: attachments.length, visibility, }); return parseJson( 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 { return parseJson( 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 { 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 { trackEvent("comment_added", { target: "objective", length_bucket: lengthBucket(body), is_reply: parentId !== null, mention_count: mentionedUserIds.length, attachment_count: attachments.length, }); return parseJson( 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 { const saved = await parseJson( 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 { 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 { return parseJson( 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 { 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" }); }