+
+
+
+
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+
+ ⚠️ @errorMessage
+
+ }
+
+ @if (!string.IsNullOrEmpty(successMessage))
+ {
+
+ ✅ @successMessage
+
+ }
+
+ @if (editor is null)
+ {
+
+ }
+ else
+ {
+
+
+
+
+
+ @if (!string.IsNullOrEmpty(editor.CoverPath))
+ {
+

+ }
+ else
+ {
+
Обложка не загружена
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @foreach (var session in editor.Sessions)
+ {
+
+ }
+
+
+
+
+
+
+ @foreach (var master in editor.Masters)
+ {
+
+ }
+
+
+
+
+
+
+ @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