Объявления: окно у игрока и редактор в админке
Окно — вариант E из макетов: заголовок в плашке-шапке, текст, точки очереди и «Понятно», крестика нет. Очередь идёт от старого к новому; закрытое сразу уходит из кэша, следующее открывается без ожидания сети. У повторно показанного объявления под заголовком — красная пометка «обновлено». AppShell показывает окно на любой странице и только когда пароль задан: обязательное окно пароля всегда первое. Новые и изменённые объявления приходят SSE-событием announcements, отложенное начало показа — перезапросом раз в 5 минут. Админка — вкладка «Объявления» с редактором R1: contenteditable и панель (жирный, курсив, золотой и красный акцент, эмодзи, снятие оформления), вставка и перетаскивание только простым текстом, счётчик символов, период показа по МСК, переключатели «показывать новым игрокам» и «показать заново». Предпросмотр — тем же окном, что у игрока. Список со статусом, счётчиком «закрыли N из M» и действиями: изменить, снять с показа, дублировать, удалить. Выделение текста в форме — полупрозрачный оранжевый вместо синего. schema.d.ts пересобран из OpenAPI. #84 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user