Первоначальная версия VidConf
This commit is contained in:
237
frontend/src/api/conferences.ts
Normal file
237
frontend/src/api/conferences.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* API-функции конференций: предустановленных комнат нет, конференции
|
||||
* создаются динамически.
|
||||
*
|
||||
* Все даты — UTC ISO-8601 со 'Z' (в БД и API только UTC).
|
||||
*/
|
||||
import { apiRequest } from '@/api/client'
|
||||
|
||||
/** Жизненный цикл конференции (см. ADR-001; `draft` не используется). */
|
||||
export type ConferenceStatus = 'scheduled' | 'active' | 'ended'
|
||||
|
||||
/** Тип повторения закреплённой конференции. */
|
||||
export type RecurrenceType = 'weekly' | 'biweekly' | 'monthly' | 'every_n_days'
|
||||
|
||||
/**
|
||||
* Режим рассылки саммари конференции — переопределение дефолта
|
||||
* инстанса (`null` = «по умолчанию», см. `SettingsOut.summary_recipients`
|
||||
* в `src/api/admin.ts`).
|
||||
*/
|
||||
export type SummaryRecipientsMode = 'all' | 'owner'
|
||||
|
||||
/**
|
||||
* Правило повторения закреплённой конференции — форма 1:1 с pydantic-моделью
|
||||
* `backend/services/recurrence.py::RecurrenceRule` (истина о форме — там).
|
||||
* Поля, специфичные для типа (`weekdays` для weekly/biweekly, `day_of_month`
|
||||
* для monthly, `interval_days` для every_n_days), обязательны только для
|
||||
* своего типа — см. валидацию на backend.
|
||||
*/
|
||||
export interface ConferenceRecurrence {
|
||||
type: RecurrenceType
|
||||
/** 0 (понедельник) .. 6 (воскресенье) — обязателен для weekly/biweekly. */
|
||||
weekdays?: number[]
|
||||
/** 1..31 — обязателен для monthly (переносится на последний день короткого месяца). */
|
||||
day_of_month?: number
|
||||
/** >= 1 — обязателен для every_n_days. */
|
||||
interval_days?: number
|
||||
/** Дата первого вхождения серии (YYYY-MM-DD) — якорь отсчёта для biweekly/every_n_days. */
|
||||
anchor_date: string
|
||||
/** Локальное время начала вхождения, формат HH:MM. */
|
||||
time_local: string
|
||||
/** IANA-таймзона (браузера пользователя на момент создания/редактирования). */
|
||||
timezone: string
|
||||
/** Длительность вхождения в минутах — дублирует верхнеуровневое поле конференции (того требует модель RecurrenceRule). */
|
||||
duration_minutes: number
|
||||
}
|
||||
|
||||
/** Данные для входа в LiveKit-комнату конференции. */
|
||||
export interface ConferenceJoinData {
|
||||
livekit_url: string
|
||||
token: string
|
||||
room_name: string
|
||||
conference_id: string
|
||||
/** Включён ли чат для этой конференции — при `false` панель/кнопка чата не рендерятся. */
|
||||
chat_enabled: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Приглашённый участник конференции — либо
|
||||
* зарегистрированный пользователь (`user_id` заполнен), либо внешний гость
|
||||
* по email (`user_id === null`). Организатор всегда присутствует первым
|
||||
* элементом (`is_organizer: true`), добавляется backend'ом автоматически.
|
||||
*/
|
||||
export interface InviteeOut {
|
||||
user_id: string | null
|
||||
email: string
|
||||
name: string | null
|
||||
avatar_url: string | null
|
||||
is_organizer: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Приглашаемый участник в теле создания/обновления — либо `user_id`
|
||||
* зарегистрированного пользователя, либо `email` внешнего гостя (ровно одно
|
||||
* из полей). Организатора указывать не нужно — backend добавляет его
|
||||
* автоматически и обязательно.
|
||||
*/
|
||||
export type InviteeIn = { user_id: string } | { email: string }
|
||||
|
||||
/** Конференция (ответ API). */
|
||||
export interface ConferenceOut {
|
||||
id: string
|
||||
number: string
|
||||
slug: string
|
||||
title: string | null
|
||||
status: ConferenceStatus
|
||||
is_pinned: boolean
|
||||
is_closed: boolean
|
||||
scheduled_at: string | null
|
||||
duration_minutes: number | null
|
||||
recurrence: ConferenceRecurrence | null
|
||||
next_occurrence: string | null
|
||||
created_at: string
|
||||
/** `null` — используется дефолт инстанса (см. `SettingsOut.summary_recipients`). */
|
||||
summary_recipients: SummaryRecipientsMode | null
|
||||
/** Присутствует только у мгновенной конференции — сразу входим, не дожидаясь отдельного join. */
|
||||
join?: ConferenceJoinData
|
||||
/** Владелец (организатор) конференции. */
|
||||
owner_id: string
|
||||
/** Организатор ли конференции текущий пользователь — относительно него же скрываются кнопки правки/удаления. */
|
||||
is_owner: boolean
|
||||
organizer_name: string | null
|
||||
/** Присутствует ТОЛЬКО в ответе детального эндпоинта (`GET /conferences/{id}`, owner|admin) — списковые эндпоинты (`/conferences/my`, календарь) его не возвращают. */
|
||||
participants?: InviteeOut[]
|
||||
}
|
||||
|
||||
/** Тело запроса на создание конференции. Без `scheduled_at` — мгновенная (ответ сразу содержит `join`). */
|
||||
export interface ConferenceCreatePayload {
|
||||
title?: string
|
||||
/** UTC ISO-8601. */
|
||||
scheduled_at?: string
|
||||
duration_minutes?: number
|
||||
is_pinned?: boolean
|
||||
recurrence?: ConferenceRecurrence
|
||||
is_closed?: boolean
|
||||
password?: string
|
||||
/** `null`/не передано — дефолт инстанса; `'all'`/`'owner'` — явное переопределение. */
|
||||
summary_recipients?: SummaryRecipientsMode | null
|
||||
/** `undefined` — состав не менялся (не отправлять поле); `null`/массив — полная замена состава (кроме организатора, добавляется backend'ом сам). */
|
||||
participants?: InviteeIn[] | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Краткие сведения о конференции по ссылке/номеру — для экрана подключения
|
||||
* (JoinPage). Для `status === 'ended'` (ADR-001) backend отдаёт минимальный
|
||||
* ответ — `is_closed`/`requires_password` отсутствуют (незачем: повторный
|
||||
* вход недоступен независимо от закрытости).
|
||||
*/
|
||||
export interface ConferenceResolveOut {
|
||||
id: string
|
||||
title: string | null
|
||||
status: ConferenceStatus
|
||||
is_closed?: boolean
|
||||
requires_password?: boolean
|
||||
}
|
||||
|
||||
/** Тело входа авторизованного пользователя в конференцию. */
|
||||
export interface ConferenceJoinPayload {
|
||||
password?: string
|
||||
}
|
||||
|
||||
/** Тело гостевого входа — представление обязательно, email факультативен (для рассылки саммари). */
|
||||
export interface ConferenceGuestJoinPayload {
|
||||
display_name: string
|
||||
email?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
/** Тело частичного обновления конференции — те же поля, что и при создании, все опциональны. */
|
||||
export type ConferenceUpdatePayload = Partial<ConferenceCreatePayload>
|
||||
|
||||
/** Одно вхождение (развёрнутое по recurrence или разовое) в календарной выборке. */
|
||||
export interface OccurrenceOut {
|
||||
conference_id: string
|
||||
title: string | null
|
||||
/** UTC ISO-8601. */
|
||||
starts_at: string
|
||||
/** UTC ISO-8601. */
|
||||
ends_at: string
|
||||
number: string
|
||||
slug: string
|
||||
is_pinned: boolean
|
||||
is_closed: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать конференцию. Без `scheduled_at` тела — мгновенная: создатель сразу
|
||||
* входит (см. `ConferenceOut.join`).
|
||||
*/
|
||||
export async function createConference(payload: ConferenceCreatePayload = {}): Promise<ConferenceOut> {
|
||||
return apiRequest<ConferenceOut>('/conferences', { method: 'POST', body: payload })
|
||||
}
|
||||
|
||||
/**
|
||||
* Резолв конференции по ссылке (slug) или номеру — публичный эндпоинт,
|
||||
* доступен без авторизации (нужен и гостю до входа). 404 — не найдена/недоступна
|
||||
* (в т.ч. незакреплённая, уже завершившаяся).
|
||||
*/
|
||||
export async function resolveConference(query: string): Promise<ConferenceResolveOut> {
|
||||
const params = new URLSearchParams({ q: query })
|
||||
return apiRequest<ConferenceResolveOut>(`/conferences/resolve?${params.toString()}`, {
|
||||
skipAuthRefresh: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Вход авторизованного пользователя в конференцию.
|
||||
* 403 (`password_required`/`invalid_password`), 410 (`conference_ended`).
|
||||
*/
|
||||
export async function joinConference(id: string, payload?: ConferenceJoinPayload): Promise<ConferenceJoinData> {
|
||||
return apiRequest<ConferenceJoinData>(`/conferences/${id}/join`, { method: 'POST', body: payload })
|
||||
}
|
||||
|
||||
/** Гостевой вход без аккаунта — те же коды ошибок, что и `joinConference`. */
|
||||
export async function guestJoinConference(
|
||||
id: string,
|
||||
payload: ConferenceGuestJoinPayload,
|
||||
): Promise<ConferenceJoinData> {
|
||||
return apiRequest<ConferenceJoinData>(`/conferences/${id}/guest-join`, {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
skipAuthRefresh: true,
|
||||
})
|
||||
}
|
||||
|
||||
/** Список «моих» конференций — закреплённые (повторяющиеся) и предстоящие разовые владельца. */
|
||||
export async function getMyConferences(): Promise<ConferenceOut[]> {
|
||||
return apiRequest<ConferenceOut[]>('/conferences/my')
|
||||
}
|
||||
|
||||
/**
|
||||
* Вхождения конференций в календарном диапазоне `[from, to]` (обе границы —
|
||||
* UTC ISO, диапазон не длиннее 62 дней — ограничение backend).
|
||||
*/
|
||||
export async function getCalendarOccurrences(fromIso: string, toIso: string): Promise<OccurrenceOut[]> {
|
||||
const params = new URLSearchParams({ from: fromIso, to: toIso })
|
||||
return apiRequest<OccurrenceOut[]>(`/conferences/calendar?${params.toString()}`)
|
||||
}
|
||||
|
||||
/** Частично обновить конференцию (название, время, повторение, закрытость и т.п.). */
|
||||
export async function updateConference(id: string, payload: ConferenceUpdatePayload): Promise<ConferenceOut> {
|
||||
return apiRequest<ConferenceOut>(`/conferences/${id}`, { method: 'PATCH', body: payload })
|
||||
}
|
||||
|
||||
/** Удалить конференцию навсегда. 409 — конференция сейчас идёт (`status: 'active'`). */
|
||||
export async function deleteConference(id: string): Promise<void> {
|
||||
await apiRequest(`/conferences/${id}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Детали одной конференции с полным составом участников (`participants`).
|
||||
* Доступно только владельцу или admin — иначе 403.
|
||||
* Используется ховер-карточкой (§F2) и формой редактирования для подгрузки
|
||||
* состава участников.
|
||||
*/
|
||||
export async function getConference(id: string): Promise<ConferenceOut> {
|
||||
return apiRequest<ConferenceOut>(`/conferences/${id}`)
|
||||
}
|
||||
Reference in New Issue
Block a user