feat: add stickers delete api
This commit is contained in:
@@ -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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<header>
|
<header>
|
||||||
@@ -101,14 +121,29 @@ export default function RewardGiftsPage() {
|
|||||||
<p className="line-clamp-2 text-sm font-medium text-foreground">
|
<p className="line-clamp-2 text-sm font-medium text-foreground">
|
||||||
{r.name}
|
{r.name}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<div className="mt-auto w-full space-y-1.5">
|
||||||
type="button"
|
<button
|
||||||
disabled={sending !== null}
|
type="button"
|
||||||
onClick={() => send(r)}
|
disabled={sending !== null}
|
||||||
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"
|
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>
|
{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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -138,12 +173,23 @@ function CustomGift({
|
|||||||
const upload = async (original: File) => {
|
const upload = async (original: File) => {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
try {
|
try {
|
||||||
// Same compression as the banner: artwork renders at ~120px in the
|
// WebP, NOT the helper's default JPEG: JPEG has no alpha channel, and
|
||||||
// gallery and small again in a chat bubble, so a full-size original is
|
// compress-image paints a white backdrop behind it so transparency
|
||||||
// wasted bytes on every devotee's device.
|
// doesn't turn black. A sticker has to keep its cut-out, so it encodes
|
||||||
const file = await compressImage(original, { maxDimension: 512 }).catch(
|
// to a format that has alpha — the white fill is skipped for anything
|
||||||
() => original,
|
// 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(
|
const { upload_url, image_url } = await rewardGifts.presignImage(
|
||||||
file.type,
|
file.type,
|
||||||
);
|
);
|
||||||
@@ -217,7 +263,9 @@ function CustomGift({
|
|||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<input
|
<input
|
||||||
type="file"
|
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}
|
disabled={uploading}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const f = e.target.files?.[0];
|
const f = e.target.files?.[0];
|
||||||
|
|||||||
@@ -459,6 +459,12 @@ export const rewardGifts = {
|
|||||||
{ method: "POST", body: JSON.stringify({ content_type: contentType }) },
|
{ method: "POST", body: JSON.stringify({ content_type: contentType }) },
|
||||||
).then((r) => r.data),
|
).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. */
|
/** Add a sticker to the catalog. It reaches nobody until it is sent. */
|
||||||
create: (input: { name: string; meaning: string; image_url: string }) =>
|
create: (input: { name: string; meaning: string; image_url: string }) =>
|
||||||
call<GiftableReward>("/admin/reward-gifts/catalog", {
|
call<GiftableReward>("/admin/reward-gifts/catalog", {
|
||||||
|
|||||||
Reference in New Issue
Block a user