2026-06-04 17:46:09 +08:00
|
|
|
import { apiBase, itemsOrEmpty } from "../api";
|
|
|
|
|
import type { Post } from "../types/post";
|
2026-06-02 00:36:11 +08:00
|
|
|
|
|
|
|
|
export type FavoriteSort = "favorited_at" | "published_at" | "hot";
|
|
|
|
|
|
|
|
|
|
export type FavoriteListResponse = {
|
2026-06-04 17:46:09 +08:00
|
|
|
items: Post[];
|
2026-06-04 17:06:29 +08:00
|
|
|
page?: number;
|
|
|
|
|
limit?: number;
|
|
|
|
|
total?: number;
|
2026-06-02 00:36:11 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type FavoriteIdsResponse = {
|
|
|
|
|
ids: string[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type FavoriteMutationResponse = {
|
|
|
|
|
ok: boolean;
|
2026-06-04 17:06:29 +08:00
|
|
|
changed?: boolean;
|
|
|
|
|
resourceId?: string;
|
|
|
|
|
favorited?: boolean;
|
2026-06-02 00:36:11 +08:00
|
|
|
favoritedAt?: string;
|
2026-06-04 17:06:29 +08:00
|
|
|
favoriteCount?: number;
|
2026-06-02 00:36:11 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function authHeaders(token: string): HeadersInit {
|
|
|
|
|
return { Authorization: `Bearer ${token}` };
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:06:29 +08:00
|
|
|
function authJSONHeaders(token: string): HeadersInit {
|
|
|
|
|
return {
|
|
|
|
|
...authHeaders(token),
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 03:43:13 +08:00
|
|
|
/** HTTP error that preserves the status code so callers can react to 401s. */
|
|
|
|
|
export class FavoriteHttpError extends Error {
|
|
|
|
|
readonly status: number;
|
|
|
|
|
constructor(status: number, message: string) {
|
|
|
|
|
super(message || `Request failed (${status})`);
|
|
|
|
|
this.name = "FavoriteHttpError";
|
|
|
|
|
this.status = status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** True when an error means the wallet session is no longer authorized. */
|
|
|
|
|
export function isFavoritesAuthError(error: unknown): boolean {
|
|
|
|
|
return error instanceof FavoriteHttpError && error.status === 401;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 00:36:11 +08:00
|
|
|
async function parseJSON<T>(res: Response): Promise<T> {
|
2026-06-02 03:43:13 +08:00
|
|
|
if (!res.ok) throw new FavoriteHttpError(res.status, await res.text());
|
2026-06-02 00:36:11 +08:00
|
|
|
return res.json() as Promise<T>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listFavorites(
|
|
|
|
|
token: string,
|
|
|
|
|
params: {
|
|
|
|
|
sort?: FavoriteSort;
|
|
|
|
|
page?: number;
|
|
|
|
|
limit?: number;
|
|
|
|
|
category?: string;
|
|
|
|
|
q?: string;
|
|
|
|
|
includeUnavailable?: boolean;
|
|
|
|
|
lang?: string;
|
|
|
|
|
} = {},
|
|
|
|
|
): Promise<FavoriteListResponse> {
|
|
|
|
|
const sp = new URLSearchParams();
|
|
|
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
|
|
|
if (value === undefined || value === "") return;
|
|
|
|
|
sp.set(key, String(value));
|
|
|
|
|
});
|
|
|
|
|
const suffix = sp.toString() ? `?${sp}` : "";
|
2026-06-04 17:06:29 +08:00
|
|
|
const res = await fetch(`${apiBase}/api/favorites${suffix}`, {
|
2026-06-02 00:36:11 +08:00
|
|
|
headers: authHeaders(token),
|
|
|
|
|
});
|
|
|
|
|
return parseJSON<FavoriteListResponse>(res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getFavoriteIds(
|
|
|
|
|
token: string,
|
|
|
|
|
resourceIds: string[],
|
|
|
|
|
): Promise<string[]> {
|
|
|
|
|
if (resourceIds.length === 0) return [];
|
|
|
|
|
const uniqueIds = [...new Set(resourceIds)].slice(0, 100);
|
|
|
|
|
const res = await fetch(
|
2026-06-04 17:06:29 +08:00
|
|
|
`${apiBase}/api/favorites?ids=${encodeURIComponent(uniqueIds.join(","))}`,
|
2026-06-02 00:36:11 +08:00
|
|
|
{ headers: authHeaders(token) },
|
|
|
|
|
);
|
2026-06-04 17:06:29 +08:00
|
|
|
const data = await parseJSON<FavoriteIdsResponse | FavoriteListResponse>(res);
|
|
|
|
|
if ("ids" in data && Array.isArray(data.ids)) return data.ids;
|
|
|
|
|
if ("items" in data) return itemsOrEmpty(data.items).map((item) => item.id);
|
|
|
|
|
return [];
|
2026-06-02 00:36:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function addFavorite(
|
|
|
|
|
token: string,
|
|
|
|
|
resourceId: string,
|
|
|
|
|
): Promise<FavoriteMutationResponse> {
|
2026-06-04 17:06:29 +08:00
|
|
|
const res = await fetch(`${apiBase}/api/posts/${resourceId}/favorite`, {
|
2026-06-02 00:36:11 +08:00
|
|
|
method: "POST",
|
2026-06-04 17:06:29 +08:00
|
|
|
headers: authJSONHeaders(token),
|
|
|
|
|
body: JSON.stringify({ add: true }),
|
2026-06-02 00:36:11 +08:00
|
|
|
});
|
|
|
|
|
return parseJSON<FavoriteMutationResponse>(res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function removeFavorite(
|
|
|
|
|
token: string,
|
|
|
|
|
resourceId: string,
|
|
|
|
|
): Promise<FavoriteMutationResponse> {
|
2026-06-04 17:06:29 +08:00
|
|
|
const res = await fetch(`${apiBase}/api/posts/${resourceId}/favorite`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: authJSONHeaders(token),
|
|
|
|
|
body: JSON.stringify({ add: false }),
|
2026-06-02 00:36:11 +08:00
|
|
|
});
|
|
|
|
|
return parseJSON<FavoriteMutationResponse>(res);
|
|
|
|
|
}
|