Объявления: окно у игрока и редактор в админке
Окно — вариант 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:
@@ -2,7 +2,12 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { api, authProbeRetry, unwrap } from "../api/client";
|
||||
import { qk } from "../api/queryKeys";
|
||||
import type { AdminMe, MatchUpdate } from "../domain/types";
|
||||
import type {
|
||||
AdminMe,
|
||||
AnnouncementUpdate,
|
||||
AnnouncementWrite,
|
||||
MatchUpdate,
|
||||
} from "../domain/types";
|
||||
import { uploadFile } from "../lib/upload";
|
||||
|
||||
export function useAdminMe() {
|
||||
@@ -282,3 +287,57 @@ export function useUploadAchievementIcon() {
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminAchievements }),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Объявления ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function useAdminAnnouncements() {
|
||||
return useQuery({
|
||||
queryKey: qk.adminAnnouncements,
|
||||
queryFn: async () => unwrap(await api.GET("/api/admin/announcements")),
|
||||
});
|
||||
}
|
||||
|
||||
function useAnnouncementMutation<A, R>(fn: (args: A) => Promise<R>) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: fn,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminAnnouncements }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useAdminCreateAnnouncement() {
|
||||
return useAnnouncementMutation(async (body: AnnouncementWrite) =>
|
||||
unwrap(await api.POST("/api/admin/announcements", { body })),
|
||||
);
|
||||
}
|
||||
|
||||
export function useAdminUpdateAnnouncement() {
|
||||
return useAnnouncementMutation(async (args: { id: number; body: AnnouncementUpdate }) =>
|
||||
unwrap(
|
||||
await api.PUT("/api/admin/announcements/{announcement_id}", {
|
||||
params: { path: { announcement_id: args.id } },
|
||||
body: args.body,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function useAdminStopAnnouncement() {
|
||||
return useAnnouncementMutation(async (id: number) =>
|
||||
unwrap(
|
||||
await api.POST("/api/admin/announcements/{announcement_id}/stop", {
|
||||
params: { path: { announcement_id: id } },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function useAdminDeleteAnnouncement() {
|
||||
return useAnnouncementMutation(async (id: number) =>
|
||||
unwrap(
|
||||
await api.DELETE("/api/admin/announcements/{announcement_id}", {
|
||||
params: { path: { announcement_id: id } },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user