feat(gallery-reactions): ✨ Add gallery reactions API client with types and methods for managing reactions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
85135a6507
commit
7f969e8fbd
3 changed files with 93 additions and 0 deletions
|
|
@ -0,0 +1,57 @@
|
|||
import { resolveBaseUrl } from '../base-url';
|
||||
import {
|
||||
GALLERY_REACTION_MAX_DELTA,
|
||||
type GalleryReactionMap,
|
||||
type GalleryReactionPayload,
|
||||
} from '../types/gallery-reaction';
|
||||
|
||||
/** Fetch all reaction counts for a provider's gallery. Uncached. */
|
||||
export async function fetchGalleryReactions(provider = 'quinn'): Promise<GalleryReactionMap> {
|
||||
const url = `${resolveBaseUrl()}/www/gallery-reactions?provider=${encodeURIComponent(provider)}`;
|
||||
try {
|
||||
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) return {};
|
||||
const body = (await res.json()) as { reactions?: GalleryReactionMap };
|
||||
return body.reactions ?? {};
|
||||
} catch {
|
||||
// The counter is non-critical decoration — never let it break the gallery.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function postReaction(url: string, provider: string, src: string, emoji: string, delta: number): void {
|
||||
const body = JSON.stringify({ provider, src, emoji, delta });
|
||||
if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
|
||||
const blob = new Blob([body], { type: 'application/json' });
|
||||
if (navigator.sendBeacon(url, blob)) return;
|
||||
}
|
||||
void fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
keepalive: true,
|
||||
}).catch(() => {
|
||||
// fire-and-forget
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a batched reaction delta. Fire-and-forget — the UI already incremented
|
||||
* optimistically, and a dropped tap on a public click-counter is non-fatal.
|
||||
*
|
||||
* A fast tapper can accumulate a delta larger than the server's per-request cap
|
||||
* (GALLERY_REACTION_MAX_DELTA); we chunk it into capped pieces so every tap is
|
||||
* counted server-side rather than rejected wholesale. Prefers sendBeacon so
|
||||
* flushes survive page unload (mobile tab close / navigation).
|
||||
*/
|
||||
export function submitGalleryReaction(payload: GalleryReactionPayload): void {
|
||||
const url = `${resolveBaseUrl()}/www/gallery-reactions`;
|
||||
const provider = payload.provider ?? 'quinn';
|
||||
let remaining = Math.floor(payload.delta);
|
||||
if (!Number.isFinite(remaining) || remaining < 1) return;
|
||||
while (remaining > 0) {
|
||||
const chunk = Math.min(remaining, GALLERY_REACTION_MAX_DELTA);
|
||||
postReaction(url, provider, payload.src, payload.emoji, chunk);
|
||||
remaining -= chunk;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,3 +39,11 @@ export {
|
|||
fetchPseoTerm,
|
||||
fetchPseoHobbyTerms,
|
||||
} from './endpoints/pseo';
|
||||
export type {
|
||||
GalleryReactionEmoji,
|
||||
GalleryReactionCounts,
|
||||
GalleryReactionMap,
|
||||
GalleryReactionPayload,
|
||||
} from './types/gallery-reaction';
|
||||
export { GALLERY_REACTION_EMOJIS, GALLERY_REACTION_MAX_DELTA } from './types/gallery-reaction';
|
||||
export { fetchGalleryReactions, submitGalleryReaction } from './endpoints/gallery-reactions';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Gallery reactions — anonymous, unlimited emoji "click counter" per photo.
|
||||
* Mirror of the server allowlist in quinn-api (entities/gallery-reaction/types.ts);
|
||||
* keep the palette and max-delta in sync.
|
||||
*/
|
||||
|
||||
export const GALLERY_REACTION_EMOJIS = ['🔥', '😍', '💋', '🥵', '👅', '🍑', '🍆', '💦', '😈'] as const;
|
||||
export type GalleryReactionEmoji = (typeof GALLERY_REACTION_EMOJIS)[number];
|
||||
|
||||
/**
|
||||
* Largest delta a single POST may carry (server rejects more). The client chunks
|
||||
* accumulated taps into pieces no larger than this so a fast tapper never loses
|
||||
* counts to the server-side clamp.
|
||||
*/
|
||||
export const GALLERY_REACTION_MAX_DELTA = 50;
|
||||
|
||||
/** emoji → total taps for one photo. */
|
||||
export type GalleryReactionCounts = Partial<Record<GalleryReactionEmoji, number>>;
|
||||
|
||||
/** Photo src (DB src / reactionKey) → counts. */
|
||||
export type GalleryReactionMap = Record<string, GalleryReactionCounts>;
|
||||
|
||||
export interface GalleryReactionPayload {
|
||||
readonly src: string;
|
||||
readonly emoji: GalleryReactionEmoji;
|
||||
readonly delta: number;
|
||||
readonly provider?: string;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue