profile picture and basic info editors added

This commit is contained in:
2024-11-03 23:01:34 +01:00
Unverified
parent 88e8e330aa
commit 3604c066e7
34 changed files with 607 additions and 64 deletions

View File

@@ -2,10 +2,13 @@ using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Accounts;
public class AccountRequest : Account
public class AccountProfileInfoRequest
{
#region PROPERTIES
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("gender_id")]
public short? GenderId { get; set; }
@@ -13,12 +16,24 @@ public class AccountRequest : Account
#region CONSTRUCTORS
public AccountProfileInfoRequest() { }
public AccountProfileInfoRequest(AccountResponse accountResponse)
{
Description = accountResponse.Description;
GenderId = accountResponse.Gender?.Id;
}
#endregion
#region PUBLIC METHODS
public void UpdateAccount(Database.Model.Account.Account account)
{
account.Username = Username;
account.Email = Email;
account.Description = Description;
account.GenderId = GenderId;
}

View File

@@ -0,0 +1,33 @@
using System.Diagnostics.CodeAnalysis;
namespace WatchIt.Common.Model.Accounts;
public class AccountProfilePictureRequest : AccountProfilePicture
{
#region CONSTRUCTORS
public AccountProfilePictureRequest() {}
[SetsRequiredMembers]
public AccountProfilePictureRequest(Picture image)
{
Image = image.Image;
MimeType = image.MimeType;
}
#endregion
public Database.Model.Account.AccountProfilePicture CreateMediaPosterImage() => new Database.Model.Account.AccountProfilePicture
{
Image = Image,
MimeType = MimeType,
};
public void UpdateMediaPosterImage(Database.Model.Account.AccountProfilePicture item)
{
item.Image = Image;
item.MimeType = MimeType;
item.UploadDate = DateTime.UtcNow;
}
}

View File

@@ -27,7 +27,7 @@ public class AccountsController(IAccountsControllerService accountsControllerSer
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> Authenticate([FromBody]AuthenticateRequest body) => await accountsControllerService.Authenticate(body);
[HttpPost("authenticate-refresh")]
[HttpPost("authenticate_refresh")]
[Authorize(AuthenticationSchemes = "refresh")]
[ProducesResponseType(typeof(AuthenticateResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
@@ -39,25 +39,38 @@ public class AccountsController(IAccountsControllerService accountsControllerSer
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
public async Task<ActionResult> Logout() => await accountsControllerService.Logout();
[HttpGet("{id}/profile-picture")]
[HttpGet("{id}/profile_picture")]
[AllowAnonymous]
[ProducesResponseType(typeof(AccountProfilePictureResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetAccountProfilePicture([FromRoute(Name = "id")]long id) => await accountsControllerService.GetAccountProfilePicture(id);
[HttpPut("profile_picture")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(AccountProfilePictureResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> PutAccountProfilePicture([FromBody]AccountProfilePictureRequest body) => await accountsControllerService.PutAccountProfilePicture(body);
[HttpDelete("profile_picture")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> DeleteAccountProfilePicture() => await accountsControllerService.DeleteAccountProfilePicture();
[HttpGet("{id}/info")]
[AllowAnonymous]
[ProducesResponseType(typeof(AccountResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetAccountInfo([FromRoute]long id) => await accountsControllerService.GetAccountInfo(id);
[HttpPut("info")]
[HttpPut("profile_info")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(AccountResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PutAccountInfo([FromBody]AccountRequest data) => await accountsControllerService.PutAccountInfo(data);
public async Task<ActionResult> PutAccountProfileInfo([FromBody]AccountProfileInfoRequest data) => await accountsControllerService.PutAccountProfileInfo(data);
[HttpGet("{id}/movies")]
[AllowAnonymous]

View File

@@ -143,6 +143,48 @@ public class AccountsControllerService(
return RequestResult.Ok(picture);
}
public async Task<RequestResult> PutAccountProfilePicture(AccountProfilePictureRequest data)
{
Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId());
Database.Model.Account.AccountProfilePicture? picture = account.ProfilePicture;
if (picture is null)
{
picture = data.CreateMediaPosterImage();
await database.AccountProfilePictures.AddAsync(picture);
await database.SaveChangesAsync();
account.ProfilePictureId = picture.Id;
}
else
{
data.UpdateMediaPosterImage(picture);
}
await database.SaveChangesAsync();
AccountProfilePictureResponse returnData = new AccountProfilePictureResponse(picture);
return RequestResult.Ok(returnData);
}
public async Task<RequestResult> DeleteAccountProfilePicture()
{
Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId());
Database.Model.Account.AccountProfilePicture? picture = account.ProfilePicture;
if (picture is not null)
{
account.ProfilePictureId = null;
await database.SaveChangesAsync();
database.AccountProfilePictures.Attach(picture);
database.AccountProfilePictures.Remove(picture);
await database.SaveChangesAsync();
}
return RequestResult.NoContent();
}
public async Task<RequestResult> GetAccountInfo(long id)
{
Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == id);
@@ -151,11 +193,11 @@ public class AccountsControllerService(
return RequestResult.NotFound();
}
AccountResponse response = new AccountResponse(account);
return RequestResult.Ok(response);
AccountResponse profileInfoResponse = new AccountResponse(account);
return RequestResult.Ok(profileInfoResponse);
}
public async Task<RequestResult> PutAccountInfo(AccountRequest data)
public async Task<RequestResult> PutAccountProfileInfo(AccountProfileInfoRequest data)
{
Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == userService.GetUserId());
if (account is null)
@@ -164,6 +206,8 @@ public class AccountsControllerService(
}
data.UpdateAccount(account);
await database.SaveChangesAsync();
return RequestResult.Ok();
}

View File

@@ -14,8 +14,10 @@ public interface IAccountsControllerService
Task<RequestResult> AuthenticateRefresh();
Task<RequestResult> Logout();
Task<RequestResult> GetAccountProfilePicture(long id);
Task<RequestResult> PutAccountProfilePicture(AccountProfilePictureRequest data);
Task<RequestResult> DeleteAccountProfilePicture();
Task<RequestResult> GetAccountInfo(long id);
Task<RequestResult> PutAccountInfo(AccountRequest data);
Task<RequestResult> PutAccountProfileInfo(AccountProfileInfoRequest data);
Task<RequestResult> GetAccountRatedMovies(long id, MovieRatedQueryParameters query);
Task<RequestResult> GetAccountRatedSeries(long id, SeriesRatedQueryParameters query);
Task<RequestResult> GetAccountRatedPersons(long id, PersonRatedQueryParameters query);

View File

@@ -4,14 +4,10 @@ using WatchIt.Database;
namespace WatchIt.WebAPI.Validators.Accounts;
public class AccountRequestValidator : AbstractValidator<AccountRequest>
public class AccountProfileInfoRequestValidator : AbstractValidator<AccountProfileInfoRequest>
{
public AccountRequestValidator(DatabaseContext database)
public AccountProfileInfoRequestValidator(DatabaseContext database)
{
RuleFor(x => x.Username).NotEmpty()
.MaximumLength(50);
RuleFor(x => x.Email).EmailAddress()
.MaximumLength(320);
RuleFor(x => x.Description).MaximumLength(1000);
When(x => x.GenderId.HasValue, () =>
{

View File

@@ -0,0 +1,13 @@
using FluentValidation;
using WatchIt.Common.Model.Accounts;
namespace WatchIt.WebAPI.Validators.Accounts;
public class AccountProfilePictureRequestValidator : AbstractValidator<AccountProfilePictureRequest>
{
public AccountProfilePictureRequestValidator()
{
RuleFor(x => x.Image).NotEmpty();
RuleFor(x => x.MimeType).Matches(@"\w+/.+").WithMessage("Incorrect mimetype");
}
}

View File

@@ -71,7 +71,7 @@ public class AccountsClientService(IHttpClientService httpClientService, IConfig
public async Task GetAccountProfilePicture(long id, Action<AccountProfilePictureResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.GetProfilePicture, id);
string url = GetUrl(EndpointsConfiguration.Accounts.GetAccountProfilePicture, id);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
@@ -81,6 +81,34 @@ public class AccountsClientService(IHttpClientService httpClientService, IConfig
.ExecuteAction();
}
public async Task PutAccountProfilePicture(AccountProfilePictureRequest data, Action<AccountProfilePictureResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.PutAccountProfilePicture);
HttpRequest request = new HttpRequest(HttpMethodType.Put, url)
{
Body = data
};
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.ExecuteAction();
}
public async Task DeleteAccountProfilePicture(Action? successAction = null, Action? unauthorizedAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.DeleteAccountProfilePicture);
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.ExecuteAction();
}
public async Task GetAccountInfo(long id, Action<AccountResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.GetAccountInfo, id);
@@ -92,9 +120,9 @@ public class AccountsClientService(IHttpClientService httpClientService, IConfig
.ExecuteAction();
}
public async Task PutAccountInfo(AccountRequest data, Action<AccountResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? notFoundAction = null)
public async Task PutAccountProfileInfo(AccountProfileInfoRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.PutAccountInfo);
string url = GetUrl(EndpointsConfiguration.Accounts.PutAccountProfileInfo);
HttpRequest request = new HttpRequest(HttpMethodType.Put, url)
{
Body = data,
@@ -104,7 +132,6 @@ public class AccountsClientService(IHttpClientService httpClientService, IConfig
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}

View File

@@ -12,8 +12,10 @@ public interface IAccountsClientService
Task AuthenticateRefresh(Action<AuthenticateResponse>? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task Logout(Action? successAction = null);
Task GetAccountProfilePicture(long id, Action<AccountProfilePictureResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null);
Task PutAccountProfilePicture(AccountProfilePictureRequest data, Action<AccountProfilePictureResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null);
Task DeleteAccountProfilePicture(Action? successAction = null, Action? unauthorizedAction = null);
Task GetAccountInfo(long id, Action<AccountResponse>? successAction = null, Action? notFoundAction = null);
Task PutAccountInfo(AccountRequest data, Action<AccountResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? notFoundAction = null);
Task PutAccountProfileInfo(AccountProfileInfoRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null);
Task GetAccountRatedMovies(long id, MovieRatedQueryParameters query, Action<IEnumerable<MovieRatedResponse>>? successAction = null, Action? notFoundAction = null);
Task GetAccountRatedSeries(long id, SeriesRatedQueryParameters query, Action<IEnumerable<SeriesRatedResponse>>? successAction = null, Action? notFoundAction = null);
Task GetAccountRatedPersons(long id, PersonRatedQueryParameters query, Action<IEnumerable<PersonRatedResponse>>? successAction = null, Action? notFoundAction = null);

View File

@@ -7,9 +7,11 @@ public class Accounts
public string Authenticate { get; set; }
public string AuthenticateRefresh { get; set; }
public string Logout { get; set; }
public string GetProfilePicture { get; set; }
public string GetAccountProfilePicture { get; set; }
public string PutAccountProfilePicture { get; set; }
public string DeleteAccountProfilePicture { get; set; }
public string GetAccountInfo { get; set; }
public string PutAccountInfo { get; set; }
public string PutAccountProfileInfo { get; set; }
public string GetAccountRatedMovies { get; set; }
public string GetAccountRatedSeries { get; set; }
public string GetAccountRatedPersons { get; set; }

View File

@@ -10,10 +10,10 @@
<!-- CSS -->
<link rel="stylesheet" href="css/general.css?version=0.3.0.5"/>
<link rel="stylesheet" href="css/panel.css?version=0.3.0.3"/>
<link rel="stylesheet" href="css/panel.css?version=0.3.0.5"/>
<link rel="stylesheet" href="css/main_button.css?version=0.3.0.0"/>
<link rel="stylesheet" href="css/gaps.css?version=0.3.0.1"/>
<link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.4.0.13"/>
<link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.4.0.16"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<!-- BOOTSTRAP -->

View File

@@ -2,7 +2,7 @@
@if (_loaded)
{
<div class="vstack gap-3">
<PictureComponent Picture="@(_pictureSelected)" Placeholder="@(PicturePlaceholder)" AlternativeText="poster" Width="@(ContentWidth)" AspectRatio="PictureComponent.PictureComponentAspectRatio.Default"/>
<PictureComponent Picture="@(_pictureSelected)" Placeholder="@(PicturePlaceholder)" AlternativeText="loaded_picture" Circle="@(Circle)" Width="@(ContentWidth)"/>
<InputFile class="form-control content-width" OnChange="Load" disabled="@(!Id.HasValue)" autocomplete="off"/>
@if (_pictureChanged || _pictureSaved is not null)
{

View File

@@ -11,10 +11,12 @@ public partial class PictureEditorPanelComponent : ComponentBase
[Parameter] public long? Id { get; set; }
[Parameter] public int ContentWidth { get; set; } = 300;
[Parameter] public required string PicturePlaceholder { get; set; }
[Parameter] public bool Circle { get; set; }
[Parameter] public string Class { get; set; } = string.Empty;
[Parameter] public required Func<long, Action<Picture>, Task> PictureGetTask { get; set; }
[Parameter] public required Func<long, Picture, Action<Picture>, Task> PicturePutTask { get; set; }
[Parameter] public required Func<long, Action, Task> PictureDeleteTask { get; set; }
[Parameter] public Action<Picture?>? OnPictureChanged { get; set; }
#endregion
@@ -92,6 +94,7 @@ public partial class PictureEditorPanelComponent : ComponentBase
_pictureSelected = data;
_pictureChanged = false;
_pictureSaving = false;
OnPictureChanged?.Invoke(data);
}
_pictureSaving = true;
@@ -112,6 +115,7 @@ public partial class PictureEditorPanelComponent : ComponentBase
_pictureSelected = null;
_pictureChanged = false;
_pictureDeleting = false;
OnPictureChanged?.Invoke(null);
}
_pictureDeleting = true;

View File

@@ -1 +1,7 @@
<img class="rounded-circle object-fit-cover @(Class)" alt="avatar" height="@(Size)" src="@(_picture is null ? "assets/user_placeholder.png" : _picture.ToString())"/>
<PictureComponent Class="@(Class)"
Height="Size"
Circle="true"
Shadow="false"
Placeholder="assets/user_placeholder.png"
AlternativeText="avatar"
Picture="@(_picture)"/>

View File

@@ -33,24 +33,25 @@ public partial class AccountPictureComponent : ComponentBase
#region PUBLIC METHODS
public async Task Reload()
{
await AccountsClientService.GetAccountProfilePicture(Id, data => _picture = data, notFoundAction: () => _picture = null);
StateHasChanged();
}
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
List<Task> endTasks = new List<Task>();
// STEP 0
endTasks.AddRange(
[
AccountsClientService.GetAccountProfilePicture(Id, data => _picture = data)
]);
// END
await Task.WhenAll(endTasks);
StateHasChanged();
await Reload();
}
}

View File

@@ -1 +1 @@
<img id="imgObject" class="rounded-2 shadow object-fit-cover @(Class)" src="@(Picture is not null ? Picture.ToString() : Placeholder)" alt="@(AlternativeText)" @attributes="@(_attributes)" style="aspect-ratio: @(AspectRatio.ToString());"/>
<img class="@(Circle ? "rounded-circle" : "rounded-2") @(Shadow ? "shadow" : string.Empty) object-fit-cover @(Class)" src="@(Picture is not null ? Picture.ToString() : Placeholder)" alt="@(AlternativeText)" @attributes="@(_attributes)" style="aspect-ratio: @(AspectRatio.ToString());"/>

View File

@@ -14,6 +14,8 @@ public partial class PictureComponent : ComponentBase
[Parameter] public string Class { get; set; } = string.Empty;
[Parameter] public int? Height { get; set; }
[Parameter] public int? Width { get; set; }
[Parameter] public bool Circle { get; set; }
[Parameter] public bool Shadow { get; set; } = true;
#endregion
@@ -40,6 +42,11 @@ public partial class PictureComponent : ComponentBase
{
_attributes.Add("width", Width.Value);
}
if (Circle)
{
AspectRatio = PictureComponentAspectRatio.Square;
}
}
#endregion
@@ -71,6 +78,7 @@ public partial class PictureComponent : ComponentBase
public static readonly PictureComponentAspectRatio Default = new PictureComponentAspectRatio();
public static readonly PictureComponentAspectRatio Photo = new PictureComponentAspectRatio(16, 9);
public static readonly PictureComponentAspectRatio Square = new PictureComponentAspectRatio(1, 1);
#endregion

View File

@@ -0,0 +1,60 @@
@using WatchIt.Common.Model.Genders
<div class="panel @(Class)">
@if (_loaded)
{
<div class="vstack gap-3">
<h4 class="fw-bold">Basic profile info</h4>
<EditForm Model="@(_accountProfileInfo)">
<AntiforgeryToken/>
<div class="container-grid">
<div class="row form-group my-1">
<label for="desc" class="col-2 col-form-label">Description</label>
<div class="col-10">
<InputTextArea id="desc" class="form-control" @bind-Value="_accountProfileInfo!.Description"/>
</div>
</div>
<div class="row form-group my-1">
<label for="desc" class="col-2 col-form-label">Gender</label>
<div class="col-10">
<InputSelect TValue="short?" id="desc" class="form-control" @bind-Value="_accountProfileInfo!.GenderId">
<option value="@(default(short?))">No choice</option>
@foreach (GenderResponse gender in _genders)
{
<option value="@(gender.Id)">@(gender.Name)</option>
}
</InputSelect>
</div>
</div>
<div class="row mt-2">
<div class="col align-self-center">
@if (!string.IsNullOrWhiteSpace(_error))
{
<span class="text-danger">@(_error)</span>
}
</div>
<div class="col-auto">
<button type="submit" class="btn btn-secondary" disabled="@(_saving)" @onclick="@(Save)">
@if (!_saving)
{
<span>Save</span>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Saving...</span>
}
</button>
</div>
</div>
</div>
</EditForm>
</div>
}
else
{
<LoadingComponent Color="@(LoadingComponent.LoadingComponentColors.Light)"/>
}
</div>

View File

@@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model.Accounts;
using WatchIt.Common.Model.Genders;
using WatchIt.Website.Services.Client.Accounts;
using WatchIt.Website.Services.Client.Genders;
namespace WatchIt.Website.Components.Pages.UserEditPage.Panels;
public partial class ProfileEditFormPanelComponent : ComponentBase
{
#region SERVICES
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private IAccountsClientService AccountsClientService { get; set; } = default!;
[Inject] private IGendersClientService GendersClientService { get; set; } = default!;
#endregion
#region PARAMETERS
[Parameter] public long Id { get; set; }
[Parameter] public string Class { get; set; } = string.Empty;
#endregion
#region FIELDS
private bool _loaded;
private bool _saving;
private string? _error;
private IEnumerable<GenderResponse> _genders = [];
private AccountProfileInfoRequest _accountProfileInfo = new AccountProfileInfoRequest();
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.WhenAll(
[
GendersClientService.GetAllGenders(successAction: data => _genders = data),
AccountsClientService.GetAccountInfo(Id, data => _accountProfileInfo = new AccountProfileInfoRequest(data))
]);
_loaded = true;
StateHasChanged();
}
}
private async Task Save()
{
void Success()
{
_error = null;
_saving = false;
}
void BadRequest(IDictionary<string, string[]> errors)
{
_error = errors.SelectMany(x => x.Value).FirstOrDefault() ?? "Unknown error";
_saving = false;
}
void AuthError()
{
_error = "Authentication error";
_saving = false;
}
_saving = true;
await AccountsClientService.PutAccountProfileInfo(_accountProfileInfo, Success, BadRequest, AuthError);
}
#endregion
}

View File

@@ -0,0 +1,5 @@
<div class="panel panel-section-header">
<div class="d-flex">
<h3 class="fw-bold m-0">Profile settings</h3>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div class="panel" role="button" @onclick="@(() => NavigationManager.NavigateTo("/user"))">
<div class="d-flex gap-3 align-items-center">
<AccountPictureComponent @ref="_accountPicture" Id="@(User.Id)" Size="50"/>
<div class="d-flex-inline flex-column">
<h2 id="username" class="fw-bold m-0">@(User.Username)</h2>
<span id="secondaryText" class="text-secondary">User settings</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Website.Components.Common.Subcomponents;
using WatchIt.Website.Services.Authentication;
namespace WatchIt.Website.Components.Pages.UserEditPage.Panels;
public partial class UserEditPageHeaderPanelComponent : ComponentBase
{
#region SERVICES
[Inject] public NavigationManager NavigationManager { get; set; } = default!;
#endregion
#region PARAMETERS
[Parameter] public required User User { get; set; }
#endregion
#region FIELDS
private AccountPictureComponent _accountPicture = default!;
#endregion
#region PUBLIC METHODS
public async Task ReloadPicture() => await _accountPicture.Reload();
#endregion
}

View File

@@ -0,0 +1,9 @@
/* IDS */
#username {
margin-top: -8px !important;
}
#secondaryText {
color: lightgray !important;
}

View File

@@ -1,26 +1,26 @@
<div id="base" class="vstack">
<AccountPictureComponent Class="shadow position-absolute z-1 start-50 translate-middle" Id="@(AccountData.Id)" Size="240"/>
<AccountPictureComponent Class="shadow position-absolute z-1 start-50 translate-middle" Id="@(AccountProfileInfoData.Id)" Size="240"/>
<div class="panel z-0">
<div class="vstack gap-3">
<div id="space" class="container-grid"></div>
<div class="d-flex justify-content-center">
<h3 class="fw-bold">@(AccountData.Username)</h3>
<h3 class="fw-bold m-0">@(AccountProfileInfoData.Username)</h3>
</div>
@if (!string.IsNullOrWhiteSpace(AccountData.Description))
@if (!string.IsNullOrWhiteSpace(AccountProfileInfoData.Description))
{
<span>
@(AccountData.Description)
<span class="text-center w-100 mb-2">
@(AccountProfileInfoData.Description)
</span>
}
<div class="d-flex flex-wrap justify-content-center metadata-pill-container">
<div class="metadata-pill"><strong>Email:</strong> @(AccountData.Email)</div>
@if (!string.IsNullOrWhiteSpace(AccountData.Gender?.Name))
<div class="metadata-pill"><strong>Email:</strong> @(AccountProfileInfoData.Email)</div>
@if (!string.IsNullOrWhiteSpace(AccountProfileInfoData.Gender?.Name))
{
<div class="metadata-pill"><strong>Gender:</strong> @(AccountData.Gender?.Name)</div>
<div class="metadata-pill"><strong>Gender:</strong> @(AccountProfileInfoData.Gender?.Name)</div>
}
<div class="metadata-pill"><strong>Account created:</strong> @(AccountData.CreationDate.ToShortDateString())</div>
<div class="metadata-pill"><strong>Last active:</strong> @(AccountData.LastActive.ToShortDateString())</div>
@if (AccountData.IsAdmin)
<div class="metadata-pill"><strong>Account created:</strong> @(AccountProfileInfoData.CreationDate.ToShortDateString())</div>
<div class="metadata-pill"><strong>Last active:</strong> @(AccountProfileInfoData.LastActive.ToShortDateString())</div>
@if (AccountProfileInfoData.IsAdmin)
{
<div class="metadata-pill"><strong>Admin</strong></div>
}

View File

@@ -18,7 +18,7 @@ public partial class UserPageHeaderPanelComponent : ComponentBase
#region PARAMETERS
[Parameter] public required AccountResponse AccountData { get; set; }
[Parameter] public required AccountResponse AccountProfileInfoData { get; set; }
#endregion
@@ -43,7 +43,7 @@ public partial class UserPageHeaderPanelComponent : ComponentBase
// STEP 0
endTasks.AddRange(
[
AccountsClientService.GetAccountProfilePicture(AccountData.Id, data => _accountProfilePicture = data),
AccountsClientService.GetAccountProfilePicture(AccountProfileInfoData.Id, data => _accountProfilePicture = data),
]);
// END

View File

@@ -55,7 +55,7 @@
<Dropdown RightAligned>
<Button Color="Color.Default" Clicked="@(() => NavigationManager.NavigateTo("/user"))">
<div class="d-flex gap-2 align-items-center">
<AccountPictureComponent Id="@(_user.Id)" Size="30"/>
<AccountPictureComponent @ref="_profilePicture" Id="@(_user.Id)" Size="30"/>
<span>@(_user.Username)</span>
</div>
</Button>
@@ -80,7 +80,7 @@
</div>
</div>
</div>
<div class="row pb-3">
<div class="row pt-3 pb-3">
<div class="col">
@Body
</div>

View File

@@ -2,6 +2,7 @@ using System.Net;
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model.Accounts;
using WatchIt.Common.Model.Photos;
using WatchIt.Website.Components.Common.Subcomponents;
using WatchIt.Website.Services.Authentication;
using WatchIt.Website.Services.Tokens;
using WatchIt.Website.Services.Client.Accounts;
@@ -27,6 +28,8 @@ public partial class MainLayout : LayoutComponentBase
#region FIELDS
private AccountPictureComponent? _profilePicture;
private bool _loaded;
private User? _user;
@@ -56,6 +59,20 @@ public partial class MainLayout : LayoutComponentBase
#region PUBLIC METHODS
public async Task ReloadProfilePicture()
{
if (_profilePicture is not null)
{
await _profilePicture.Reload();
}
}
#endregion
#region PRIVATE METHODS
#region Main

View File

@@ -1 +1,82 @@
@using System.Text
@using WatchIt.Common.Model
@using WatchIt.Website.Components.Pages.UserEditPage.Panels
@page "/user/edit"
@{
StringBuilder sb = new StringBuilder(" - WatchIt");
if (_user is null) sb.Insert(0, "Loading...");
else sb.Insert(0, "User settings");
<PageTitle>@(sb.ToString())</PageTitle>
}
<div class="container-grid">
@if (_user is not null)
{
<div class="row">
<div class="col">
<UserEditPageHeaderPanelComponent @ref="@(_header)" User="@(_user)"/>
</div>
</div>
<div class="row mt-default">
<div class="col">
<Tabs Pills
RenderMode="TabsRenderMode.LazyLoad"
Class="panel panel-menu panel-background-menu justify-content-center"
SelectedTab="profile">
<Items>
<Tab Name="profile">Profile</Tab>
<Tab Name="account">Account</Tab>
</Items>
<Content>
<TabPanel Name="profile">
<div class="mt-default">
<div class="container-grid">
<div class="row">
<div class="col">
<ProfileEditHeaderPanelComponent/>
</div>
</div>
<div class="row mt-default gx-default">
<div class="col-auto">
<PictureEditorPanelComponent Id="@(_user.Id)"
Class="h-100"
PicturePlaceholder="assets/user_placeholder.png"
Circle="true"
PictureGetTask="@((id, action) => AccountsClientService.GetAccountProfilePicture(id, action))"
PicturePutTask="@((_, picture, action) => AccountsClientService.PutAccountProfilePicture(new AccountProfilePictureRequest(picture), action))"
PictureDeleteTask="@((_, action) => AccountsClientService.DeleteAccountProfilePicture(action))"
OnPictureChanged="@(async (_) => await PictureChanged())"/>
</div>
<div class="col">
<ProfileEditFormPanelComponent Id="@(_user.Id)"
Class="h-100"/>
</div>
</div>
</div>
</div>
</TabPanel>
<TabPanel Name="account">
</TabPanel>
</Content>
</Tabs>
</div>
</div>
}
else
{
<div class="row">
<div class="col">
<div class="m-5">
<LoadingComponent/>
</div>
</div>
</div>
}
</div>

View File

@@ -1,7 +1,68 @@
using System.Net;
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model;
using WatchIt.Website.Components.Pages.UserEditPage.Panels;
using WatchIt.Website.Layout;
using WatchIt.Website.Services.Authentication;
using WatchIt.Website.Services.Client.Accounts;
namespace WatchIt.Website.Pages;
public partial class UserEditPage : ComponentBase
{
#region SERVICES
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private IAuthenticationService AuthenticationService { get; set; } = default!;
[Inject] private IAccountsClientService AccountsClientService { get; set; } = default!;
#endregion
#region PARAMETERS
[CascadingParameter] public MainLayout Layout { get; set; } = default!;
#endregion
#region FIELDS
private User? _user;
private UserEditPageHeaderPanelComponent _header = default!;
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Layout.BackgroundPhoto = null;
_user = await AuthenticationService.GetUserAsync();
if (_user is null)
{
NavigationManager.NavigateTo($"/auth?redirect_to={WebUtility.UrlEncode("/user/edit")}");
}
else
{
StateHasChanged();
}
}
}
private async Task PictureChanged() => await Task.WhenAll(
[
_header.ReloadPicture(),
Layout.ReloadProfilePicture()
]);
#endregion
}

View File

@@ -48,7 +48,7 @@
{
<div class="row mt-header">
<div class="col">
<UserPageHeaderPanelComponent AccountData="@(_accountData)"/>
<UserPageHeaderPanelComponent AccountProfileInfoData="@(_accountData)"/>
</div>
</div>
<div class="row mt-over-panel-menu">

View File

@@ -1,3 +1,4 @@
using System.Net;
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model.Accounts;
using WatchIt.Website.Layout;
@@ -75,7 +76,7 @@ public partial class UserPage : ComponentBase
{
if (user is null)
{
NavigationManager.NavigateTo("/auth");
NavigationManager.NavigateTo($"/auth?redirect_to={WebUtility.UrlEncode("/user")}");
_redirection = true;
return;
}

View File

@@ -19,11 +19,13 @@
"Base": "/accounts",
"Register": "/register",
"Authenticate": "/authenticate",
"AuthenticateRefresh": "/authenticate-refresh",
"AuthenticateRefresh": "/authenticate_refresh",
"Logout": "/logout",
"GetProfilePicture": "/{0}/profile-picture",
"GetAccountProfilePicture": "/{0}/profile_picture",
"PutAccountProfilePicture": "/profile_picture",
"DeleteAccountProfilePicture": "/profile_picture",
"GetAccountInfo": "/{0}/info",
"PutAccountInfo": "/info",
"PutAccountProfileInfo": "/profile_info",
"GetAccountRatedMovies": "/{0}/movies",
"GetAccountRatedSeries": "/{0}/series",
"GetAccountRatedPersons": "/{0}/persons"

View File

@@ -28,7 +28,7 @@
gap: 1rem;
padding: 1rem 1.5rem !important;
background-color: rgba(0, 0, 0, 0.8);
background-color: rgba(0, 0, 0, 0.7);
}
.panel-menu > li > a {
@@ -52,6 +52,14 @@
/* SECTION HEADER */
.panel-section-header {
padding: 0.75rem 1.5rem;
}
/* BACKGROUNDS */
.panel-background-gold {