Refactoring, database structure changed

This commit is contained in:
2025-03-03 00:56:32 +01:00
Unverified
parent d3805ef3db
commit c603c41c0b
913 changed files with 21764 additions and 32775 deletions

View File

@@ -0,0 +1,9 @@
@inherits Component
<Image Class="@(Class)"
Height="Size"
Circle="true"
Shadow="false"
Placeholder="assets/placeholders/user.png"
AlternativeText="avatar"
Content="@(ProfilePictureIncluded ? Item.ProfilePicture : _picture)"/>

View File

@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Components;
using Refit;
using WatchIt.DTO.Models.Controllers.Accounts.Account;
using WatchIt.DTO.Models.Generics.Image;
using WatchIt.Website.Clients;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class AccountPicture : Component
{
#region SERVICES
[Inject] private IAccountsClient AccountsClient { get; set; } = default!;
#endregion
#region PARAMETERS
[Parameter] public required AccountResponse Item { get; set; }
[Parameter] public bool ProfilePictureIncluded { get; set; }
[Parameter] public required int Size { get; set; }
[Parameter] public string Class { get; set; } = string.Empty;
#endregion
#region FIELDS
private ImageResponse? _picture;
#endregion
#region PRIVATE METHODS
protected override async Task OnFirstRenderAsync()
{
if (ProfilePictureIncluded)
{
_picture = Item.ProfilePicture;
}
else
{
IApiResponse<ImageResponse> response = await AccountsClient.GetAccountProfilePicture(Item.Id);
if (response.IsSuccessful)
{
_picture = response.Content;
}
}
StateHasChanged();
}
#endregion
}

View File

@@ -0,0 +1,26 @@
@inherits Component
@if (BaseLayout.AuthorizationLoaded)
{
if (BaseLayout.AuthorizedAccount is null || (Admin && !BaseLayout.AuthorizedAccount.IsAdmin))
{
if (NotAuthorized is not null)
{
@(NotAuthorized)
}
}
else
{
if (Authorized is not null)
{
@(Authorized)
}
}
}
else
{
if (Loading is not null)
{
@(Loading)
}
}

View File

@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Website.Components.Layout;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class Authorization : Component
{
#region PARAMETERS
[CascadingParameter] public required BaseLayout BaseLayout { get; set; }
[Parameter] public RenderFragment? NotAuthorized { get; set; }
[Parameter] public RenderFragment? Authorized { get; set; }
[Parameter] public RenderFragment? Loading { get; set; }
[Parameter] public bool Admin { get; set; }
#endregion
}

View File

@@ -0,0 +1,54 @@
@using Blazorise.Extensions
@using WatchIt.DTO.Models.Generics.Rating
@inherits Component
<div class="d-flex align-items-center gap-2">
@if (Rating is not IRatingOverallResponse overallRating || overallRating.Count > 0 || EmptyMode == DisplayRatingComponentEmptyMode.DoubleDash)
{
<i id="star" class="fas fa-star"></i>
}
<div class="vstack">
@switch (Rating)
{
case RatingUserResponse userRating:
<span id="ratingSingleLine">@($"{userRating.Rating}/10")</span>
break;
case IRatingOverallResponse { Count: > 0 } overallResponse:
<span id="ratingAverage">@($"{Math.Round(overallResponse.Rating!.Value, 2)}/10")</span>
<span id="ratingCount">@(overallResponse.Count)</span>
break;
default:
<div id="ratingSingleLine">
@switch (EmptyMode)
{
case DisplayRatingComponentEmptyMode.NoRatings: @("no ratings"); break;
case DisplayRatingComponentEmptyMode.DoubleDash: @("--/10"); break;
}
</div>
break;
}
</div>
</div>
<style>
#star {
font-size: @((2 * Scale).ToCultureInvariantString())rem;
}
#ratingAverage {
margin-top: @((-5 * Scale).ToCultureInvariantString())px;
font-size: @((1.3 * Scale).ToCultureInvariantString())rem;
}
#ratingSingleLine {
font-size: @((1.3 * Scale).ToCultureInvariantString())rem;
}
#ratingCount {
margin-top: @((-5 * Scale).ToCultureInvariantString())px;
font-size: @((0.8 * Scale).ToCultureInvariantString())rem;
}
</style>

View File

@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Components;
using WatchIt.DTO.Models.Generics.Rating;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class DisplayRating : Component
{
#region PARAMETERS
[Parameter] public required IRatingResponse? Rating { get; set; }
[Parameter] public DisplayRatingComponentEmptyMode EmptyMode { get; set; } = DisplayRatingComponentEmptyMode.NoRatings;
[Parameter] public double Scale { get; set; } = 1;
#endregion
#region ENUMS
public enum DisplayRatingComponentEmptyMode
{
NoRatings,
DoubleDash,
}
#endregion
}

View File

@@ -0,0 +1,20 @@
@inherits Component
<div class="d-flex flex-column align-items-center gap-2 h-100">
<Image Class="w-100" Content="@(_poster)" Placeholder="@(PosterPlaceholder)" AlternativeText="poster"/>
<div class="container-grid">
<div class="row">
@if (Place.HasValue)
{
<div class="col-auto">
<div class="text-center border border-2 border-light rounded-circle place-circle fw-bold">@(Place)</div>
</div>
}
<div class="col">
<div class="d-flex justify-content-@(Place.HasValue ? "end" : "center")">
<div class="pt-05 @(Place.HasValue ? "text-end" : "text-center")">@(Name)</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Components;
using Refit;
using WatchIt.DTO.Models.Generics.Image;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class HorizontalListItem : Component
{
#region PARAMETERS
[Parameter] public int? Place { get; set; }
[Parameter] public required string Name { get; set; }
[Parameter] public required string PosterPlaceholder { get; set; }
[Parameter] public required Func<Task<ImageResponse?>> GetPosterAction { get; set; }
#endregion
#region FIELDS
private ImageResponse? _poster;
#endregion
#region PRIVATE METHODS
protected override async Task OnFirstRenderAsync()
{
_poster = await GetPosterAction();
StateHasChanged();
}
#endregion
}

View File

@@ -0,0 +1,12 @@
/* CLASSES */
.border-2 {
border-width: 2px;
}
.place-circle {
width: 30px;
height: 30px;
vertical-align: middle;
line-height: 25px;
}

View File

@@ -0,0 +1,3 @@
@inherits Component
<img class="@(Circle ? "rounded-circle" : "rounded-2") @(Shadow ? "shadow" : string.Empty) object-fit-cover @(Class)" src="@(Content?.ToString() ?? Placeholder)" alt="@(AlternativeText)" @attributes="@(_attributes)" style="aspect-ratio: @(AspectRatio.ToString());"/>

View File

@@ -0,0 +1,95 @@
using Microsoft.AspNetCore.Components;
using WatchIt.DTO.Models.Generics.Image;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class Image : Component
{
#region PARAMETERS
[Parameter] public ImageBase? Content { get; set; }
[Parameter] public required string Placeholder { get; set; }
[Parameter] public ImageComponentAspectRatio AspectRatio { get; set; } = ImageComponentAspectRatio.Default;
[Parameter] public string AlternativeText { get; set; } = "picture";
[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
#region FIELDS
private Dictionary<string, object> _attributes = [];
#endregion
#region PRIVATE METHODS
protected override void OnParametersSet()
{
_attributes.Clear();
if (Height.HasValue)
{
_attributes.Add("height", Height.Value);
}
else if (Width.HasValue)
{
_attributes.Add("width", Width.Value);
}
if (Circle)
{
AspectRatio = ImageComponentAspectRatio.Square;
}
}
#endregion
#region STRUCTS
public struct ImageComponentAspectRatio
{
#region Properties
public int Vertical { get; set; }
public int Horizontal { get; set; }
#endregion
#region Constructors
public ImageComponentAspectRatio() : this(3, 5) {}
public ImageComponentAspectRatio(int horizontal, int vertical)
{
Horizontal = horizontal;
Vertical = vertical;
}
public static readonly ImageComponentAspectRatio Default = new ImageComponentAspectRatio();
public static readonly ImageComponentAspectRatio Photo = new ImageComponentAspectRatio(16, 9);
public static readonly ImageComponentAspectRatio Square = new ImageComponentAspectRatio(1, 1);
#endregion
#region Public methods
public override string ToString() => $"{Horizontal}/{Vertical}";
#endregion
}
#endregion
}

View File

@@ -0,0 +1,12 @@
@inherits Component
<div class="d-flex flex-column">
<div class="d-flex justify-content-center">
<div id="spinner" class="spinner-border text-@(GetColor())"></div>
</div>
<div class="d-flex justify-content-center">
<p id="text" class="text-@(GetColor())" m-0>Loading...</p>
</div>
</div>

View File

@@ -0,0 +1,47 @@
using System.ComponentModel;
using System.Reflection;
using Microsoft.AspNetCore.Components;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class Loading : Component
{
#region PARAMETERS
[Parameter] public Colors Color { get; set; } = Colors.Dark;
#endregion
#region PRIVATE METHODS
private string GetColor()
{
DescriptionAttribute? attribute = Color.GetType()
.GetTypeInfo()
.GetMember(Color.ToString())
.FirstOrDefault(member => member.MemberType == MemberTypes.Field)!
.GetCustomAttributes(typeof(DescriptionAttribute), false)
.SingleOrDefault()
as DescriptionAttribute;
return attribute!.Description;
}
#endregion
#region ENUMS
public enum Colors
{
[Description("dark")]
Dark,
[Description("light")]
Light,
}
#endregion
}

View File

@@ -0,0 +1,11 @@
/* IDS */
#spinner {
width: 5rem;
height: 5rem;
}
#text {
font-size: 25px;
border-width: 0.4rem;
}

View File

@@ -0,0 +1,15 @@
@if (IsLoading)
{
<LoadingInline Content="@(LoadingContent)"/>
}
else
{
if (ChildContent is null)
{
<span>@Content</span>
}
else
{
@(ChildContent)
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Components;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class LoadingButtonContent : 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
}

View File

@@ -0,0 +1,6 @@
@inherits Component
<div class="d-flex gap-2 align-items-center">
<span class="spinner-border spinner-border-sm" role="status"></span>
<span>@(Content)</span>
</div>

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Components;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class LoadingInline : Component
{
#region PARAMETERS
[Parameter] public string Content { get; set; } = "Loading...";
#endregion
}

View File

@@ -0,0 +1,9 @@
@inherits Component
<div class="d-flex gap-2 align-items-center">
<div class="input-group input-group-sm">
<InputText class="form-control" placeholder="Search" @bind-Value="@(_searchText)"/>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="@(Search)">⌕</button>
<button type="button" class="btn btn-sm" @onclick="@(() => OnCloseButtonClicked?.Invoke())">&#10060;&#xFE0E;</button>
</div>

View File

@@ -0,0 +1,46 @@
using System.Net;
using Microsoft.AspNetCore.Components;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class Searchbar : Component
{
#region SERVICES
[Inject] private NavigationManager NavigationManager { get; set; } = null!;
#endregion
#region PARAMETERS
[Parameter] public Action? OnCloseButtonClicked { get; set; }
[Parameter] public Action? OnSearchButtonClicked { get; set; }
#endregion
#region FIELDS
private string? _searchText;
#endregion
#region PRIVATE METHODS
public void Search()
{
OnSearchButtonClicked?.Invoke();
if (!string.IsNullOrWhiteSpace(_searchText))
{
string query = WebUtility.UrlEncode(_searchText);
NavigationManager.NavigateTo($"/search/{query}", true);
}
}
#endregion
}

View File

@@ -0,0 +1,14 @@
@inherits Component
<div class="container-grid">
<div class="row gx-3">
<div class="col">
<span id="title" class="fw-bold">@(Title)</span>
</div>
<div class="col-auto align-self-center">
<DisplayRating Rating="@(Rating)"/>
</div>
</div>
</div>

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Components;
using WatchIt.DTO.Models.Generics.Rating;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class TitledDisplayRating : Component
{
#region PARAMETERS
[Parameter] public required IRatingResponse? Rating { get; set; }
[Parameter] public required string Title { get; set; }
#endregion
}

View File

@@ -0,0 +1,5 @@
/* IDS */
#title {
font-size: 1.5rem;
}

View File

@@ -0,0 +1,96 @@
@inherits Component
<div class="container-grid">
<div class="row">
<div class="col-auto">
<a class="text-reset text-decoration-none" href="@(ItemUrl)">
<Image Content="@(_picture)" Placeholder="@(PicturePlaceholder)" AlternativeText="poster" Height="@(PictureHeight)"/>
</a>
</div>
<div class="col">
<div class="d-flex align-items-start flex-column h-100">
<div class="mb-auto">
<a style="font-size: @(NameSize)px" class="text-reset text-decoration-none" href="@(ItemUrl)">
<strong>@(Name)</strong>@(string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo)
</a>
</div>
<table id="ratingTable" class="table table-transparent table-bordered align-middle m-0">
<thead>
<tr>
@if (GlobalRating is not null)
{
<th scope="col">
<span class="rating-name-text">Global rating:</span>
</th>
}
@if (_secondaryRating is not null)
{
<th scope="col">
<span class="rating-name-text">@(SecondaryRatingTitle):</span>
</th>
}
@if (GetYourRatingMethod is not null && PutYourRatingMethod is not null && DeleteYourRatingMethod is not null)
{
<th class scope="col">
<span class="rating-name-text">Your rating:</span>
</th>
}
</tr>
</thead>
<tbody>
<tr>
@if (GlobalRating is not null)
{
<td>
@switch (_globalRatingLoaded)
{
case true: <DisplayRating Rating="@(GlobalRating)"
EmptyMode="DisplayRating.DisplayRatingComponentEmptyMode.DoubleDash"
Scale="0.85"/> break;
case false: <LoadingInline/> break;
}
</td>
}
@if (_secondaryRating is not null)
{
<td>
@switch (_secondaryRatingLoaded)
{
case true: <DisplayRating Rating="@(_secondaryRating)"
EmptyMode="DisplayRating.DisplayRatingComponentEmptyMode.DoubleDash"
Scale="0.85"/> break;
case false: <LoadingInline/> break;
}
</td>
}
@if (GetYourRatingMethod is not null && PutYourRatingMethod is not null && DeleteYourRatingMethod is not null)
{
<td>
<div class="d-inline-flex align-items-center h-100">
<Authorization>
<Loading>
<LoadingInline/>
</Loading>
<NotAuthorized>
<span id="ratingLoginInfoText">You must be logged in to rate</span>
</NotAuthorized>
<Authorized>
@switch (_yourRatingLoaded)
{
case true: <Blazorise.Rating Color="Blazorise.Color.Light" MaxValue="10" @bind-SelectedValue="@(_yourRating)" @onclick="@(RatingChanged)"/> break;
case false: <LoadingInline/> break;
}
</Authorized>
</Authorization>
</div>
</td>
}
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,132 @@
using Blazorise.Snackbar;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using WatchIt.DTO.Models.Generics.Image;
using WatchIt.DTO.Models.Generics.Rating;
using WatchIt.Website.Components.Layout;
using WatchIt.Website.Services.Authentication;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class VerticalListItem : Component
{
#region SERVICES
[Inject] private IAuthenticationService AuthenticationService { get; set; } = default!;
#endregion
#region PARAMETERS
[Parameter] public required string Name { get; set; }
[Parameter] public string? AdditionalInfo { get; set; }
[Parameter] public int NameSize { get; set; } = 25;
[Parameter] public required string PicturePlaceholder { get; set; }
[Parameter] public int PictureHeight { get; set; } = 150;
[Parameter] public required Func<Task<ImageResponse?>> PictureFunc { get; set; }
[Parameter] public string? SecondaryRatingTitle { get; set; }
[Parameter] public RatingOverallResponse? GlobalRating { get; set; }
[Parameter] public required Func<Task<RatingOverallResponse?>> GetGlobalRatingMethod { get; set; }
[Parameter] public Func<Task<IRatingResponse?>>? GetSecondaryRatingMethod { get; set; }
[Parameter] public Func<long, Task<int?>>? GetYourRatingMethod { get; set; }
[Parameter] public Func<RatingRequest, Task>? PutYourRatingMethod { get; set; }
[Parameter] public Func<Task>? DeleteYourRatingMethod { get; set; }
[Parameter] public Action? OnRatingChanged { get; set; }
[Parameter] public required string ItemUrl { get; set; }
#endregion
#region FIELDS
private ImageResponse? _picture;
private IRatingResponse? _secondaryRating;
private int _yourRating;
private bool _globalRatingLoaded;
private bool _secondaryRatingLoaded;
private bool _yourRatingLoaded;
#endregion
#region PRIVATE METHODS
protected override async Task OnFirstRenderAsync()
{
await base.OnFirstRenderAsync();
List<Task> tasks =
[
Task.Run(async () => _picture = await PictureFunc()),
];
if (GlobalRating is null)
{
tasks.Add(UpdateGlobalRating());
}
else
{
_globalRatingLoaded = true;
}
if (GetSecondaryRatingMethod is not null && !string.IsNullOrWhiteSpace(SecondaryRatingTitle))
{
tasks.Add(GetSecondaryRatingMethod());
}
if (GetYourRatingMethod is not null && Base.AuthorizedAccount is not null)
{
tasks.Add(GetUserRating());
}
await Task.WhenAll(tasks);
StateHasChanged();
}
private async Task GetUserRating()
{
int? rating = await GetYourRatingMethod!(Base.AuthorizedAccount!.Id);
_yourRating = rating ?? 0;
_yourRatingLoaded = true;
}
private async Task RatingChanged()
{
if (Base.AuthorizedAccount is null)
{
await Base.SnackbarStack.PushAsync("An error has occurred. You are not logged in.", SnackbarColor.Danger);
return;
}
if (_yourRating == 0)
{
await DeleteYourRatingMethod!();
}
else
{
await PutYourRatingMethod!(new RatingRequest
{
Rating = (byte)_yourRating,
});
}
await UpdateGlobalRating();
OnRatingChanged?.Invoke();
}
private async Task UpdateGlobalRating()
{
_globalRatingLoaded = false;
GlobalRating = await GetGlobalRatingMethod();
_globalRatingLoaded = true;
}
#endregion
}

View File

@@ -0,0 +1,39 @@
/* TAGS */
tbody, td, tfoot, th, thead, tr {
border-color: inherit;
border-style: unset;
border-width: 0;
}
/* IDS */
#ratingLoginInfoText {
font-size: 13px;
}
#ratingTable {
width: fit-content;
border-spacing: unset;
border-collapse: separate;
margin-left: -0.5rem !important;
}
#ratingTable > thead > tr > th:not(:last-child) {
border-right: 1px solid #444;
}
#ratingTable > tbody > tr > td:not(:last-child) {
border-right: 1px solid #444;
}
/* CLASSES */
.rating-name-text {
font-size: 14px;
font-weight: bold;
}

View File

@@ -0,0 +1,10 @@
@inherits Component
<div class="d-flex align-items-center gap-4" role="button" @onclick="@(() => NavigationManager.NavigateTo($"/users/{Item.Id}", true))">
<AccountPicture Item="@(Item)"
ProfilePictureIncluded="@(ProfilePictureIncluded)"
Size="@(PictureSize)"/>
<h4 class="fw-bold">@(Item.Username)</h4>
</div>

View File

@@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Components;
using WatchIt.DTO.Models.Controllers.Accounts.Account;
namespace WatchIt.Website.Components.Subcomponents.Common;
public partial class VerticalListUserItem : Component
{
#region SERVICES
[Inject] private NavigationManager NavigationManager { get; set; } = null!;
#endregion
#region PROPERTIES
[Parameter] public required AccountResponse Item { get; set; }
[Parameter] public bool ProfilePictureIncluded { get; set; }
[Parameter] public int PictureSize { get; set; } = 90;
#endregion
}