Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View 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}`)
}