Окно — вариант E из макетов: заголовок в плашке-шапке, текст, точки очереди и «Понятно», крестика нет. Очередь идёт от старого к новому; закрытое сразу уходит из кэша, следующее открывается без ожидания сети. У повторно показанного объявления под заголовком — красная пометка «обновлено». AppShell показывает окно на любой странице и только когда пароль задан: обязательное окно пароля всегда первое. Новые и изменённые объявления приходят SSE-событием announcements, отложенное начало показа — перезапросом раз в 5 минут. Админка — вкладка «Объявления» с редактором R1: contenteditable и панель (жирный, курсив, золотой и красный акцент, эмодзи, снятие оформления), вставка и перетаскивание только простым текстом, счётчик символов, период показа по МСК, переключатели «показывать новым игрокам» и «показать заново». Предпросмотр — тем же окном, что у игрока. Список со статусом, счётчиком «закрыли N из M» и действиями: изменить, снять с показа, дублировать, удалить. Выделение текста в форме — полупрозрачный оранжевый вместо синего. schema.d.ts пересобран из OpenAPI. #84 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
import { useId } from "react";
|
||
|
||
/**
|
||
* Окно объявления администрации (вариант E из #84): заголовок — в плашке-шапке, ниже текст,
|
||
* точки очереди и «Понятно». Крестика нет намеренно: закрытие — только осознанное.
|
||
*
|
||
* bodyHtml у игрока — HTML, очищенный сервером по белому списку (announcement_service),
|
||
* поэтому вставляется как есть. В предпросмотре админки — содержимое его же редактора.
|
||
*/
|
||
export function AnnouncementWindow({
|
||
title,
|
||
bodyHtml,
|
||
updated = false,
|
||
index = 0,
|
||
total = 1,
|
||
onOk,
|
||
}: {
|
||
title: string;
|
||
bodyHtml: string;
|
||
updated?: boolean;
|
||
index?: number;
|
||
total?: number;
|
||
onOk: () => void;
|
||
}) {
|
||
const titleId = useId();
|
||
return (
|
||
<div className="modal-overlay">
|
||
<div className="modal ann-modal" role="dialog" aria-modal="true" aria-labelledby={titleId}>
|
||
<div className="ann-ribbon">
|
||
<div className="ann-ribbon-title" id={titleId}>
|
||
{title}
|
||
</div>
|
||
{updated && <div className="ann-updated">обновлено</div>}
|
||
</div>
|
||
<div className="ann-body" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
|
||
<div className="modal-actions ann-actions">
|
||
<div className="ann-dots" aria-hidden="true">
|
||
{total > 1 &&
|
||
Array.from({ length: total }, (_, k) => (
|
||
<i key={k} className={k <= index ? "on" : undefined} />
|
||
))}
|
||
</div>
|
||
<button className="btn btn-primary" onClick={onOk} autoFocus>
|
||
Понятно
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|