@page "/portfolio/manage/{PortfolioGameId:guid}" @using GmRelay.Web.Services @using GmRelay.Web.Services.Portfolio @using GmRelay.Web.Services.Portfolio.Covers @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @attribute [Authorize] @inject AuthorizedPortfolioService PortfolioService @inject AuthenticationStateProvider AuthStateProvider @inject NavigationManager Navigation Портфолио — GM-Relay
@if (!string.IsNullOrEmpty(errorMessage)) {
⚠️ @errorMessage
} @if (!string.IsNullOrEmpty(successMessage)) {
✅ @successMessage
} @if (editor is null) {
} else {

Параметры публикации

Управление видимостью и обложкой приключения.

@(editor.IsPublic ? "Опубликовано" : "Черновик")
@if (!string.IsNullOrEmpty(editor.CoverPath)) { Обложка } else {
Обложка не загружена
}
Латиница, цифры и дефисы, например "night-city-run".

Проведённые сессии

Отметьте игры, которые вошли в это приключение.

@editorModel.SessionIds.Count
@foreach (var session in editor.Sessions) { }

Мастера приключения

Выберите мастеров, которые вели это приключение.

@editorModel.MasterPlayerIds.Count
@foreach (var master in editor.Masters) { }

Модерация отзывов

Одобрите, отклоните или скройте отзывы игроков перед публикацией.

@editor.Reviews.Count(r => r.ModerationStatus == "Pending") на модерации
@if (editor.Reviews.Count == 0) {
Отзывов пока нет

Игроки смогут оставить отзыв после публикации приключения.

} else {
@foreach (var review in editor.Reviews) {
@review.AuthorDisplayName @TranslateReviewStatus(review.ModerationStatus) @review.CreatedAt.ToString("dd.MM.yyyy HH:mm")

@review.Body

}
}
}
@code { [Parameter] public Guid PortfolioGameId { get; set; } private PortfolioGameEditor? editor; private PortfolioEditorModel editorModel = new(); private Guid? groupId; private string? errorMessage; private string? successMessage; private bool isSaving; private bool isUploadingCover; private bool isUpdatingPublication; private bool isDeleting; private Guid? moderatingReviewId; private IBrowserFile? pendingCoverFile; protected override async Task OnParametersSetAsync() { await Reload(); } private async Task Reload() { editor = await PortfolioService.GetPortfolioGameForCurrentUserAsync(PortfolioGameId); if (editor is null) { Navigation.NavigateTo("/access-denied"); return; } groupId = editor.GroupId; editorModel = new PortfolioEditorModel { Title = editor.Title, PublicSlug = editor.PublicSlug ?? string.Empty, Description = editor.Description ?? string.Empty, System = editor.System ?? string.Empty, Format = editor.Format ?? string.Empty, SessionIds = editor.Sessions.Where(s => s.Selected).Select(s => s.Id).ToList(), MasterPlayerIds = editor.Masters.Where(m => m.Selected).Select(m => m.PlayerId).ToList() }; } private void ToggleSession(Guid sessionId, bool isChecked) { if (isChecked) { if (!editorModel.SessionIds.Contains(sessionId)) { editorModel.SessionIds.Add(sessionId); } } else { editorModel.SessionIds.Remove(sessionId); } } private void ToggleMaster(Guid playerId, bool isChecked) { if (isChecked) { if (!editorModel.MasterPlayerIds.Contains(playerId)) { editorModel.MasterPlayerIds.Add(playerId); } } else { editorModel.MasterPlayerIds.Remove(playerId); } } private async Task SaveDraft() { errorMessage = null; successMessage = null; isSaving = true; try { await PortfolioService.UpdateDraftForCurrentUserAsync( PortfolioGameId, new PortfolioGameUpdate( editorModel.Title, editorModel.PublicSlug, editorModel.Description, editorModel.System, editorModel.Format, editorModel.SessionIds, editorModel.MasterPlayerIds)); successMessage = "Черновик сохранён."; await Reload(); } catch (InvalidOperationException ex) { errorMessage = ex.Message; } catch (Exception ex) { errorMessage = "Не удалось сохранить: " + ex.Message; } finally { isSaving = false; } } private void TriggerCoverUpload() { // The InputFile control is rendered with a label. No-op click handler kept for symmetry. } private async Task HandleFileSelected(InputFileChangeEventArgs e) { var file = e.File; if (file is null) { return; } pendingCoverFile = file; errorMessage = null; successMessage = null; isUploadingCover = true; try { await using var stream = file.OpenReadStream(LocalPortfolioCoverStorage.MaxBytes); await PortfolioService.ReplaceCoverForCurrentUserAsync(PortfolioGameId, stream, file.ContentType); successMessage = "Обложка обновлена."; await Reload(); } catch (InvalidOperationException ex) { errorMessage = ex.Message; } catch (Exception ex) { errorMessage = "Не удалось загрузить обложку: " + ex.Message; } finally { isUploadingCover = false; pendingCoverFile = null; } } private async Task SetPublication(bool isPublic) { errorMessage = null; successMessage = null; isUpdatingPublication = true; try { await PortfolioService.SetPublicationForCurrentUserAsync(PortfolioGameId, isPublic); successMessage = isPublic ? "Приключение опубликовано." : "Приключение скрыто."; await Reload(); } catch (InvalidOperationException ex) { errorMessage = ex.Message; } catch (Exception ex) { errorMessage = "Не удалось обновить публикацию: " + ex.Message; } finally { isUpdatingPublication = false; } } private async Task DeletePortfolio() { errorMessage = null; successMessage = null; isDeleting = true; try { await PortfolioService.DeleteForCurrentUserAsync(PortfolioGameId); Navigation.NavigateTo(groupId.HasValue ? $"/group/{groupId.Value}" : "/"); } catch (Exception ex) { errorMessage = "Не удалось удалить: " + ex.Message; isDeleting = false; } } private async Task Moderate(Guid reviewId, string moderationStatus) { errorMessage = null; successMessage = null; moderatingReviewId = reviewId; try { await PortfolioService.ModerateReviewForCurrentUserAsync(PortfolioGameId, reviewId, moderationStatus); successMessage = "Модерация обновлена."; await Reload(); } catch (InvalidOperationException ex) { errorMessage = ex.Message; } catch (Exception ex) { errorMessage = "Не удалось обновить отзыв: " + ex.Message; } finally { moderatingReviewId = null; } } private static string GetReviewStatusClass(string status) => status switch { "Approved" => "status-success", "Rejected" => "status-danger", "Hidden" => "status-warning", _ => "status-neutral" }; private static string TranslateReviewStatus(string status) => status switch { "Approved" => "Одобрен", "Rejected" => "Отклонён", "Hidden" => "Скрыт", _ => "На модерации" }; private sealed class PortfolioEditorModel { public string Title { get; set; } = string.Empty; public string PublicSlug { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public string System { get; set; } = string.Empty; public string Format { get; set; } = string.Empty; public List SessionIds { get; set; } = new(); public List MasterPlayerIds { get; set; } = new(); } }