mirror of
https://github.com/owncast/owncast.git
synced 2025-11-01 10:55:57 +08:00
* Initial plan * Add localization support to admin form status and error messages Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Format updated files with prettier Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Replace t() with Translation component in admin page JSX Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * update package-lock.json * Update web/i18n/en/translation.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gabek <414923+gabek@users.noreply.github.com> Co-authored-by: Gabe Kangas <gabek@real-ity.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
230 lines
6.6 KiB
TypeScript
230 lines
6.6 KiB
TypeScript
import { Avatar, Button, Card, Col, Row, Tooltip, Typography } from 'antd';
|
|
import Upload, { RcFile } from 'antd/lib/upload';
|
|
import React, { ReactElement, useEffect, useState } from 'react';
|
|
import dynamic from 'next/dynamic';
|
|
import { useTranslation } from 'next-export-i18n';
|
|
import FormStatusIndicator from '../../../components/admin/FormStatusIndicator';
|
|
import { DELETE_EMOJI, fetchData, UPLOAD_EMOJI } from '../../../utils/apis';
|
|
import { ACCEPTED_IMAGE_TYPES, getBase64 } from '../../../utils/images';
|
|
import {
|
|
createInputStatus,
|
|
STATUS_ERROR,
|
|
STATUS_PROCESSING,
|
|
STATUS_SUCCESS,
|
|
} from '../../../utils/input-statuses';
|
|
import { RESET_TIMEOUT } from '../../../utils/config-constants';
|
|
import { AdminLayout } from '../../../components/layouts/AdminLayout';
|
|
import { Translation } from '../../../components/ui/Translation/Translation';
|
|
import { Localization } from '../../../types/localization';
|
|
|
|
const URL_CUSTOM_EMOJIS = `/api/emoji`;
|
|
|
|
const { Meta } = Card;
|
|
// Lazy loaded components
|
|
|
|
const CloseOutlined = dynamic(() => import('@ant-design/icons/CloseOutlined'), {
|
|
ssr: false,
|
|
});
|
|
|
|
type CustomEmoji = {
|
|
name: string;
|
|
url: string;
|
|
};
|
|
|
|
const { Title, Paragraph } = Typography;
|
|
|
|
const Emoji = () => {
|
|
const [emojis, setEmojis] = useState<CustomEmoji[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [submitStatus, setSubmitStatus] = useState(null);
|
|
const [uploadFile, setUploadFile] = useState<RcFile>(null);
|
|
const { t } = useTranslation();
|
|
|
|
let resetTimer = null;
|
|
const resetStates = () => {
|
|
setSubmitStatus(null);
|
|
clearTimeout(resetTimer);
|
|
resetTimer = null;
|
|
};
|
|
|
|
async function getEmojis() {
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetchData(URL_CUSTOM_EMOJIS);
|
|
setEmojis(response);
|
|
} catch (error) {
|
|
console.error('error fetching emojis', error);
|
|
}
|
|
setLoading(false);
|
|
}
|
|
useEffect(() => {
|
|
getEmojis();
|
|
}, []);
|
|
|
|
async function handleDelete(fullPath: string) {
|
|
const name = `/${fullPath.split('/').slice(3).join('/')}`;
|
|
console.log(name);
|
|
|
|
setLoading(true);
|
|
|
|
setSubmitStatus(
|
|
createInputStatus(STATUS_PROCESSING, t(Localization.Admin.StatusMessages.deletingEmoji)),
|
|
);
|
|
|
|
try {
|
|
const response = await fetchData(DELETE_EMOJI, {
|
|
method: 'POST',
|
|
data: { name },
|
|
});
|
|
|
|
if (response instanceof Error) {
|
|
throw response;
|
|
}
|
|
|
|
setSubmitStatus(
|
|
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.emojiDeleted)),
|
|
);
|
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
|
} catch (error) {
|
|
setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`));
|
|
setLoading(false);
|
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
|
}
|
|
|
|
getEmojis();
|
|
}
|
|
|
|
async function handleEmojiUpload() {
|
|
setLoading(true);
|
|
try {
|
|
setSubmitStatus(
|
|
createInputStatus(STATUS_PROCESSING, t(Localization.Admin.StatusMessages.convertingEmoji)),
|
|
);
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
const emojiData = await new Promise<CustomEmoji>((res, rej) => {
|
|
if (!ACCEPTED_IMAGE_TYPES.includes(uploadFile.type)) {
|
|
const msg = `File type is not supported: ${uploadFile.type}`;
|
|
// eslint-disable-next-line no-promise-executor-return
|
|
return rej(msg);
|
|
}
|
|
|
|
getBase64(uploadFile, (url: string) =>
|
|
res({
|
|
name: uploadFile.name,
|
|
url,
|
|
}),
|
|
);
|
|
});
|
|
|
|
setSubmitStatus(
|
|
createInputStatus(STATUS_PROCESSING, t(Localization.Admin.StatusMessages.uploadingEmoji)),
|
|
);
|
|
|
|
const response = await fetchData(UPLOAD_EMOJI, {
|
|
method: 'POST',
|
|
data: {
|
|
name: emojiData.name,
|
|
data: emojiData.url,
|
|
},
|
|
});
|
|
|
|
if (response instanceof Error) {
|
|
throw response;
|
|
}
|
|
|
|
setSubmitStatus(
|
|
createInputStatus(
|
|
STATUS_SUCCESS,
|
|
t(Localization.Admin.StatusMessages.emojiUploadedSuccessfully),
|
|
),
|
|
);
|
|
getEmojis();
|
|
} catch (error) {
|
|
setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`));
|
|
}
|
|
|
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
|
setLoading(false);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<Title>
|
|
<Translation translationKey={Localization.Admin.emojis} />
|
|
</Title>
|
|
<Paragraph>
|
|
<Translation translationKey={Localization.Admin.emojiPageDescription} />
|
|
</Paragraph>
|
|
<Paragraph>
|
|
<Translation translationKey={Localization.Admin.emojiUploadBulkGuide} />
|
|
</Paragraph>
|
|
<br />
|
|
<Upload
|
|
name="emoji"
|
|
listType="picture"
|
|
className="emoji-uploader"
|
|
showUploadList={false}
|
|
accept={ACCEPTED_IMAGE_TYPES.join(',')}
|
|
beforeUpload={setUploadFile}
|
|
customRequest={handleEmojiUpload}
|
|
disabled={loading}
|
|
>
|
|
<Button type="primary" disabled={loading}>
|
|
<Translation translationKey={Localization.Admin.uploadNewEmoji} />
|
|
</Button>
|
|
</Upload>
|
|
<FormStatusIndicator status={submitStatus} />
|
|
<br />
|
|
<Row>
|
|
{emojis.map(record => (
|
|
<Col style={{ padding: '10px' }} key={record.name}>
|
|
<Card style={{ width: 120, marginTop: 16 }} actions={[]}>
|
|
<Meta
|
|
description={[
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyItems: 'center',
|
|
alignItems: 'center',
|
|
flexDirection: 'column',
|
|
gap: '20px',
|
|
}}
|
|
>
|
|
<Tooltip title={record.name}>
|
|
<Avatar style={{ height: 50, width: 50 }} src={record.url} />
|
|
</Tooltip>
|
|
<Button
|
|
size="small"
|
|
type="ghost"
|
|
title={t(Localization.Admin.deleteEmoji)}
|
|
style={{
|
|
position: 'absolute',
|
|
right: 0,
|
|
top: 0,
|
|
height: 24,
|
|
width: 24,
|
|
border: 'none',
|
|
color: 'gray',
|
|
}}
|
|
onClick={() => handleDelete(record.url)}
|
|
icon={<CloseOutlined />}
|
|
/>
|
|
</div>,
|
|
]}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
<br />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
Emoji.getLayout = function getLayout(page: ReactElement) {
|
|
return <AdminLayout page={page} />;
|
|
};
|
|
|
|
export default Emoji;
|