Merge pull request #116 from mateuszskoczek/features/media_page_roles_panels
Features/media page roles panels
This commit is contained in:
@@ -4,7 +4,7 @@ using WatchIt.Common.Query;
|
||||
|
||||
namespace WatchIt.Common.Model.Roles;
|
||||
|
||||
public class ActorRoleResponse : ActorRole, IQueryOrderable<ActorRoleResponse>
|
||||
public class ActorRoleResponse : ActorRole, IQueryOrderable<ActorRoleResponse>, IRoleResponse
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using WatchIt.Common.Query;
|
||||
|
||||
namespace WatchIt.Common.Model.Roles;
|
||||
|
||||
public class CreatorRoleResponse : CreatorRole, IQueryOrderable<CreatorRoleResponse>
|
||||
public class CreatorRoleResponse : CreatorRole, IQueryOrderable<CreatorRoleResponse>, IRoleResponse
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace WatchIt.Common.Model.Roles;
|
||||
|
||||
public interface IRoleResponse
|
||||
{
|
||||
Guid Id { get; set; }
|
||||
long MediaId { get; set; }
|
||||
long PersonId { get; set; }
|
||||
short TypeId { get; set; }
|
||||
}
|
||||
@@ -42,7 +42,14 @@ public static class Program
|
||||
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
{
|
||||
scope.ServiceProvider.GetService<DatabaseContext>().Database.Migrate();
|
||||
DatabaseContext dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||
|
||||
while (!dbContext.Database.CanConnect())
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
|
||||
dbContext.Database.Migrate();
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
|
||||
@@ -5,5 +5,6 @@ public class ConfigurationData
|
||||
public Logging Logging { get; set; }
|
||||
public string AllowedHosts { get; set; }
|
||||
public StorageKeys StorageKeys { get; set; }
|
||||
public Style Style { get; set; }
|
||||
public Endpoints Endpoints { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace WatchIt.Website.Services.Utility.Configuration.Model;
|
||||
|
||||
public class Style
|
||||
{
|
||||
public int DefaultPanelPadding { get; set; }
|
||||
}
|
||||
@@ -9,8 +9,10 @@
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="css/general.css?version=0.3.0.1"/>
|
||||
<link rel="stylesheet" href="css/general.css?version=0.3.0.2"/>
|
||||
<link rel="stylesheet" href="css/panel.css?version=0.3.0.2"/>
|
||||
<link rel="stylesheet" href="css/main_button.css?version=0.3.0.0"/>
|
||||
<link rel="stylesheet" href="css/gaps.css?version=0.3.0.0"/>
|
||||
<link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.2.0.12"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
@if (IsLoading)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>@LoadingContent</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ChildContent is null)
|
||||
{
|
||||
<span>@Content</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@(ChildContent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace WatchIt.Website.Components;
|
||||
|
||||
public partial class LoadingButtonContentComponent : ComponentBase
|
||||
{
|
||||
#region PARAMETERS
|
||||
|
||||
[Parameter] public bool IsLoading { get; set; }
|
||||
[Parameter] public string? LoadingContent { get; set; }
|
||||
[Parameter] public string Content { get; set; }
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="panel panel-padding-regular panel-radius-regular panel-background-regular @(Class)">
|
||||
<div class="vstack gap-3">
|
||||
<span class="panel-text-title">Actors</span>
|
||||
<RoleListComponent Id="@(Id)"
|
||||
TRole="WatchIt.Common.Model.Roles.ActorRoleResponse"
|
||||
TQuery="WatchIt.Common.Model.Roles.ActorRoleMediaQueryParameters"
|
||||
GetRolesAction="MediaWebAPIService.GetMediaAllActorRoles"
|
||||
AdditionalTextSource="@(data => data.Name)"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using WatchIt.Website.Services.WebAPI.Media;
|
||||
|
||||
namespace WatchIt.Website.Components.MediaPage;
|
||||
|
||||
public partial class ActorRolesPanelComponent : ComponentBase
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
[Inject] private IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PARAMETERS
|
||||
|
||||
[Parameter] public string Class { get; set; } = string.Empty;
|
||||
[Parameter] public required long Id { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@using WatchIt.Common.Model.Roles
|
||||
|
||||
|
||||
|
||||
<div class="panel panel-padding-regular panel-radius-regular panel-background-regular @(Class)">
|
||||
@if (_loaded)
|
||||
{
|
||||
<div class="vstack gap-3">
|
||||
<span class="panel-text-title">Creators</span>
|
||||
<div class="d-flex justify-content-center">
|
||||
<RadioGroup TValue="short" Color="Color.Default" Buttons Size="Size.Small" CheckedValue="@(_query.TypeId!.Value)" CheckedValueChanged="CheckedTypeChanged">
|
||||
@foreach (RoleTypeResponse roleType in _roleTypes)
|
||||
{
|
||||
<Radio Value="@(roleType.Id)">@roleType.Name</Radio>
|
||||
}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<RoleListComponent @ref=@(_roleListComponent)
|
||||
Id="@(Id)"
|
||||
TRole="CreatorRoleResponse"
|
||||
TQuery="CreatorRoleMediaQueryParameters"
|
||||
GetRolesAction="MediaWebAPIService.GetMediaAllCreatorRoles"
|
||||
Query="@(_query)"/>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<LoadingComponent Color="white"/>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using WatchIt.Common.Model.Roles;
|
||||
using WatchIt.Website.Services.WebAPI.Media;
|
||||
using WatchIt.Website.Services.WebAPI.Roles;
|
||||
|
||||
namespace WatchIt.Website.Components.MediaPage;
|
||||
|
||||
public partial class CreatorRolesPanelComponent : ComponentBase
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
[Inject] private IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
|
||||
[Inject] private IRolesWebAPIService RolesWebAPIService { get; set; } = default!;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PARAMETERS
|
||||
|
||||
[Parameter] public string Class { get; set; } = string.Empty;
|
||||
[Parameter] public required long Id { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region FIELDS
|
||||
|
||||
private RoleListComponent<CreatorRoleResponse, CreatorRoleMediaQueryParameters> _roleListComponent;
|
||||
|
||||
private bool _loaded;
|
||||
|
||||
private IEnumerable<RoleTypeResponse> _roleTypes;
|
||||
private CreatorRoleMediaQueryParameters _query;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
List<Task> endTasks = new List<Task>();
|
||||
|
||||
// STEP 0
|
||||
endTasks.AddRange(
|
||||
[
|
||||
RolesWebAPIService.GetAllCreatorRoleTypes(successAction: data => _roleTypes = data)
|
||||
]);
|
||||
|
||||
// END
|
||||
await Task.WhenAll(endTasks);
|
||||
_query = new CreatorRoleMediaQueryParameters { TypeId = _roleTypes.First().Id };
|
||||
|
||||
_loaded = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckedTypeChanged(short value)
|
||||
{
|
||||
_query.TypeId = value;
|
||||
await _roleListComponent.Refresh();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@using WatchIt.Common.Model.Roles
|
||||
@typeparam TRole where TRole : WatchIt.Common.Model.Roles.IRoleResponse
|
||||
|
||||
<div class="container-grid">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<img id="picture" class="rounded-2 shadow object-fit-cover picture-aspect-ratio" src="@(_picture is not null ? _picture.ToString() : "assets/person_picture.png")" alt="picture" height="100"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="d-flex align-items-start flex-column h-100">
|
||||
<div class="mb-auto">
|
||||
<span id="nameText">
|
||||
@if (_person is null)
|
||||
{
|
||||
<span>Loading...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<strong>@(_person.Name)</strong>@(Role is ActorRoleResponse actor ? $" as {actor.Name}" : string.Empty)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using WatchIt.Common.Model;
|
||||
using WatchIt.Common.Model.Persons;
|
||||
using WatchIt.Common.Model.Roles;
|
||||
using WatchIt.Website.Services.WebAPI.Persons;
|
||||
|
||||
namespace WatchIt.Website.Components.MediaPage;
|
||||
|
||||
public partial class RoleComponent<TRole> : ComponentBase where TRole : IRoleResponse
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
[Inject] private IPersonsWebAPIService PersonsWebAPIService { get; set; } = default!;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PARAMETERS
|
||||
|
||||
[Parameter] public required TRole Role { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region FIELDS
|
||||
|
||||
private PersonResponse? _person;
|
||||
private Picture? _picture;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
List<Task> endTasks = new List<Task>();
|
||||
|
||||
// STEP 0
|
||||
endTasks.AddRange(
|
||||
[
|
||||
PersonsWebAPIService.GetPersonPhoto(Role.PersonId, data =>
|
||||
{
|
||||
_picture = data;
|
||||
StateHasChanged();
|
||||
}),
|
||||
PersonsWebAPIService.GetPerson(Role.PersonId, data =>
|
||||
{
|
||||
_person = data;
|
||||
StateHasChanged();
|
||||
})
|
||||
]);
|
||||
|
||||
// END
|
||||
await Task.WhenAll(endTasks);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
@typeparam TRole where TRole : WatchIt.Common.Model.Roles.IRoleResponse, WatchIt.Common.Query.IQueryOrderable<TRole>
|
||||
@typeparam TQuery where TQuery : WatchIt.Common.Query.QueryParameters<TRole>
|
||||
|
||||
@if (_loaded)
|
||||
{
|
||||
if (_roles.Count > 0)
|
||||
{
|
||||
<div class="vstack">
|
||||
@for (int i = 0; i < _roles.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
<hr/>
|
||||
}
|
||||
<a class="text-reset text-decoration-none" href="/person/@(_roles[i].PersonId)">
|
||||
<RoleComponent TRole="TRole"
|
||||
Role="@(_roles[i])"/>
|
||||
</a>
|
||||
}
|
||||
@if (!_allItemsLoaded)
|
||||
{
|
||||
<div class="d-flex justify-content-center">
|
||||
<button class="btn btn-secondary" @onclick="@(async () => await GetRoles())">
|
||||
<LoadingButtonContentComponent Content="Load more"
|
||||
LoadingContent="Loading..."
|
||||
IsLoading="@(_rolesFetching)"/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-center">No roles found</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<LoadingComponent/>
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.ComponentModel;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using WatchIt.Common.Model.Roles;
|
||||
using WatchIt.Common.Query;
|
||||
|
||||
namespace WatchIt.Website.Components.MediaPage;
|
||||
|
||||
public partial class RoleListComponent<TRole, TQuery> : ComponentBase where TRole : IRoleResponse, IQueryOrderable<TRole> where TQuery : QueryParameters<TRole>
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
[Parameter] public required long Id { get; set; }
|
||||
[Parameter] public required Func<long, TQuery, Action<IEnumerable<TRole>>, Task> GetRolesAction { get; set; }
|
||||
[Parameter] public TQuery Query { get; set; } = Activator.CreateInstance<TQuery>();
|
||||
[Parameter] public Func<TRole, string>? AdditionalTextSource { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region FIELDS
|
||||
|
||||
private readonly int _pageSize = 20;
|
||||
|
||||
private bool _loaded;
|
||||
private bool _allItemsLoaded;
|
||||
|
||||
private List<TRole> _roles = new List<TRole>();
|
||||
private bool _rolesFetching;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public async Task Refresh()
|
||||
{
|
||||
_loaded = false;
|
||||
_roles.Clear();
|
||||
Query.After = null;
|
||||
Query.First = _pageSize;
|
||||
await GetRoles(true);
|
||||
_loaded = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
List<Task> endTasks = new List<Task>();
|
||||
|
||||
// INIT
|
||||
Query.First = _pageSize;
|
||||
|
||||
// STEP 0
|
||||
endTasks.AddRange(
|
||||
[
|
||||
GetRoles(true)
|
||||
]);
|
||||
|
||||
// END
|
||||
await Task.WhenAll(endTasks);
|
||||
|
||||
_loaded = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GetRoles(bool firstFetch = false)
|
||||
{
|
||||
_rolesFetching = true;
|
||||
await GetRolesAction(Id, Query, data =>
|
||||
{
|
||||
_roles.AddRange(data);
|
||||
if (data.Count() < _pageSize)
|
||||
{
|
||||
_allItemsLoaded = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (firstFetch)
|
||||
{
|
||||
Query.After = 0;
|
||||
}
|
||||
}
|
||||
Query.After += data.Count();
|
||||
_rolesFetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="container-xl">
|
||||
<div class="row sticky-top top-3 mb-2rem">
|
||||
<div class="col">
|
||||
<div class="panel panel-header rounded-3 px-3">
|
||||
<div class="panel panel-header">
|
||||
<div class="container-grid">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
|
||||
@@ -190,6 +190,31 @@ else
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-over-panel-menu">
|
||||
<div class="col">
|
||||
<Tabs Pills
|
||||
RenderMode="TabsRenderMode.LazyLoad"
|
||||
SelectedTab="actors"
|
||||
Class="panel panel-menu panel-background-menu">
|
||||
<Items>
|
||||
<Tab Name="actors">Actors</Tab>
|
||||
<Tab Name="creators">Creators</Tab>
|
||||
</Items>
|
||||
<Content>
|
||||
<TabPanel Name="actors">
|
||||
<div class="mt-default">
|
||||
<ActorRolesPanelComponent Id="@(Id)"/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel Name="creators">
|
||||
<div class="mt-default">
|
||||
<CreatorRolesPanelComponent Id="@(Id)"/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Content>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
@using WatchIt.Website.Components
|
||||
@using WatchIt.Website.Components.PersonEditPage
|
||||
@using WatchIt.Website.Components.MediaEditPage
|
||||
@using WatchIt.Website.Components.MediaPage
|
||||
@using WatchIt.Common.Model.Accounts
|
||||
@using WatchIt.Common.Model.Media
|
||||
@using WatchIt.Website.Services.Utility.Tokens
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"AccessToken": "access_token",
|
||||
"RefreshToken": "refresh_token"
|
||||
},
|
||||
"Style": {
|
||||
"DefaultPanelPadding": 4
|
||||
},
|
||||
"Endpoints": {
|
||||
"Base": "https://localhost:7160",
|
||||
"Accounts": {
|
||||
|
||||
17
WatchIt.Website/WatchIt.Website/wwwroot/css/gaps.css
Normal file
17
WatchIt.Website/WatchIt.Website/wwwroot/css/gaps.css
Normal file
@@ -0,0 +1,17 @@
|
||||
/* DEFAULT */
|
||||
|
||||
.mt-default {
|
||||
margin-top: 1rem !important;
|
||||
}
|
||||
|
||||
.gx-default {
|
||||
--bs-gutter-x: 1rem !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* OTHERS */
|
||||
|
||||
.mt-over-panel-menu {
|
||||
margin-top: 3rem !important;
|
||||
}
|
||||
@@ -25,6 +25,8 @@ body, html {
|
||||
font-family: "PT Sans";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* CLASSES */
|
||||
|
||||
.container-grid {
|
||||
@@ -53,6 +55,8 @@ body, html {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.top-3 {
|
||||
top: 1rem !important;
|
||||
}
|
||||
@@ -69,22 +73,16 @@ body, html {
|
||||
margin-top: 9rem !important;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.panel-regular {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
background-color: rgba(0, 0, 0, 0.6) !important;
|
||||
}
|
||||
|
||||
.panel-yellow {
|
||||
background-color: rgba(255, 184, 58, 0.6);
|
||||
background-color: rgba(255, 184, 58, 0.6) !important;
|
||||
}
|
||||
|
||||
.panel {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(25px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
68
WatchIt.Website/WatchIt.Website/wwwroot/css/panel.css
Normal file
68
WatchIt.Website/WatchIt.Website/wwwroot/css/panel.css
Normal file
@@ -0,0 +1,68 @@
|
||||
/* MAIN */
|
||||
|
||||
.panel {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(25px);
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* HEADER */
|
||||
|
||||
.panel-header {
|
||||
padding: 0 1rem !important;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* MENU */
|
||||
|
||||
.panel-menu {
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem !important;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.panel-menu > li > a {
|
||||
--bs-nav-pills-link-active-bg: transparent;
|
||||
--bs-nav-link-hover-color: white;
|
||||
--bs-nav-link-color: #a6a6a6;
|
||||
|
||||
--bs-nav-pills-border-radius: 1.25rem;
|
||||
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.panel-menu > li > a.active {
|
||||
--bs-nav-link-color: white;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* BACKGROUNDS */
|
||||
|
||||
.panel-background-gold {
|
||||
background-color: rgba(255, 184, 58, 0.6);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* OTHERS */
|
||||
|
||||
.panel-text-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
Reference in New Issue
Block a user