// Browser-side like / unlike calls and the pure card update they drive. import type { CardPost } from "./feed-model"; export interface LikeOutcome { isLiked: boolean; likesCount: number | null; } export class LikeError extends Error { constructor(public readonly signedOut: boolean) { super(signedOut ? "sign_in" : "like_failed"); } } export async function sendLike( postId: string, liked: boolean, fetchImpl: typeof fetch = fetch, ): Promise { let res: Response; try { res = await fetchImpl(`/api/posts/${encodeURIComponent(postId)}/like`, { method: liked ? "POST" : "DELETE", credentials: "same-origin", }); } catch { throw new LikeError(false); } if (res.status === 401) throw new LikeError(true); if (!res.ok) throw new LikeError(false); try { const b = (await res.json()) as { isLiked?: unknown; likesCount?: unknown }; return { isLiked: typeof b.isLiked === "boolean" ? b.isLiked : liked, likesCount: typeof b.likesCount === "number" && Number.isInteger(b.likesCount) && b.likesCount >= 0 ? b.likesCount : null, }; } catch { throw new LikeError(false); } } /** The card after a successful call: the server count when present, else flip and adjust by one. */ export function applyLikeOutcome(post: CardPost, wanted: boolean, out: LikeOutcome): CardPost { if (out.likesCount !== null) return { ...post, isLiked: out.isLiked, likesCount: out.likesCount }; const changed = post.isLiked !== wanted; const delta = changed ? (wanted ? 1 : -1) : 0; return { ...post, isLiked: wanted, likesCount: Math.max(0, post.likesCount + delta) }; }