Files
fluxer/packages/admin/src/pages/guild_detail/tabs/StickersTab.tsx
2026-02-17 12:22:36 +00:00

161 lines
5.2 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {hasPermission} from '@fluxer/admin/src/AccessControlList';
import {getErrorMessage} from '@fluxer/admin/src/api/Errors';
import {listGuildStickers} from '@fluxer/admin/src/api/GuildAssets';
import {ErrorCard} from '@fluxer/admin/src/components/ErrorDisplay';
import {Badge} from '@fluxer/admin/src/components/ui/Badge';
import {HStack} from '@fluxer/admin/src/components/ui/Layout/HStack';
import {VStack} from '@fluxer/admin/src/components/ui/Layout/VStack';
import {Stack} from '@fluxer/admin/src/components/ui/Stack';
import {Caption, Heading, Text} from '@fluxer/admin/src/components/ui/Typography';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
import type {GuildStickerAsset} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import {Button} from '@fluxer/ui/src/components/Button';
import {Card} from '@fluxer/ui/src/components/Card';
import {CsrfInput} from '@fluxer/ui/src/components/CsrfInput';
import type {FC} from 'hono/jsx';
interface StickersTabProps {
config: Config;
session: Session;
guildId: string;
adminAcls: Array<string>;
csrfToken: string;
}
function stickerAnimatedLabel(animated: boolean): string {
return animated ? 'Animated' : 'Static';
}
const RenderPermissionNotice: FC = () => (
<Card padding="md">
<Stack gap="md">
<Heading level={2} size="base">
Permission required
</Heading>
<Text size="sm" color="muted">
You need the {AdminACLs.ASSET_PURGE} ACL to manage guild stickers.
</Text>
</Stack>
</Card>
);
const RenderStickerCard: FC<{config: Config; guildId: string; sticker: GuildStickerAsset; csrfToken: string}> = ({
config,
guildId,
sticker,
csrfToken,
}) => {
return (
<Card padding="none" class="overflow-hidden shadow-sm">
<VStack gap={0}>
<VStack gap={0} class="h-32 items-center justify-center bg-neutral-100 p-6">
<img src={sticker.media_url} alt={sticker.name} class="max-h-full max-w-full object-contain" loading="lazy" />
</VStack>
<VStack gap={1} class="flex-1 px-4 py-3">
<HStack gap={2} justify="between" align="center">
<Text size="sm" weight="semibold">
{sticker.name}
</Text>
<Badge size="sm" variant="neutral">
{stickerAnimatedLabel(sticker.animated)}
</Badge>
</HStack>
<Caption class="break-words">ID: {sticker.id}</Caption>
<a href={`${config.basePath}/users/${sticker.creator_id}`} class="text-blue-600 text-xs hover:underline">
Uploader: {sticker.creator_id}
</a>
<form
action={`${config.basePath}/guilds/${guildId}?tab=stickers&action=delete_sticker`}
method="post"
class="mt-4"
>
<CsrfInput token={csrfToken} />
<input type="hidden" name="sticker_id" value={sticker.id} />
<Button type="submit" variant="danger" size="small" fullWidth>
Delete Sticker
</Button>
</form>
</VStack>
</VStack>
</Card>
);
};
const RenderStickers: FC<{config: Config; guildId: string; stickers: Array<GuildStickerAsset>; csrfToken: string}> = ({
config,
guildId,
stickers,
csrfToken,
}) => {
return (
<Card padding="md">
<Stack gap="md">
<Heading level={2} size="base">
Stickers ({stickers.length})
</Heading>
{stickers.length === 0 ? (
<Text size="sm" color="muted">
No stickers found for this guild.
</Text>
) : (
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{stickers.map((sticker) => (
<RenderStickerCard config={config} guildId={guildId} sticker={sticker} csrfToken={csrfToken} />
))}
</div>
)}
</Stack>
</Card>
);
};
export async function StickersTab({config, session, guildId, adminAcls, csrfToken}: StickersTabProps) {
const hasAssetPurge = hasPermission(adminAcls, AdminACLs.ASSET_PURGE);
if (!hasAssetPurge) {
return <RenderPermissionNotice />;
}
const result = await listGuildStickers(config, session, guildId);
if (!result.ok) {
return (
<VStack gap={4}>
<ErrorCard title="Error" message={getErrorMessage(result.error)} />
<a
href={`${config.basePath}/guilds/${guildId}?tab=stickers`}
class="inline-block rounded bg-neutral-900 px-4 py-2 font-medium text-sm text-white transition-colors hover:bg-neutral-800"
>
Back to Guild
</a>
</VStack>
);
}
return <RenderStickers config={config} guildId={guildId} stickers={result.data.stickers} csrfToken={csrfToken} />;
}