feat: add stickers delete api

This commit is contained in:
gosangam
2026-08-30 10:32:13 +05:30
parent f295f59d0b
commit df9af9dbac
2 changed files with 69 additions and 15 deletions
+63 -15
View File
@@ -54,6 +54,26 @@ export default function RewardGiftsPage() {
}
};
const remove = async (reward: GiftableReward) => {
if (
!window.confirm(
`Delete “${reward.name}”? It disappears from the gallery of everyone who has it. Stickers already sent in chats stay put.`,
)
) {
return;
}
setSending(reward.key);
try {
await rewardGifts.remove(reward.key);
setCatalog((prev) => prev?.filter((c) => c.key !== reward.key) ?? null);
toast.success("Deleted");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Could not delete");
} finally {
setSending(null);
}
};
return (
<div className="space-y-6">
<header>
@@ -101,14 +121,29 @@ export default function RewardGiftsPage() {
<p className="line-clamp-2 text-sm font-medium text-foreground">
{r.name}
</p>
<button
type="button"
disabled={sending !== null}
onClick={() => send(r)}
className="mt-auto w-full rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{sending === r.key ? "Sending…" : "Send to everyone"}
</button>
<div className="mt-auto w-full space-y-1.5">
<button
type="button"
disabled={sending !== null}
onClick={() => send(r)}
className="w-full rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{sending === r.key ? "Sending…" : "Send to everyone"}
</button>
{/* Seeded rewards have no delete: they would return on the
next seed run, having been taken from the galleries of
everyone who earned them. */}
{r.key.startsWith("custom_") && (
<button
type="button"
disabled={sending !== null}
onClick={() => remove(r)}
className="w-full rounded-md border border-border px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted disabled:opacity-50"
>
Delete
</button>
)}
</div>
</li>
))}
</ul>
@@ -138,12 +173,23 @@ function CustomGift({
const upload = async (original: File) => {
setUploading(true);
try {
// Same compression as the banner: artwork renders at ~120px in the
// gallery and small again in a chat bubble, so a full-size original is
// wasted bytes on every devotee's device.
const file = await compressImage(original, { maxDimension: 512 }).catch(
() => original,
);
// WebP, NOT the helper's default JPEG: JPEG has no alpha channel, and
// compress-image paints a white backdrop behind it so transparency
// doesn't turn black. A sticker has to keep its cut-out, so it encodes
// to a format that has alpha — the white fill is skipped for anything
// that isn't JPEG.
//
// 1024px, not the gallery tile's 120px: the reward detail renders the
// artwork at 224 CSS px (rewards-gallery.tsx:184), which is 672 real
// pixels on a 3× screen — so anything smaller is upscaled and visibly
// soft exactly where the artwork is largest. Quality is raised too:
// stickers are flat illustration with hard edges, where WebP's default
// ringing shows far more than it does on a photograph.
const file = await compressImage(original, {
maxDimension: 1024,
quality: 0.95,
mimeType: "image/webp",
}).catch(() => original);
const { upload_url, image_url } = await rewardGifts.presignImage(
file.type,
);
@@ -217,7 +263,9 @@ function CustomGift({
<div className="flex flex-wrap items-center gap-3">
<input
type="file"
accept="image/png,image/jpeg,image/webp"
// No JPEG: it cannot carry transparency, so it can only produce a
// sticker with a hard rectangular edge.
accept="image/png,image/webp"
disabled={uploading}
onChange={(e) => {
const f = e.target.files?.[0];
+6
View File
@@ -459,6 +459,12 @@ export const rewardGifts = {
{ method: "POST", body: JSON.stringify({ content_type: contentType }) },
).then((r) => r.data),
/** Delete a custom sticker. Cascades out of every gallery and vault. */
remove: (key: string) =>
call<void>(`/admin/reward-gifts/${encodeURIComponent(key)}`, {
method: "DELETE",
}),
/** Add a sticker to the catalog. It reaches nobody until it is sent. */
create: (input: { name: string; meaning: string; image_url: string }) =>
call<GiftableReward>("/admin/reward-gifts/catalog", {