diff --git a/app/(dashboard)/reward-gifts/page.tsx b/app/(dashboard)/reward-gifts/page.tsx index 63266e6..dc54740 100644 --- a/app/(dashboard)/reward-gifts/page.tsx +++ b/app/(dashboard)/reward-gifts/page.tsx @@ -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 (
@@ -101,14 +121,29 @@ export default function RewardGiftsPage() {

{r.name}

- +
+ + {/* 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_") && ( + + )} +
))} @@ -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({
{ const f = e.target.files?.[0]; diff --git a/lib/api.ts b/lib/api.ts index bc399fd..b844d70 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -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(`/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("/admin/reward-gifts/catalog", {