53 lines
1.6 KiB
TypeScript
53 lines
1.6 KiB
TypeScript
// 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<LikeOutcome> {
|
|
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) };
|
|
}
|