Чот много сделано.

This commit is contained in:
THE_KONDRAT 2024-10-21 01:52:41 +03:00
parent bad2805994
commit 9a54341fcc
151 changed files with 21587 additions and 538 deletions

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.29.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,89 @@
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using Microsoft.AspNetCore.Mvc;
namespace Common.WebApi.Middlewares;
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly IHostEnvironment _host;
private readonly ILogger _logger;
private readonly JsonSerializerOptions _serializerOptions;
public ExceptionMiddleware(RequestDelegate next, IHostEnvironment host, ILogger<ExceptionMiddleware> logger)
{
_logger = logger;
_next = next;
_host = host;
_serializerOptions = new JsonSerializerOptions
{
Converters = {
new JsonStringEnumConverter()
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
//if (httpContext.Request.Method == "POST")
//{
// using var streamreader = new System.IO.StreamReader(httpContext.Request.Body);
// var body = await streamreader.ReadToEndAsync();
//}
await _next(httpContext);
}
catch (Exception exc)
{
var title = exc.Message;
var detail = "Unexpected error occured";
if (!_host.IsProduction()) detail = GetExceptionDetail(exc, false, true);
if (exc is MongoException dbUpdateException)
{
title = "Database error";
if (!_host.IsProduction()) detail = GetExceptionDetail(dbUpdateException, true, true);
}
else if (exc.Source?.Contains("Mongo") == true)
{
title = "Database error";
if (!_host.IsProduction()) detail = GetExceptionDetail(exc, true, true);
}
var problemDetail = new ProblemDetails
{
Title = title,
Detail = detail,
Instance = httpContext.Request.Path,
Status = StatusCodes.Status400BadRequest
};
_logger.LogError("Необработанная ошибка: {Message}", exc.Message);
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
httpContext.Response.ContentType = "application/problem+json";
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(problemDetail, _serializerOptions));
}
}
private string GetExceptionDetail(Exception exc, bool includeMessage, bool includeInner)
{
var sb = new StringBuilder();
if (!string.IsNullOrWhiteSpace(exc.Message) && includeMessage) sb.Append(exc.Message);
if (!string.IsNullOrWhiteSpace(exc.InnerException?.Message) && includeInner)
{
if (sb.Length > 0) sb.AppendLine();
sb.AppendLine("Inner:");
sb.Append(exc.InnerException.Message);
}
if (sb.Length > 0) sb.AppendLine();
sb.Append(exc.StackTrace);
return sb.ToString();
}
}

View File

@ -0,0 +1,7 @@
namespace Modules.Account.Api
{
public class Class1
{
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,25 @@
using MediatR;
using Modules.Account.Application.Gateways;
using Modules.Account.Domain.Gateways;
namespace Modules.Account.Application.Commands;
public class CreateAccountCommand : IRequest<Guid>
{
public string Email { get; set; } = default!;
public string Password { get; set; } = default!;
}
public class CreateAccountCommandHandler(IAccountGateway gateway, IPasswordHasher passwordHasher) : IRequestHandler<CreateAccountCommand, Guid>
{
public async Task<Guid> Handle(CreateAccountCommand request, CancellationToken cancellationToken)
{
if (await gateway.IsExists(request.Email)) throw new Exception("Account with the same eamil already exists");
var newAccount = Domain.Entities.Account.Create(request.Email, request.Password, passwordHasher);
return await gateway.Create(new Models.Account
{
Email = newAccount.Email.Value,
HashedPassword = newAccount.HashedPassword,
});
}
}

View File

@ -0,0 +1,13 @@
using MediatR;
namespace Modules.Account.Application.Gateways;
public interface IAccountGateway
{
public Task<Models.Account?> TryGetByEmail(string email);
public Task<Models.Account> GetByEmail(string email);
public Task<Guid> Create(Models.Account account);
public Task<bool> Update(Guid id, Models.Account account);
public Task<bool> IsExists(string email);
public Task<bool> Delete(Guid id);
}

View File

@ -0,0 +1,8 @@
namespace Modules.Account.Application.Models;
public class Account
{
public Guid Id { get; set; }
public string Email { get; set; } = default!;
public string HashedPassword { get; set; } = default!;
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Modules.Account.Database\Modules.Account.Database.csproj" />
<ProjectReference Include="..\Modules.Account.Domain\Modules.Account.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Identity;
using Modules.Account.Domain.Gateways;
namespace Modules.Account.Application;
public class PasswordHasher : IPasswordHasher
{
private readonly PasswordHasher<Domain.Entities.Account> _hasher;
public PasswordHasher()
{
_hasher = new();
}
public string HashPassword(Domain.Entities.Account account, string password) =>
_hasher.HashPassword(account, password);
public bool VerifyPassword(Domain.Entities.Account account, string password) =>
_hasher.VerifyHashedPassword(account, password, account.HashedPassword) != PasswordVerificationResult.Failed;
}

View File

@ -0,0 +1,24 @@
using MediatR;
using Modules.Account.Application.Gateways;
using Modules.Account.Domain.Gateways;
namespace Modules.Account.Application.Queries;
public class GetAccountQuery : IRequest<Models.Account>
{
public string Email { get; set; } = default!;
public string Password { get; set; } = default!;
}
public class GetAccountQueryHandler(IAccountGateway accountGateway, IPasswordHasher passwordHasher) : IRequestHandler<GetAccountQuery, Models.Account>
{
const string error = "Invalid email or password";
public async Task<Models.Account> Handle(GetAccountQuery request, CancellationToken cancellationToken)
{
var account = await accountGateway.TryGetByEmail(request.Email);
if (account == null) throw new Exception(error);
if (passwordHasher.VerifyPassword(new Domain.Entities.Account(account.Id, account.Email, account.HashedPassword), request.Password))
throw new Exception(error);
return account;
}
}

View File

@ -0,0 +1,7 @@
namespace Modules.Account.Database
{
public class Class1
{
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,45 @@
using Modules.Account.Domain.Gateways;
namespace Modules.Account.Domain.Entities;
public class Account
{
public Guid Id { get; set; }
public Email Email { get; private set; }
public string HashedPassword { get; private set; }
public Account(Guid id, string email, string hashedPassword)
{
Id = id;
Email = new Email(email);
HashedPassword = hashedPassword;
}
private Account(Email email)
{
Email = email;
HashedPassword = "";
}
public static Account Create(string email, string password, IPasswordHasher passwordHasher)
{
var account = new Account(new Email(email));
var a = passwordHasher.HashPassword(account, password);
return account;
}
public void SetPassword(string email)
{
//var newPasswordHash = passwordHasher.HashPassword(password);
//if (newPasswordHash != HashedPassword) throw new Exception("Password must not be aqual to previous");
if (string.IsNullOrWhiteSpace(email)) throw new Exception("Email is empty");
Email = new Email(email);
}
public void SetPassword(string password, IPasswordHasher passwordHasher)
{
var newPasswordHash = passwordHasher.HashPassword(this, password);
if (newPasswordHash != HashedPassword) throw new Exception("Password must not be aqual to previous");
HashedPassword = newPasswordHash;
}
}

View File

@ -0,0 +1,11 @@
namespace Modules.Account.Domain.Entities;
public class Email
{
public string Value { get; private set; }
public Email(string value)
{
Value = value;
}
}

View File

@ -0,0 +1,7 @@
namespace Modules.Account.Domain.Gateways;
public interface IPasswordHasher
{
public string HashPassword(Entities.Account account, string password);
public bool VerifyPassword(Entities.Account account, string password);
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -12,8 +12,8 @@ public class EditDescriptionCommand : IRequest<Unit>
public Guid LanguageId { get; set; } public Guid LanguageId { get; set; }
public string Value { get; set; } = default!; public string Value { get; set; } = default!;
public bool IsOriginal { get; set; } public bool IsOriginal { get; set; }
public string NewValue { get; set; } = default!; public Guid? NewLanguageId { get; set; }
public string? NewValue { get; set; }
} }
public class EditDescriptionCommandHandler(IAnimeTitleGateway titleGateway, ILanguageGateway languageGateway) : IRequestHandler<EditDescriptionCommand, Unit> public class EditDescriptionCommandHandler(IAnimeTitleGateway titleGateway, ILanguageGateway languageGateway) : IRequestHandler<EditDescriptionCommand, Unit>
@ -27,7 +27,7 @@ public class EditDescriptionCommandHandler(IAnimeTitleGateway titleGateway, ILan
var episode = season.Episodes.FirstOrDefault(q => q.Id == request.EpisodeId) ?? var episode = season.Episodes.FirstOrDefault(q => q.Id == request.EpisodeId) ??
throw new Exception("Episode not found"); throw new Exception("Episode not found");
var description = new Domain.Entities.MediaContent.CommonProperties.Description(request.LanguageId, request.IsOriginal, request.Value); var description = new Domain.Entities.MediaContent.CommonProperties.Description(request.LanguageId, request.IsOriginal, request.Value);
episode.CommonProperties.SetDescriptionValue(description, request.NewValue); episode.CommonProperties.SetDescriptionValue(description, request.NewLanguageId, request.NewValue);
await titleGateway.Update(title); await titleGateway.Update(title);
return Unit.Value; return Unit.Value;
} }

View File

@ -13,20 +13,24 @@ public class EditNameCommand : IRequest<Unit>
public CommonModels.NameType NameType { get; set; } public CommonModels.NameType NameType { get; set; }
public Guid LanguageId { get; set; } public Guid LanguageId { get; set; }
public string Value { get; set; } = default!; public string Value { get; set; } = default!;
public string NewValue { get; set; } = default!; public string? NewValue { get; set; }
public Guid? NewLanguageId { get; set; }
} }
public class EditNameCommandHandler(IAnimeTitleGateway titleGateway) : IRequestHandler<EditNameCommand, Unit> public class EditNameCommandHandler(IAnimeTitleGateway titleGateway) : IRequestHandler<EditNameCommand, Unit>
{ {
public async Task<Unit> Handle(EditNameCommand request, CancellationToken cancellationToken) public async Task<Unit> Handle(EditNameCommand request, CancellationToken cancellationToken)
{ {
if (!request.NewLanguageId.HasValue && string.IsNullOrWhiteSpace(request.NewValue))
{
throw new ArgumentNullException($"{nameof(EditNameCommand.NewLanguageId)}, {nameof(EditNameCommand.NewValue)}");
}
var title = await titleGateway.GetById(request.TitleId); var title = await titleGateway.GetById(request.TitleId);
var season = title.Items.OfType<AnimeSeason>().FirstOrDefault(q => q.Id == request.SeasonId) ?? var season = title.Items.OfType<AnimeSeason>().FirstOrDefault(q => q.Id == request.SeasonId) ??
throw new Exception("Season not found"); throw new Exception("Season not found");
var episode = season.Episodes.FirstOrDefault(q => q.Id == request.EpisodeId) ?? var episode = season.Episodes.FirstOrDefault(q => q.Id == request.EpisodeId) ??
throw new Exception("Episode not found"); throw new Exception("Episode not found");
episode.CommonProperties.SetNameValue(new NameItem((NameType)request.NameType, request.LanguageId, request.Value), request.NewValue); episode.CommonProperties.SetNameValue(new NameItem((NameType)request.NameType, request.LanguageId, request.Value), request.NewLanguageId, request.NewValue);
await titleGateway.Update(title); await titleGateway.Update(title);
return Unit.Value; return Unit.Value;
} }

View File

@ -11,8 +11,8 @@ public class EditDescriptionCommand : IRequest<Unit>
public Guid LanguageId { get; set; } public Guid LanguageId { get; set; }
public string Value { get; set; } = default!; public string Value { get; set; } = default!;
public bool IsOriginal { get; set; } public bool IsOriginal { get; set; }
public string NewValue { get; set; } = default!; public string? NewValue { get; set; }
public Guid? NewLanguageId { get; set; }
} }
public class EditDescriptionCommandHandler(IAnimeTitleGateway titleGateway, ILanguageGateway languageGateway) : IRequestHandler<EditDescriptionCommand, Unit> public class EditDescriptionCommandHandler(IAnimeTitleGateway titleGateway, ILanguageGateway languageGateway) : IRequestHandler<EditDescriptionCommand, Unit>
@ -24,7 +24,7 @@ public class EditDescriptionCommandHandler(IAnimeTitleGateway titleGateway, ILan
var description = new Domain.Entities.MediaContent.CommonProperties.Description(request.LanguageId, request.IsOriginal, request.Value); var description = new Domain.Entities.MediaContent.CommonProperties.Description(request.LanguageId, request.IsOriginal, request.Value);
var season = title.Items.OfType<AnimeSeason>().FirstOrDefault(q => q.Id == request.SeasonId) ?? var season = title.Items.OfType<AnimeSeason>().FirstOrDefault(q => q.Id == request.SeasonId) ??
throw new Exception("Season not found"); throw new Exception("Season not found");
season.CommonProperties.SetDescriptionValue(description, request.NewValue); season.CommonProperties.SetDescriptionValue(description, request.NewLanguageId, request.NewValue);
await titleGateway.Update(title); await titleGateway.Update(title);
return Unit.Value; return Unit.Value;
} }

View File

@ -12,18 +12,22 @@ public class EditNameCommand : IRequest<Unit>
public CommonModels.NameType NameType { get; set; } public CommonModels.NameType NameType { get; set; }
public Guid LanguageId { get; set; } public Guid LanguageId { get; set; }
public string Value { get; set; } = default!; public string Value { get; set; } = default!;
public string NewValue { get; set; } = default!; public string? NewValue { get; set; }
public Guid? NewLanguageId { get; set; }
} }
public class EditNameCommandHandler(IAnimeTitleGateway titleGateway) : IRequestHandler<EditNameCommand, Unit> public class EditNameCommandHandler(IAnimeTitleGateway titleGateway) : IRequestHandler<EditNameCommand, Unit>
{ {
public async Task<Unit> Handle(EditNameCommand request, CancellationToken cancellationToken) public async Task<Unit> Handle(EditNameCommand request, CancellationToken cancellationToken)
{ {
if (!request.NewLanguageId.HasValue && string.IsNullOrWhiteSpace(request.NewValue))
{
throw new ArgumentNullException($"{nameof(EditNameCommand.NewLanguageId)}, {nameof(EditNameCommand.NewValue)}");
}
var title = await titleGateway.GetById(request.TitleId); var title = await titleGateway.GetById(request.TitleId);
var season = title.Items.OfType<AnimeSeason>().FirstOrDefault(q => q.Id == request.SeasonId) ?? var season = title.Items.OfType<AnimeSeason>().FirstOrDefault(q => q.Id == request.SeasonId) ??
throw new Exception("Season not found"); throw new Exception("Season not found");
season.CommonProperties.SetNameValue(new NameItem((NameType)request.NameType, request.LanguageId, request.Value), request.NewValue); season.CommonProperties.SetNameValue(new NameItem((NameType)request.NameType, request.LanguageId, request.Value), request.NewLanguageId, request.NewValue);
await titleGateway.Update(title); await titleGateway.Update(title);
return Unit.Value; return Unit.Value;
} }

View File

@ -1,5 +1,6 @@
using MediatR; using MediatR;
using Modules.Library.Application.Gateways; using Modules.Library.Application.Gateways;
using Modules.Library.Application.Models;
namespace Modules.Library.Application.Commands.Anime.Title; namespace Modules.Library.Application.Commands.Anime.Title;
@ -7,6 +8,7 @@ public class CreateAnimeTitleCommand : IRequest<Guid>
{ {
public string NameOriginal { get; set; } = default!; public string NameOriginal { get; set; } = default!;
public Guid NameOriginalLanguageId { get; set; } public Guid NameOriginalLanguageId { get; set; }
public MediaInfo? Preview { get; set; }
} }
public class CreateAnimtTitleCommandHandler(IAnimeTitleGateway titleGateway, ILanguageGateway languageGateway) : IRequestHandler<CreateAnimeTitleCommand, Guid> public class CreateAnimtTitleCommandHandler(IAnimeTitleGateway titleGateway, ILanguageGateway languageGateway) : IRequestHandler<CreateAnimeTitleCommand, Guid>
@ -15,6 +17,7 @@ public class CreateAnimtTitleCommandHandler(IAnimeTitleGateway titleGateway, ILa
{ {
var language = await languageGateway.GetLanguageById(request.NameOriginalLanguageId); var language = await languageGateway.GetLanguageById(request.NameOriginalLanguageId);
var animeTitle = new Domain.Entities.MediaContent.Items.Anime.AnimeTitle(language.Id, request.NameOriginal); var animeTitle = new Domain.Entities.MediaContent.Items.Anime.AnimeTitle(language.Id, request.NameOriginal);
if (request.Preview != null) animeTitle.CommonProperties.SetPreview(request.Preview.Url, (Domain.Entities.MediaInfoType)request.Preview.Type);
return await titleGateway.Create(animeTitle); return await titleGateway.Create(animeTitle);
} }
} }

View File

@ -9,7 +9,8 @@ public class EditDescriptionCommand : IRequest<Unit>
public Guid LanguageId { get; set; } public Guid LanguageId { get; set; }
public string Value { get; set; } = default!; public string Value { get; set; } = default!;
public bool IsOriginal { get; set; } public bool IsOriginal { get; set; }
public string NewValue { get; set; } = default!; public Guid? NewLanguageId { get; set; }
public string? NewValue { get; set; }
} }
@ -20,7 +21,7 @@ public class EditDescriptionCommandHandler(IAnimeTitleGateway titleGateway, ILan
var title = await titleGateway.GetById(request.TitleId); var title = await titleGateway.GetById(request.TitleId);
if (!await languageGateway.IsLanguageExists(request.LanguageId)) throw new Exception(); if (!await languageGateway.IsLanguageExists(request.LanguageId)) throw new Exception();
var description = new Domain.Entities.MediaContent.CommonProperties.Description(request.LanguageId, request.IsOriginal, request.Value); var description = new Domain.Entities.MediaContent.CommonProperties.Description(request.LanguageId, request.IsOriginal, request.Value);
title.CommonProperties.SetDescriptionValue(description, request.NewValue); title.CommonProperties.SetDescriptionValue(description, request.NewLanguageId, request.NewValue);
await titleGateway.Update(title); await titleGateway.Update(title);
return Unit.Value; return Unit.Value;
} }

View File

@ -10,16 +10,20 @@ public class EditNameCommand : IRequest<Unit>
public CommonModels.NameType NameType { get; set; } public CommonModels.NameType NameType { get; set; }
public Guid LanguageId { get; set; } public Guid LanguageId { get; set; }
public string Value { get; set; } = default!; public string Value { get; set; } = default!;
public string NewValue { get; set; } = default!; public Guid? NewLanguageId { get; set; }
public string? NewValue { get; set; } = default!;
} }
public class EditNameCommandHandler(IAnimeTitleGateway titleGateway) : IRequestHandler<EditNameCommand, Unit> public class EditNameCommandHandler(IAnimeTitleGateway titleGateway) : IRequestHandler<EditNameCommand, Unit>
{ {
public async Task<Unit> Handle(EditNameCommand request, CancellationToken cancellationToken) public async Task<Unit> Handle(EditNameCommand request, CancellationToken cancellationToken)
{ {
if (!request.NewLanguageId.HasValue && string.IsNullOrWhiteSpace(request.NewValue))
{
throw new ArgumentNullException($"{nameof(EditNameCommand.NewLanguageId)}, {nameof(EditNameCommand.NewValue)}");
}
var title = await titleGateway.GetById(request.TitleId); var title = await titleGateway.GetById(request.TitleId);
title.CommonProperties.SetNameValue(new NameItem((NameType)request.NameType, request.LanguageId, request.Value), request.NewValue); title.CommonProperties.SetNameValue(new NameItem((NameType)request.NameType, request.LanguageId, request.Value), request.NewLanguageId, request.NewValue);
await titleGateway.Update(title); await titleGateway.Update(title);
return Unit.Value; return Unit.Value;
} }

View File

@ -0,0 +1,25 @@
using MediatR;
using Modules.Library.Application.Gateways;
using Modules.Rating.Api.Commands;
namespace Modules.Library.Application.Commands.Anime.Title;
public class RateTitleCommand : IRequest<Unit>
{
public Guid TitleId { get; set; }
public ushort RatePercentage { get; set; }
}
public class RateTitleCommandHandler(IAnimeTitleGateway titleGateway, IMediator mediator) : IRequestHandler<RateTitleCommand, Unit>
{
public async Task<Unit> Handle(RateTitleCommand request, CancellationToken cancellationToken)
{
var subjectId = new Guid("8393230f-78e3-473b-a5dc-3221917e0aeb"); //user
var title = await titleGateway.GetById(request.TitleId);
if (title != null && !title.Deleted)
{
await mediator.Send(new RateObjectCommand { ObjectId = title.Id, SubjectId = subjectId, RatePercentage = request.RatePercentage });
}
return Unit.Value;
}
}

View File

@ -0,0 +1,24 @@
using MediatR;
using Modules.Library.Application.Gateways;
using Modules.Rating.Api.Commands;
namespace Modules.Library.Application.Commands.Anime.Title;
public class UnrateTitleCommand : IRequest<Unit>
{
public Guid TitleId { get; set; }
}
public class UnrateTitleCommandHandler(IAnimeTitleGateway titleGateway, IMediator mediator) : IRequestHandler<UnrateTitleCommand, Unit>
{
public async Task<Unit> Handle(UnrateTitleCommand request, CancellationToken cancellationToken)
{
var subjectId = new Guid("8393230f-78e3-473b-a5dc-3221917e0aeb"); //user
var title = await titleGateway.GetById(request.TitleId);
if (title != null && !title.Deleted)
{
await mediator.Send(new UnrateObjectCommand { ObjectId = title.Id, SubjectId = subjectId });
}
return Unit.Value;
}
}

View File

@ -3,4 +3,5 @@ public class MediaInfo
{ {
public MediaInfoType Type { get; set; } public MediaInfoType Type { get; set; }
public string Url { get; set; } = default!; public string Url { get; set; } = default!;
public string ContentType { get; set; } = default!;
} }

View File

@ -8,11 +8,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Modules.Library.Domain\Modules.Library.Domain.csproj" /> <ProjectReference Include="..\Modules.Library.Domain\Modules.Library.Domain.csproj" />
<ProjectReference Include="..\Modules.Rating.Api\Modules.Rating.Api.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,17 +1,26 @@
using MediatR; using MediatR;
using Modules.Library.Application.Gateways; using Modules.Library.Application.Gateways;
using Modules.Rating.Api.Querirs;
namespace Modules.Library.Application.Queries.Anime.AnimeTitle; namespace Modules.Library.Application.Queries.Anime.AnimeTitle;
public class AnimeTitleListQuery : IRequest<List<Models.Anime.Title>> public class AnimeTitleListQuery : IRequest<List<Models.Anime.Title>>
{ {
public Guid? UserId { get; set; }
} }
public class AnimeTitleListQueryHandler(IAnimeTitleGateway titleGateway) : IRequestHandler<AnimeTitleListQuery, List<Models.Anime.Title>> public class AnimeTitleListQueryHandler(IAnimeTitleGateway titleGateway, IMediator mediator) : IRequestHandler<AnimeTitleListQuery, List<Models.Anime.Title>>
{ {
public async Task<List<Models.Anime.Title>> Handle(AnimeTitleListQuery request, CancellationToken cancellationToken) public async Task<List<Models.Anime.Title>> Handle(AnimeTitleListQuery request, CancellationToken cancellationToken)
{ {
return await titleGateway.GetList(); var titles = await titleGateway.GetList();
var rates = await mediator.Send(new ObjectRatingListQuery { ObjectIds = titles.Select(q => q.Id), SubjectId = request.UserId, });
rates.ForEach(q =>
{
var title = titles.First(x => x.Id == q.ObjectId);
title.Rate = q.ObjectRatePercentage;
title.MyRate = q.SubjectRatePercentage;
});
return titles;
} }
} }

View File

@ -1,5 +1,6 @@
using MediatR; using MediatR;
using Modules.Library.Application.Gateways; using Modules.Library.Application.Gateways;
using Modules.Rating.Api.Querirs;
namespace Modules.Library.Application.Queries.Anime.AnimeTitle; namespace Modules.Library.Application.Queries.Anime.AnimeTitle;
@ -8,10 +9,15 @@ public class AnimeTitleQuery : IRequest<Models.Anime.Title>
public Guid Id { get; set; } public Guid Id { get; set; }
} }
public class AnimeTitleQueryHandler(IAnimeTitleGateway titleGateway) : IRequestHandler<AnimeTitleQuery, Models.Anime.Title> public class AnimeTitleQueryHandler(IAnimeTitleGateway titleGateway, IMediator mediator) : IRequestHandler<AnimeTitleQuery, Models.Anime.Title>
{ {
public async Task<Models.Anime.Title> Handle(AnimeTitleQuery request, CancellationToken cancellationToken) public async Task<Models.Anime.Title> Handle(AnimeTitleQuery request, CancellationToken cancellationToken)
{ {
return await titleGateway.GetDetail(request.Id); var title = await titleGateway.GetDetail(request.Id);
//var rate = await mediator.Send(new ObjectRatingQuery { ObjectId = request.Id, SubjectId = null, });
var rate = await mediator.Send(new ObjectRatingQuery { ObjectId = request.Id, SubjectId = new Guid("8393230f-78e3-473b-a5dc-3221917e0aeb"), });
title.Rate = rate?.ObjectRatePercentage;
title.MyRate = rate?.SubjectRatePercentage;
return title;
} }
} }

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Modules.Rating.Api;
namespace Modules.Library.Application; namespace Modules.Library.Application;

View File

@ -4,6 +4,7 @@ public class MediaInfo : Entity
{ {
public MediaInfoType Type { get; set; } = MediaInfoType.OtherFile; public MediaInfoType Type { get; set; } = MediaInfoType.OtherFile;
public string Url { get; set; } = default!; public string Url { get; set; } = default!;
//public string ContentType { get; set; } = default!;
} }
public enum MediaInfoType public enum MediaInfoType

View File

@ -14,8 +14,7 @@ internal class CommonPropertiesConverter(LanguageRepository languageRepository,
commonPropertiesLanguageIds.AddRange(commonProperties.Descriptions.Select(q => q.LanguageId)); commonPropertiesLanguageIds.AddRange(commonProperties.Descriptions.Select(q => q.LanguageId));
var languages = await languageRepository.GetWhere(q => commonPropertiesLanguageIds.Distinct().Contains(q.Id)); var languages = await languageRepository.GetWhere(q => commonPropertiesLanguageIds.Distinct().Contains(q.Id));
var genres = await genreRepository.GetWhere(q => commonProperties.Genres.Select(q => q.Id).Distinct().Contains(q.Id)); var genres = await genreRepository.GetWhere(q => commonProperties.Genres.Select(x => x.GenreId).Distinct().Contains(q.Id));
return new Application.Models.CommonProperties return new Application.Models.CommonProperties
{ {
@ -29,6 +28,9 @@ internal class CommonPropertiesConverter(LanguageRepository languageRepository,
{ {
Url = commonProperties.Preview.Url, Url = commonProperties.Preview.Url,
Type = (Application.Models.MediaInfoType)commonProperties.Preview.Type, Type = (Application.Models.MediaInfoType)commonProperties.Preview.Type,
//ContentType = commonProperties.Preview.ContentType,
ContentType = "image/png",
}, },
Descriptions = commonProperties.Descriptions.Select(q => new Application.Models.Description Descriptions = commonProperties.Descriptions.Select(q => new Application.Models.Description
{ {
@ -90,6 +92,7 @@ internal class CommonPropertiesConverter(LanguageRepository languageRepository,
Descriptions = commonProperties.Descriptions.Select(q => new DescriptionItem Descriptions = commonProperties.Descriptions.Select(q => new DescriptionItem
{ {
IsOriginal = q.IsOriginal, IsOriginal = q.IsOriginal,
LanguageId = q.LanguageId,
Value = q.Value, Value = q.Value,
}).ToList(), }).ToList(),
Genres = commonProperties.Genres.Select(q => new GenreProportionItem Genres = commonProperties.Genres.Select(q => new GenreProportionItem

View File

@ -62,5 +62,7 @@ public class GenreGateway(GenreRepository repository) : IGenreGateway
public Task<bool> IsGenreExists(Guid id) => repository.AnyWhere(q => q.Id == id && !q.Deleted); public Task<bool> IsGenreExists(Guid id) => repository.AnyWhere(q => q.Id == id && !q.Deleted);
public Task<bool> IsGenreExists(string name, Guid? selfId) => public Task<bool> IsGenreExists(string name, Guid? selfId) =>
repository.AnyWhere(q => q.Id != selfId && q.Name == name.Trim() && !q.Deleted); repository.AnyWhere(q => q.Id != selfId
&& q.Name.ToLower() == name.Trim().ToLower()
&& !q.Deleted);
} }

View File

@ -7,8 +7,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="MongoDB.Driver" Version="2.28.0" /> <PackageReference Include="MongoDB.Driver" Version="2.30.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -100,13 +100,17 @@ public class CommonProperties : ValueObject
_names.Remove(name); _names.Remove(name);
} }
public void SetNameValue(NameItem nameItem, string value) public void SetNameValue(NameItem nameItem, Guid? languageId, string? value)
{ {
var name = GetName(nameItem); var name = GetName(nameItem);
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); var newValue = value?.Trim();
if (nameItem.Type != NameType.Original && _names.Any(q => q.LanguageId == nameItem.LanguageId && q.Value == value)) if (string.IsNullOrWhiteSpace(newValue)) throw new ArgumentNullException(nameof(value));
throw new Exception("Name item with in same language with same value is already exists"); if (nameItem.Type != NameType.Original && _names.Any(q => q.LanguageId == (languageId ?? nameItem.LanguageId) && q.Value == (newValue ?? nameItem.Value)))
nameItem.SetValue(value); throw new Exception("Name item with same language and same value is already exists");
else if (nameItem.Type == NameType.Original && languageId.HasValue && _names.Any(q => q.LanguageId == languageId && q.Type == NameType.OriginalInAnotherLanguage))
throw new Exception("Could not change original name language to one of \"original in another language\" item's one");
if (languageId.HasValue) name.SetLanguage(languageId.Value);
if (!string.IsNullOrWhiteSpace(newValue)) name.SetValue(newValue);
} }
private NameItem GetName(NameItem name) => _names.FirstOrDefault(q => q == name) ?? private NameItem GetName(NameItem name) => _names.FirstOrDefault(q => q == name) ??
@ -144,13 +148,15 @@ public class CommonProperties : ValueObject
//description.SetDeleted(); //description.SetDeleted();
} }
public void SetDescriptionValue(Description descriptionItem, string value) public void SetDescriptionValue(Description descriptionItem, Guid? languageId, string? value)
{ {
var description = GetDescription(descriptionItem); var description = GetDescription(descriptionItem);
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); var newValue = value?.Trim();
if (!descriptionItem.IsOriginal && _descriptions.Any(q => q.LanguageId == descriptionItem.LanguageId && q.Value == value)) if (string.IsNullOrWhiteSpace(newValue)) throw new ArgumentNullException(nameof(value));
if (!descriptionItem.IsOriginal && _descriptions.Any(q => q.LanguageId == (languageId ?? descriptionItem.LanguageId) && q.Value == (newValue ?? descriptionItem.Value)))
throw new Exception("Descriptoin item with with same value is already exists"); throw new Exception("Descriptoin item with with same value is already exists");
descriptionItem.SetValue(value); if (languageId.HasValue) descriptionItem.SetLanguage(languageId.Value);
if (!string.IsNullOrWhiteSpace(newValue)) descriptionItem.SetValue(newValue);
} }
private Description GetDescription(Description description) private Description GetDescription(Description description)
@ -240,14 +246,15 @@ public class CommonProperties : ValueObject
public void SetEstimatedReleaseDate(DateTimeOffset? value) public void SetEstimatedReleaseDate(DateTimeOffset? value)
{ {
if (value == default) throw new ArgumentNullException(nameof(value)); //if (value == default) throw new ArgumentNullException(nameof(value));
if (value.HasValue && value.Value == default) throw new ArgumentOutOfRangeException(nameof(value));
if (AnnouncementDate.HasValue && value <= AnnouncementDate.Value) if (AnnouncementDate.HasValue && value <= AnnouncementDate.Value)
throw new Exception("Estimated release date can not be less or equal to announcement date"); throw new Exception("Estimated release date can not be less or equal to announcement date");
EstimatedReleaseDate = value; EstimatedReleaseDate = value;
} }
public void SetReleaseDate(DateTimeOffset? value) public void SetReleaseDate(DateTimeOffset? value)
{ {
if (value == default) throw new ArgumentNullException(nameof(value)); if (value.HasValue && value.Value == default) throw new ArgumentOutOfRangeException(nameof(value));
if (AnnouncementDate.HasValue && value <= AnnouncementDate.Value) if (AnnouncementDate.HasValue && value <= AnnouncementDate.Value)
throw new Exception("Release date can not be less or equal to announcement date"); throw new Exception("Release date can not be less or equal to announcement date");
ReleaseDate = value; ReleaseDate = value;

View File

@ -5,7 +5,7 @@ public class Description : ValueObject
public string Value { get; private set; } = string.Empty; public string Value { get; private set; } = string.Empty;
public bool IsOriginal { get; init; } public bool IsOriginal { get; init; }
[Required] [Required]
public Guid LanguageId { get; init; } = default!; public Guid LanguageId { get; private set; } = default!;
private Description() { } private Description() { }
@ -28,6 +28,11 @@ public class Description : ValueObject
Value = value; Value = value;
} }
public void SetLanguage(Guid languageId)
{
LanguageId = languageId;
}
protected override IEnumerable<object?> GetEqualityComponents() protected override IEnumerable<object?> GetEqualityComponents()
{ {
yield return Value; yield return Value;

View File

@ -7,7 +7,7 @@ public class NameItem : ValueObject
public string Value { get; private set; } = string.Empty; public string Value { get; private set; } = string.Empty;
public NameType Type { get; private init; } public NameType Type { get; private init; }
[Required] [Required]
public Guid LanguageId { get; private init; } = default!; public Guid LanguageId { get; private set; } = default!;
private NameItem() { } private NameItem() { }
@ -37,6 +37,11 @@ public class NameItem : ValueObject
Value = value; Value = value;
} }
public void SetLanguage(Guid languageId)
{
LanguageId = languageId;
}
protected override IEnumerable<object?> GetEqualityComponents() protected override IEnumerable<object?> GetEqualityComponents()
{ {
yield return Type; yield return Type;

View File

@ -1,6 +1,8 @@
using AutoMapper; using AutoMapper;
using Modules.Library.WebApi.Models; using Modules.Library.WebApi.Models;
using Modules.Library.WebApi.Models.Anime; using Modules.Library.WebApi.Models.Views;
using Modules.Library.WebApi.Models.Views.Anime;
using System.Linq;
namespace Modules.Library.WebApi.Automapper; namespace Modules.Library.WebApi.Automapper;
@ -32,6 +34,8 @@ public class AnimeTitleMapprigProfile : Profile
CreateMap<Application.Models.Anime.Title, Title>() CreateMap<Application.Models.Anime.Title, Title>()
.ForMember(q => q.ExpirationTimeTicks, opt => opt.MapFrom(q => q.ExpirationTime.Ticks)) .ForMember(q => q.ExpirationTimeTicks, opt => opt.MapFrom(q => q.ExpirationTime.Ticks))
.ForMember(q => q.EpisodesInsideSeasonsCount, opt => opt.MapFrom(q => q.Seasons.SelectMany(q => q.Episodes).Count())); .ForMember(q => q.EpisodesInsideSeasonsCount, opt => opt.MapFrom(q => q.Seasons
.SelectMany(q => q.Episodes).Count()));
//.SelectMany(q => q.Episodes.DefaultIfEmpty()).Count()));
} }
} }

View File

@ -1,6 +1,6 @@
using AutoMapper; using AutoMapper;
using Modules.Library.WebApi.Models; using Modules.Library.WebApi.Models;
using Modules.Library.WebApi.Models.Dictionary; using Modules.Library.WebApi.Models.Views.Dictionary;
namespace Modules.Library.WebApi.Automapper; namespace Modules.Library.WebApi.Automapper;

View File

@ -1,9 +1,11 @@
using Amazon.Runtime.Internal.Endpoints.StandardLibrary;
using AutoMapper; using AutoMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Modules.Library.WebApi.Models; using Modules.Library.WebApi.Models;
using Modules.Library.WebApi.Models.Anime; using Modules.Library.WebApi.Models.Anime;
using Modules.Library.WebApi.Models.Anime.CommonProperties;
using Modules.Library.WebApi.Models.Views.Anime;
namespace Modules.Library.WebApi.Controllers; namespace Modules.Library.WebApi.Controllers;
@ -23,20 +25,41 @@ public class TitleController : ControllerBase
_mediator = mediator; _mediator = mediator;
_logger = logger; _logger = logger;
} }
[HttpGet("List")] [HttpGet("List")]
public async Task<List<Title>> List() => public async Task<List<Title>> List() =>
_mapper.Map<List<Title>>(await _mediator.Send(new Application.Queries.Anime.AnimeTitle.AnimeTitleListQuery())); _mapper.Map<List<Title>>(await _mediator.Send(new Application.Queries.Anime.AnimeTitle.AnimeTitleListQuery { UserId = new Guid("8393230f-78e3-473b-a5dc-3221917e0aeb") }));
[HttpGet] [HttpGet]
public async Task<Title> ById(Guid TitleId) => public async Task<Title> ById(Guid TitleId) =>
_mapper.Map<Title>(await _mediator.Send(new Application.Queries.Anime.AnimeTitle.AnimeTitleQuery { Id = TitleId })); _mapper.Map<Title>(await _mediator.Send(new Application.Queries.Anime.AnimeTitle.AnimeTitleQuery { Id = TitleId }));
[HttpPost("Rate")]
public async Task Rate(RateEdit model) =>
await _mediator.Send(new Application.Commands.Anime.Title.RateTitleCommand
{
TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
RatePercentage = model.RatePercentage ?? throw new ArgumentNullException(nameof(model.RatePercentage)),
});
[HttpPost("Unrate")]
public async Task Unrate(RateEdit model) =>
await _mediator.Send(new Application.Commands.Anime.Title.UnrateTitleCommand
{
TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
});
[HttpPost("Create")] [HttpPost("Create")]
public async Task<Guid> CreateTitle([FromQuery] Guid nameOriginalLanguageId, [FromQuery] string nameOriginal) => public async Task<Guid> CreateTitle(TitleCreate model) =>
await _mediator.Send(new Application.Commands.Anime.Title.CreateAnimeTitleCommand await _mediator.Send(new Application.Commands.Anime.Title.CreateAnimeTitleCommand
{ {
NameOriginalLanguageId = nameOriginalLanguageId, NameOriginalLanguageId = model.OriginalName.LanguageId,
NameOriginal = nameOriginal, NameOriginal = model.OriginalName.Value,
Preview = model.Preview == null ? null : new Application.Models.MediaInfo
{
Url = model.Preview.Url,
Type = (Application.Models.MediaInfoType)model.Preview.Type,
}
}); });
[HttpPost("AddSeason")] [HttpPost("AddSeason")]
@ -69,86 +92,85 @@ public class TitleController : ControllerBase
[HttpPost("AddName")] [HttpPost("AddName")]
public async Task AddName([FromQuery] Guid titleId, [FromQuery] Guid languageId, [FromQuery] string name, public async Task AddName(NameEdit model) =>
[FromQuery] NameType type) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.Name.AddNameCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.Name.AddNameCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
LanguageId = languageId, LanguageId = model.LanguageId,
NameType = (Application.Commands.CommonModels.NameType)type, NameType = (Application.Commands.CommonModels.NameType)model.Type,
Value = name Value = model.Value
}); });
[HttpPost("EditName")] [HttpPost("EditName")]
public async Task EditName([FromQuery] Guid titleId, [FromQuery] Guid languageId, [FromQuery] string name, public async Task EditName(NameEdit model) =>
[FromQuery] NameType type) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.Name.EditNameCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.Name.EditNameCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
LanguageId = languageId, LanguageId = model.LanguageId,
NameType = (Application.Commands.CommonModels.NameType)type, NameType = (Application.Commands.CommonModels.NameType)model.Type,
Value = name Value = model.Value,
NewLanguageId = model.NewLanguageId,
NewValue = model.NewValue,
}); });
[HttpPost("DeleteName")] [HttpPost("DeleteName")]
public async Task DeleteName([FromQuery] Guid titleId, [FromQuery] Guid languageId, [FromQuery] string name, public async Task DeleteName(NameEdit model) =>
[FromQuery] NameType type) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.Name.DeleteNameCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.Name.DeleteNameCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
LanguageId = languageId, LanguageId = model.LanguageId,
NameType = (Application.Commands.CommonModels.NameType)type, NameType = (Application.Commands.CommonModels.NameType)model.Type,
Value = name, Value = model.Value,
}); });
[HttpPost("SetPreview")] [HttpPost("SetPreview")]
public async Task SetPreview([FromQuery] Guid titleId, [FromQuery] MediaInfoType type, [FromQuery] string url) => public async Task SetPreview(MediaInfoEdit model) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.Preview.SetPreviewCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.Preview.SetPreviewCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
Type = (Application.Commands.CommonModels.MediaInfoType)type, Type = (Application.Commands.CommonModels.MediaInfoType)model.Type,
Url = url, Url = model.Url,
}); });
[HttpPost("DeletePreview")] [HttpPost("ClearPreview")]
public async Task DeletePreview([FromQuery] Guid titleId) => public async Task ClearPreview(MediaInfoEdit model) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.Preview.DeletePreviewCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.Preview.DeletePreviewCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
}); });
[HttpPost("AddDescription")] [HttpPost("AddDescription")]
public async Task AddDescription([FromQuery] Guid titleId, [FromQuery] bool isOriginal, [FromQuery] Guid languageId, [FromQuery] string value) => public async Task AddDescription(DescriptionEdit model) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.Description.AddDescriptionCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.Description.AddDescriptionCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
IsOriginal = isOriginal, IsOriginal = model.IsOriginal,
LanguageId = languageId, LanguageId = model.LanguageId,
Value = value, Value = model.Value,
}); });
[HttpPost("EditDescription")] [HttpPost("EditDescription")]
public async Task EditDescription([FromQuery] Guid titleId, bool isOriginal, [FromQuery] Guid languageId, [FromQuery] string value, public async Task EditDescription(DescriptionEdit model) =>
[FromQuery] string newValue) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.Description.EditDescriptionCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.Description.EditDescriptionCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
LanguageId = languageId, LanguageId = model.LanguageId,
IsOriginal = isOriginal, IsOriginal = model.IsOriginal,
Value = value, Value = model.Value,
NewValue = newValue, NewLanguageId = model.NewLanguageId,
NewValue = model.NewValue,
}); });
[HttpPost("DeleteDescription")] [HttpPost("DeleteDescription")]
public async Task DeleteDescription([FromQuery] Guid titleId, bool isOriginal, [FromQuery] Guid languageId, [FromQuery] string value) => public async Task DeleteDescription(DescriptionEdit model) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.Description.DeleteDescriptionCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.Description.DeleteDescriptionCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
LanguageId = languageId, LanguageId = model.LanguageId,
IsOriginal = isOriginal, IsOriginal = model.IsOriginal,
Value = value, Value = model.Value,
}); });
@ -229,26 +251,26 @@ public class TitleController : ControllerBase
}); });
[HttpPost("SetAnnouncementDate")] [HttpPost("SetAnnouncementDate")]
public async Task SetAnnouncementDate([FromQuery] Guid titleId, [FromQuery] DateTimeOffset? value) => public async Task SetAnnouncementDate(DateEdit model) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.SetAnnouncementDateCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.SetAnnouncementDateCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
Value = value, Value = model.Value.HasValue ? new DateTimeOffset(model.Value.Value.DateTime, TimeSpan.Zero) : null
}); });
[HttpPost("SetEstimatedReleaseDate")] [HttpPost("SetEstimatedReleaseDate")]
public async Task SetEstimatedReleaseDate([FromQuery] Guid titleId, [FromQuery] DateTimeOffset? value) => public async Task SetEstimatedReleaseDate(DateEdit model) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.SetEstimatedReleaseDateCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.SetEstimatedReleaseDateCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
Value = value, Value = model.Value.HasValue ? new DateTimeOffset(model.Value.Value.DateTime, TimeSpan.Zero) : null
}); });
[HttpPost("SetReleaseDate")] [HttpPost("SetReleaseDate")]
public async Task SetReleaseDate([FromQuery] Guid titleId, [FromQuery] DateTimeOffset? value) => public async Task SetReleaseDate(DateEdit model) =>
await _mediator.Send(new Application.Commands.Anime.Title.Properties.SetReleaseDateCommand await _mediator.Send(new Application.Commands.Anime.Title.Properties.SetReleaseDateCommand
{ {
TitleId = titleId, TitleId = model.AnimeTitleId ?? throw new ArgumentNullException(nameof(model.AnimeTitleId)),
Value = value, Value = model.Value.HasValue ? new DateTimeOffset(model.Value.Value.DateTime, TimeSpan.Zero) : null,
}); });
} }

View File

@ -1,7 +1,9 @@
using AutoMapper; using AutoMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Modules.Library.WebApi.Models.Dictionary; using Modules.Library.WebApi.Models.Views.Dictionary;
using System.Net;
namespace Modules.Library.WebApi.Controllers; namespace Modules.Library.WebApi.Controllers;
@ -9,6 +11,11 @@ namespace Modules.Library.WebApi.Controllers;
[ApiExplorerSettings(GroupName = "GenreV1")] [ApiExplorerSettings(GroupName = "GenreV1")]
[Route("Dictionaries/Genre")] [Route("Dictionaries/Genre")]
//[Route("[controller]")] //[Route("[controller]")]
[Produces("application/json")]
[ProducesResponseType(400, StatusCode = 400, Type = typeof(ProblemDetails))]
[ProducesResponseType(401, StatusCode = 401, Type = typeof(UnauthorizedResult))]
[Consumes("application/json")]
//[Authorize]
public class GenreController : ControllerBase public class GenreController : ControllerBase
{ {
private readonly IMapper _mapper; private readonly IMapper _mapper;
@ -27,14 +34,17 @@ public class GenreController : ControllerBase
_mapper.Map<List<Genre>>(await _mediator.Send(new Application.Queries.Dictionaries.Genre.GenreListQuery())); _mapper.Map<List<Genre>>(await _mediator.Send(new Application.Queries.Dictionaries.Genre.GenreListQuery()));
[HttpPost("CreateGenre")] [HttpPost("CreateGenre")]
public async Task<Guid> CreateGenre([FromQuery]string genreName) => [ProducesResponseType(typeof(Guid), (int)HttpStatusCode.OK)]
await _mediator.Send(new Application.Commands.Dictionaries.Genre.CreateGenreCommand { Name = genreName }); public async Task<IActionResult> CreateGenre([FromQuery] string genreName) =>
Ok(await _mediator.Send(new Application.Commands.Dictionaries.Genre.CreateGenreCommand { Name = genreName }));
[HttpPost("EditGenre")] [HttpPost("EditGenre")]
public async Task EditGenre([FromQuery] Guid id, [FromQuery]string genreName) => [ProducesResponseType((int)HttpStatusCode.OK)]
await _mediator.Send(new Application.Commands.Dictionaries.Genre.SetGenreNameCommand { Id = id, Name = genreName }); public async Task<IActionResult> EditGenre([FromQuery] Guid id, [FromQuery]string genreName) =>
Ok(await _mediator.Send(new Application.Commands.Dictionaries.Genre.SetGenreNameCommand { Id = id, Name = genreName }));
[HttpPost("DeleteGenre")] [HttpPost("DeleteGenre")]
public async Task DeleteGenre([FromQuery] Guid id) => [ProducesResponseType((int)HttpStatusCode.OK)]
await _mediator.Send(new Application.Commands.Dictionaries.Genre.DeleteGenreCommand { Id = id }); public async Task<IActionResult> DeleteGenre([FromQuery] Guid id) =>
Ok(await _mediator.Send(new Application.Commands.Dictionaries.Genre.DeleteGenreCommand { Id = id }));
} }

View File

@ -1,7 +1,7 @@
using AutoMapper; using AutoMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Modules.Library.WebApi.Models.Dictionary; using Modules.Library.WebApi.Models.Views.Dictionary;
namespace Modules.Library.WebApi.Controllers; namespace Modules.Library.WebApi.Controllers;
@ -27,19 +27,19 @@ public class LanguageController : ControllerBase
_mapper.Map<List<Language>>(await _mediator.Send(new Application.Queries.Dictionaries.Language.LanguageListQuery())); _mapper.Map<List<Language>>(await _mediator.Send(new Application.Queries.Dictionaries.Language.LanguageListQuery()));
[HttpPost("Create")] [HttpPost("Create")]
public async Task<Guid> CreateLanguage([FromQuery] string codeIso2, [FromQuery] string name) => public async Task<Guid> CreateLanguage([FromQuery] string codeIso3, [FromQuery] string name) =>
await _mediator.Send(new Application.Commands.Dictionaries.Language.CreateLanguageCommand await _mediator.Send(new Application.Commands.Dictionaries.Language.CreateLanguageCommand
{ {
Code = codeIso2, Code = codeIso3,
Name = name, Name = name,
}); });
[HttpPost("Edit")] [HttpPost("Edit")]
public async Task EditLanguage([FromQuery] Guid id, [FromQuery] string? codeIso2, [FromQuery]string? name) => public async Task EditLanguage([FromQuery] Guid id, [FromQuery] string? codeIso3, [FromQuery]string? name) =>
await _mediator.Send(new Application.Commands.Dictionaries.Language.SetLanguageNameCommand await _mediator.Send(new Application.Commands.Dictionaries.Language.SetLanguageNameCommand
{ {
Id = id, Id = id,
Code = codeIso2, Code = codeIso3,
Name = name, Name = name,
}); });

View File

@ -0,0 +1,9 @@
namespace Modules.Library.WebApi.Models.Anime.CommonProperties;
public class DateEdit
{
public Guid? AnimeTitleId { get; set; }
public Guid? AnimeSeasonId { get; set; }
public Guid? AnimeEpisodeId { get; set; }
public DateTimeOffset? Value { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace Modules.Library.WebApi.Models.Anime.CommonProperties;
public class DescriptionEdit
{
public Guid? AnimeTitleId { get; set; }
public Guid? AnimeSeasonId { get; set; }
public Guid? AnimeEpisodeId { get; set; }
public Guid LanguageId { get; set; } = default!;
public string Value { get; set; } = default!;
public bool IsOriginal { get; set; }
public string? NewValue { get; set; }
public Guid? NewLanguageId { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace Modules.Library.WebApi.Models.Anime.CommonProperties;
public class MediaInfoEdit
{
public Guid? AnimeTitleId { get; set; }
public Guid? AnimeSeasonId { get; set; }
public Guid? AnimeEpisodeId { get; set; }
public MediaInfoType Type { get; set; }
public string Url { get; set; } = default!;
public string ContentType { get; set; } = default!;
public MediaInfoType? NewType { get; set; }
public string? NewUrl { get; set; }
public string? NewContentType { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace Modules.Library.WebApi.Models.Anime.CommonProperties;
public class NameEdit
{
public Guid? AnimeTitleId { get; set; }
public Guid? AnimeSeasonId { get; set; }
public Guid? AnimeEpisodeId { get; set; }
public Guid LanguageId { get; set; } = default!;
public string Value { get; set; } = default!;
public NameType Type { get; set; } = NameType.Original;
public string? NewValue { get; set; }
public Guid? NewLanguageId { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace Modules.Library.WebApi.Models.Anime;
public class Name
{
public Guid LanguageId { get; set; }
public string Value { get; set; } = default!;
public NameType Type { get; set; } = NameType.Original;
}

View File

@ -0,0 +1,9 @@
namespace Modules.Library.WebApi.Models.Anime;
public class RateEdit
{
public Guid? AnimeTitleId { get; set; }
public Guid? AnimeSeasonId { get; set; }
public Guid? AnimeEpisodeId { get; set; }
public ushort? RatePercentage { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace Modules.Library.WebApi.Models.Anime;
public class TitleCreate
{
public Name OriginalName { get; set; } = default!;
public MediaInfo? Preview { get; set; }
}

View File

@ -3,4 +3,5 @@ public class MediaInfo
{ {
public MediaInfoType Type { get; set; } public MediaInfoType Type { get; set; }
public string Url { get; set; } = default!; public string Url { get; set; } = default!;
public string ContentType { get; set; } = default!;
} }

View File

@ -1,4 +1,4 @@
namespace Modules.Library.WebApi.Models.Anime; namespace Modules.Library.WebApi.Models.Views.Anime;
public abstract class AnimeItem public abstract class AnimeItem
{ {

View File

@ -1,4 +1,4 @@
namespace Modules.Library.WebApi.Models.Anime; namespace Modules.Library.WebApi.Models.Views.Anime;
public class Episode : AnimeItem public class Episode : AnimeItem
{ {

View File

@ -1,4 +1,4 @@
namespace Modules.Library.WebApi.Models.Anime; namespace Modules.Library.WebApi.Models.Views.Anime;
public class Season : AnimeItem public class Season : AnimeItem
{ {

View File

@ -1,4 +1,4 @@
namespace Modules.Library.WebApi.Models.Anime; namespace Modules.Library.WebApi.Models.Views.Anime;
public class Title public class Title
{ {

View File

@ -1,4 +1,4 @@
namespace Modules.Library.WebApi.Models; namespace Modules.Library.WebApi.Models.Views;
public class CommonProperties public class CommonProperties
{ {

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Modules.Library.WebApi.Models.Dictionary; using Modules.Library.WebApi.Models.Views.Dictionary;
namespace Modules.Library.WebApi.Models; namespace Modules.Library.WebApi.Models.Views;
public class Description public class Description
{ {
[Required] [Required]

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Modules.Library.WebApi.Models.Dictionary; namespace Modules.Library.WebApi.Models.Views.Dictionary;
public class Genre public class Genre
{ {

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Modules.Library.WebApi.Models.Dictionary; namespace Modules.Library.WebApi.Models.Views.Dictionary;
public class Language public class Language
{ {

View File

@ -1,7 +1,7 @@
using Modules.Library.WebApi.Models.Dictionary; using Modules.Library.WebApi.Models.Views.Dictionary;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Modules.Library.WebApi.Models; namespace Modules.Library.WebApi.Models.Views;
public class GenreProportion public class GenreProportion
{ {

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Modules.Library.WebApi.Models.Dictionary; using Modules.Library.WebApi.Models.Views.Dictionary;
namespace Modules.Library.WebApi.Models; namespace Modules.Library.WebApi.Models.Views;
public class NameItem public class NameItem
{ {

View File

@ -12,6 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Common.WebApi\Common.WebApi.csproj" />
<ProjectReference Include="..\Modules.Library.Application\Modules.Library.Application.csproj" /> <ProjectReference Include="..\Modules.Library.Application\Modules.Library.Application.csproj" />
<ProjectReference Include="..\Modules.Library.Database\Modules.Library.Database.csproj" /> <ProjectReference Include="..\Modules.Library.Database\Modules.Library.Database.csproj" />
<ProjectReference Include="..\MyBookmark.ServiceDefaults\MyBookmark.ServiceDefaults.csproj" /> <ProjectReference Include="..\MyBookmark.ServiceDefaults\MyBookmark.ServiceDefaults.csproj" />

View File

@ -1,7 +1,9 @@
using Common.WebApi.Middlewares;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Modules.Library.Application; using Modules.Library.Application;
using Modules.Library.Database; using Modules.Library.Database;
using Modules.Library.WebApi.Models.Anime; using Modules.Library.WebApi.Models.Views.Anime;
using Modules.Rating.Api;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI; using Swashbuckle.AspNetCore.SwaggerUI;
@ -11,6 +13,7 @@ builder.AddServiceDefaults();
// Add services to the container. // Add services to the container.
builder.Services.AddDatabase("mongodb://localhost:27017", "MyBookmarkDb"); builder.Services.AddDatabase("mongodb://localhost:27017", "MyBookmarkDb");
builder.Services.AddRates("mongodb://localhost:27017", "MyBookmarkDb");
builder.Services.AddApplicationServices(); builder.Services.AddApplicationServices();
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
@ -134,8 +137,12 @@ builder.Services.Configure<SwaggerGenOptions>(options =>
var app = builder.Build(); var app = builder.Build();
app.UseMiddleware<ExceptionMiddleware>();
app.MapDefaultEndpoints(); app.MapDefaultEndpoints();
app.UseCors(q => q.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod()); //??
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
{ {

View File

@ -0,0 +1,35 @@
using MediatR;
using Modules.Rating.Api.Database.Entities;
using Modules.Rating.Api.Repositories;
namespace Modules.Rating.Api.Commands;
public class RateObjectCommand : IRequest<Unit>
{
public Guid ObjectId { get; set; }
public Guid SubjectId { get; set; }
public ushort RatePercentage { get; set; }
}
public class RateObjectCommandHandler(RateRepository repository) : IRequestHandler<RateObjectCommand, Unit>
{
public async Task<Unit> Handle(RateObjectCommand request, CancellationToken cancellationToken)
{
var key = new RateKey
{
ObjectId = request.ObjectId,
SubjectId = request.SubjectId,
};
if (!await repository.IsRateExists(key))
{
await repository.AddAsync(new Rate { Key = key, RatePercentage = request.RatePercentage, });
}
else
{
await repository.UpdateAsync(new Rate { Key = key, RatePercentage = request.RatePercentage, });
}
return Unit.Value;
}
}

View File

@ -0,0 +1,31 @@
using MediatR;
using Modules.Rating.Api.Database.Entities;
using Modules.Rating.Api.Repositories;
namespace Modules.Rating.Api.Commands;
public class UnrateObjectCommand : IRequest<Unit>
{
public Guid ObjectId { get; set; }
public Guid SubjectId { get; set; }
}
public class UnrateObjectCommandHandler(RateRepository repository) : IRequestHandler<UnrateObjectCommand, Unit>
{
public async Task<Unit> Handle(UnrateObjectCommand request, CancellationToken cancellationToken)
{
//var key = new RateKey
//{
// ObjectId = request.ObjectId,
// SubjectId = request.SubjectId,
//};
//if (await repository.IsRateExists(key)) await repository.DeleteAsync(key);
await repository.DeleteAsync(new RateKey
{
ObjectId = request.ObjectId,
SubjectId = request.SubjectId,
});
return Unit.Value;
}
}

View File

@ -0,0 +1,10 @@
using MongoDB.Bson.Serialization.Attributes;
namespace Modules.Rating.Api.Database.Entities;
[BsonIgnoreExtraElements]
public class Rate
{
public RateKey Key { get; set; } = default!;
public ushort RatePercentage { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace Modules.Rating.Api.Database.Entities;
public class RateKey
{
public Guid ObjectId { get; set; } = default!;
public Guid SubjectId { get; set; } = default!;
}

View File

@ -0,0 +1,35 @@
using Modules.Rating.Api.Database.Entities;
using MongoDB.Driver;
namespace Modules.Rating.Api.Database;
public class MongoDbContext(IMongoDatabase database)
{
private bool _initialized;
public IMongoCollection<TDocument> GetCollection<TDocument>(string collectionName)
{
if (!_initialized) throw new Exception(string.Concat(nameof(MongoDbContext), " has not initialized yet"));
return database.GetCollection<TDocument>(collectionName);
}
public IMongoCollection<Rate> Rates => GetCollection<Rate>(nameof(Rate));
public void Initialize()
{
/*
BsonClassMap.RegisterClassMap<AnimeTitle>(q =>
{
q.AutoMap();
q.MapIdMember(c => c.Id).SetIdGenerator(CombGuidGenerator.Instance);
//q.MapCreator(q => AnimeTitleBuilder.FromAnimeTitle(q).Build());
//q.MapIdMember(c => c.Id).SetIdGenerator(StringObjectIdGenerator.Instance);
});
*/
_initialized = true;
}
//private IntSequence _paymentProviderIdSequence = new("PaymentProviderId");
//public Task<int> GetNextPaymentProviderId() => _paymentProviderIdSequence.GetNextSequenceValue(context);
}

View File

@ -0,0 +1,9 @@
namespace Modules.Rating.Api.Models;
public class Rate
{
public Guid ObjectId { get; set; }
//public Guid SubjectId { get; set; }
public ushort? ObjectRatePercentage { get; set; }
public ushort? SubjectRatePercentage { get; set; }
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="MongoDB.Driver" Version="2.30.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,27 @@
using MediatR;
using Modules.Rating.Api.Database.Entities;
using Modules.Rating.Api.Models;
using Modules.Rating.Api.Repositories;
namespace Modules.Rating.Api.Querirs;
public class ObjectRatingListQuery : IRequest<List<Models.Rate>>
{
public Guid? SubjectId { get; set; }
public IEnumerable<Guid> ObjectIds { get; set; } = [];
}
public class ObjectRatingListQueryHandler(RateRepository repository) : IRequestHandler<ObjectRatingListQuery, List<Models.Rate>>
{
public async Task<List<Models.Rate>> Handle(ObjectRatingListQuery request, CancellationToken cancellationToken)
{
if (request.ObjectIds.Count() == 0) return [];
var rates = await repository.GetRates(request.ObjectIds, request.SubjectId);
return rates.Select(q => new Models.Rate
{
ObjectId = q.ObjectId,
ObjectRatePercentage = (ushort)Math.Round((decimal)q.Rate, 0),
SubjectRatePercentage = q.SubjectRate.HasValue ? Convert.ToUInt16(q.SubjectRate) : null,
}).ToList();
}
}

View File

@ -0,0 +1,37 @@
using MediatR;
using Modules.Rating.Api.Database.Entities;
using Modules.Rating.Api.Repositories;
namespace Modules.Rating.Api.Querirs;
public class ObjectRatingQuery : IRequest<Models.Rate?>
{
public Guid ObjectId { get; set; } = default!;
public Guid? SubjectId { get; set; }
}
public class ObjectRatingQueryHandler(RateRepository repository) : IRequestHandler<ObjectRatingQuery, Models.Rate?>
{
public async Task<Models.Rate?> Handle(ObjectRatingQuery request, CancellationToken cancellationToken)
{
var rate = await repository.GetAverageObjectRate(request.ObjectId);
var subjectRate = request.SubjectId.HasValue
? await repository.GetFirstOrDefaultWhere(q => q.Key == new RateKey
{
ObjectId = request.ObjectId,
SubjectId = request.SubjectId.HasValue ? request.SubjectId.Value : q.Key.SubjectId,
})
: null;
return rate.HasValue
? new Models.Rate
{
ObjectId = request.ObjectId,
//SubjectId = request.SubjectId,
ObjectRatePercentage = (ushort)Math.Round((decimal)rate, 0),
SubjectRatePercentage = subjectRate?.RatePercentage,
}
: null;
}
}

View File

@ -0,0 +1,145 @@
using Modules.Rating.Api.Database;
using Modules.Rating.Api.Database.Entities;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using System.Linq;
using System.Linq.Expressions;
namespace Modules.Rating.Api.Repositories;
public class RateRepository(MongoDbContext context)
{
private readonly IMongoCollection<Rate> _collection = context.GetCollection<Rate>(nameof(Rate));
public async Task AddAsync(Rate entity)
{
if (await IsRateExists(entity.Key)) throw new Exception("Object is already rated by subject");
await _collection.InsertOneAsync(entity);
}
public async Task<bool> UpdateAsync(Rate entity)
{
if (!await IsRateExists(entity.Key)) throw new Exception("Rate not found");
var document = await _collection.FindOneAndReplaceAsync(q => q.Key == entity.Key, entity);
return document != null;
}
public async Task<bool> DeleteAsync(RateKey key)
{
if (!await IsRateExists(key)) throw new Exception("Rate not found");
var document = await _collection.FindOneAndDeleteAsync(q => q.Key == key);
return document != null;
}
public async Task<Rate> GetFirstOrDefaultWhere(Expression<Func<Rate, bool>> predicate) =>
await _collection.Find(predicate).SingleOrDefaultAsync();
public async Task<List<Rate>> GetWhere(Expression<Func<Rate, bool>> predicate) =>
await _collection.Find(predicate).ToListAsync();
//internal async Task<List<RateItem>> GetRates(IEnumerable<Guid> objectIds, Guid? subjectId)
//{
// //var query = _collection.AsQueryable();
// //return await query
// // .Where(q => objectIds.Contains(q.Key.ObjectId))
// // .GroupBy(q => q.Key.ObjectId, (o, r) => new { ObjectId = o, Rate = r.Average(q => q.RatePercentage) })
// // .GroupJoin(query.Where(q => q.Key.SubjectId == subjectId),
// // q => q.ObjectId,
// // q => q.Key.ObjectId,
// // (g, r) => new { g.ObjectId, g.Rate, SubjecrRates = r })
// // .SelectMany(q => q.SubjecrRates.DefaultIfEmpty(), (r, s) =>
// // new RateItem { ObjectId = r.ObjectId, Rate = r.Rate, SubjectRate = s != null ? s.RatePercentage : null })
// // .ToListAsync().ConfigureAwait(false);
// var builder = Builders<BsonDocument>.SetFields
// await _collection
// .Aggregate()
// .Match(q => objectIds.Contains(q.Key.ObjectId))
// .Group(q => q.Key.ObjectId, q => new { ObjectId = q.Key, Rate = q.Average(q => q.RatePercentage) })
// .Lookup<Rate, RateItem>(nameof(Rate), q => q.ObjectId, q)
// .FirstOrDefaultAsync().ConfigureAwait(false);
//}
internal async Task<List<RateItem>> GetRates(IEnumerable<Guid> objectIds, Guid? subjectId)
{
var matchStage = new BsonDocument("$match", new BsonDocument("Key.ObjectId", new BsonDocument("$in", new BsonArray(objectIds))));
var groupStage = new BsonDocument("$group", new BsonDocument
{
{ "_id", "$Key.ObjectId" },
{ "AverageRate", new BsonDocument("$avg", "$RatePercentage") }
});
var lookupStage = new BsonDocument("$lookup", new BsonDocument
{
{ "from", _collection.CollectionNamespace.CollectionName },
{ "let", new BsonDocument("objectId", "$_id") },
{ "pipeline", new BsonArray
{
new BsonDocument("$match", new BsonDocument("$expr", new BsonDocument("$and", new BsonArray
{
new BsonDocument("$eq", new BsonArray { "$Key.ObjectId", "$$objectId" }),
//new BsonDocument("$eq", new BsonArray { "$Key.SubjectId", subjectId.ToString() ?? "" })
new BsonDocument("$eq", new BsonArray { "$Key.SubjectId", subjectId.ToString() ?? "" })
})))
}
},
{ "as", "SubjectRates" }
});
var projectStage = new BsonDocument("$project", new BsonDocument
{
{ "ObjectId", "$_id" },
{ "Rate", "$AverageRate" },
{ "SubjectRate", new BsonDocument("$arrayElemAt", new BsonArray { "$SubjectRates.RatePercentage", 0 }) }
});
var pipeline = new[] { matchStage, groupStage, lookupStage, projectStage };
var result = await _collection.AggregateAsync<BsonDocument>(pipeline).ConfigureAwait(false);
var rateItems = new List<RateItem>();
//await result.ForEachAsync(doc =>
//{
// rateItems.Add(new RateItem
// {
// ObjectId = doc["ObjectId"].AsGuid,
// Rate = doc["Rate"].AsDouble,
// SubjectRate = doc["SubjectRate"].IsBsonNull ? (ushort?)null : doc["SubjectRate"].AsNullableInt32
// });
//}).ConfigureAwait(false);
await result.ForEachAsync(doc =>
{
var subjectRate = doc.GetValue("SubjectRate", BsonNull.Value);
rateItems.Add(new RateItem
{
ObjectId = doc.GetValue("ObjectId").AsGuid,
Rate = doc.GetValue("Rate").AsDouble,
SubjectRate = subjectRate.IsBsonNull ? null : subjectRate.AsNullableInt32
});
}).ConfigureAwait(false);
return rateItems;
}
internal class RateItem
{
internal Guid ObjectId { get; set; } = default!;
internal double Rate { get; set; }
internal int? SubjectRate { get; set; }
}
public async Task<double?> GetAverageObjectRate(Guid objectId) =>
await _collection
.Aggregate()
.Match(q => q.Key.ObjectId == objectId)
.Group(q => q.Key.ObjectId, q => q.Average(q => q.RatePercentage))
.FirstOrDefaultAsync().ConfigureAwait(false);
public async Task<bool> IsRateExists(RateKey key) => await _collection.Find(q => q.Key == key).AnyAsync();
}

View File

@ -0,0 +1,59 @@
using Microsoft.Extensions.DependencyInjection;
using Modules.Rating.Api.Database;
using Modules.Rating.Api.Repositories;
using MongoDB.Driver;
namespace Modules.Rating.Api
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRates(this IServiceCollection services, string connectionString)
{
AddMongoDb(services, connectionString);
services.AddScoped(q =>
{
var context = new MongoDbContext(q.GetRequiredService<IMongoDatabase>());
context.Initialize();
return context;
});
AddRepositories(services);
return services;
}
public static IServiceCollection AddRates(this IServiceCollection services, string connectionString, string? databaseName)
{
AddMongoDb(services, connectionString, databaseName);
//services.AddScoped<MongoDbContext>();
services.AddScoped(q =>
{
var context = new MongoDbContext(q.GetRequiredService<IMongoDatabase>());
context.Initialize();
return context;
});
AddRepositories(services);
//AddGateways(services);
return services;
}
private static void AddRepositories(IServiceCollection services)
{
services.AddScoped<RateRepository>();
}
private static void AddMongoDb(this IServiceCollection services, string? connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentNullException(nameof(connectionString));
services.AddSingleton(new MongoClient(connectionString));
}
private static void AddMongoDb(IServiceCollection services, string? connectionString, string? databaseName)
{
if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentNullException(nameof(connectionString));
if (string.IsNullOrWhiteSpace(databaseName)) throw new ArgumentNullException(nameof(databaseName));
services.AddSingleton(new MongoClient(connectionString).GetDatabase(databaseName));
}
}
}

View File

@ -0,0 +1,6 @@
namespace Modules.Rating.Application.Gateways;
public interface IRatingGateway
{
public
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Commands\" />
<Folder Include="Querirs\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Modules.Rating.Application.Querirs
{
internal class ObjectRatingQuery
{
}
}

View File

@ -0,0 +1,6 @@
namespace Modules.Rating.Application;
public static class ServiceCollectionExtensions
{
}

View File

@ -0,0 +1,5 @@
namespace Modules.Rating.Application.Services;
public class RatingService
{
}

View File

@ -0,0 +1,7 @@
namespace Modules.Rating.Database
{
public class Class1
{
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,8 @@
namespace Modules.Rating.Domain;
public class Rate
{
public Guid ObjectId { get; set; }
public Guid SubjectId { get; set; }
public ushort RatePercentage { get; set; }
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,6 @@
namespace Modules.User.Api;
public class UserMiddleware
{
}

View File

@ -0,0 +1,15 @@
namespace Modules.User.Api;
public class UserService
{
private string? _user;
public async Task<string?> GetUser()
{
if (_user == null)
{
await Task.Delay(5000);
}
return _user;
}
}

View File

@ -21,7 +21,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Views", "Views", "{A1136847
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Search", "Search", "{5BC05B7D-F17B-4776-91CA-A7552FD294EA}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Search", "Search", "{5BC05B7D-F17B-4776-91CA-A7552FD294EA}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rates", "Rates", "{C3D36A31-0F02-4E15-A57D-895E714FE8A7}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rating", "Rating", "{C3D36A31-0F02-4E15-A57D-895E714FE8A7}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UserNotes", "UserNotes", "{1A6524E9-DA15-4044-B29F-BFB337F490EE}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UserNotes", "UserNotes", "{1A6524E9-DA15-4044-B29F-BFB337F490EE}"
EndProject EndProject
@ -55,6 +55,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UI", "UI", "{30DDC74A-7529-
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyBookmark.UI.RazorPages", "MyBookmark.UI.RazorPages\MyBookmark.UI.RazorPages.csproj", "{37A4BCD2-73B9-4CB3-B12A-58B1298FE02C}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyBookmark.UI.RazorPages", "MyBookmark.UI.RazorPages\MyBookmark.UI.RazorPages.csproj", "{37A4BCD2-73B9-4CB3-B12A-58B1298FE02C}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.WebApi", "Common.WebApi\Common.WebApi.csproj", "{332B1C89-06C2-4ED9-845C-DE41F53D15E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Rating.Api", "Modules.Rating.Api\Modules.Rating.Api.csproj", "{16F6D8C7-78EE-4800-82E2-951737221958}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{86794C28-E42B-4899-B6FD-0AFA51E204B8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{C72E692E-B96D-4512-BB2E-CA0C9C9E2B28}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Account.Api", "Modules.Account.Api\Modules.Account.Api.csproj", "{1EFD5236-7F81-4AB8-8838-08C3DA7A5306}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Account.Domain", "Modules.Account.Domain\Modules.Account.Domain.csproj", "{5EFDE6FF-BE77-458C-ACD1-0D33346F2245}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Account.Database", "Modules.Account.Database\Modules.Account.Database.csproj", "{BFEBE295-1923-48C5-A922-D9FD997E150C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.Account.Application", "Modules.Account.Application\Modules.Account.Application.csproj", "{A18E14C1-422E-44F2-A140-397B1F918F45}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules.User.Api", "Modules.User.Api\Modules.User.Api.csproj", "{4EE10B8F-EF6C-40A2-A9EB-4FC2D4CF8107}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -107,6 +125,34 @@ Global
{37A4BCD2-73B9-4CB3-B12A-58B1298FE02C}.Debug|Any CPU.Build.0 = Debug|Any CPU {37A4BCD2-73B9-4CB3-B12A-58B1298FE02C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37A4BCD2-73B9-4CB3-B12A-58B1298FE02C}.Release|Any CPU.ActiveCfg = Release|Any CPU {37A4BCD2-73B9-4CB3-B12A-58B1298FE02C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37A4BCD2-73B9-4CB3-B12A-58B1298FE02C}.Release|Any CPU.Build.0 = Release|Any CPU {37A4BCD2-73B9-4CB3-B12A-58B1298FE02C}.Release|Any CPU.Build.0 = Release|Any CPU
{332B1C89-06C2-4ED9-845C-DE41F53D15E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{332B1C89-06C2-4ED9-845C-DE41F53D15E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{332B1C89-06C2-4ED9-845C-DE41F53D15E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{332B1C89-06C2-4ED9-845C-DE41F53D15E8}.Release|Any CPU.Build.0 = Release|Any CPU
{16F6D8C7-78EE-4800-82E2-951737221958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16F6D8C7-78EE-4800-82E2-951737221958}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16F6D8C7-78EE-4800-82E2-951737221958}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16F6D8C7-78EE-4800-82E2-951737221958}.Release|Any CPU.Build.0 = Release|Any CPU
{1EFD5236-7F81-4AB8-8838-08C3DA7A5306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1EFD5236-7F81-4AB8-8838-08C3DA7A5306}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1EFD5236-7F81-4AB8-8838-08C3DA7A5306}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1EFD5236-7F81-4AB8-8838-08C3DA7A5306}.Release|Any CPU.Build.0 = Release|Any CPU
{5EFDE6FF-BE77-458C-ACD1-0D33346F2245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5EFDE6FF-BE77-458C-ACD1-0D33346F2245}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5EFDE6FF-BE77-458C-ACD1-0D33346F2245}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5EFDE6FF-BE77-458C-ACD1-0D33346F2245}.Release|Any CPU.Build.0 = Release|Any CPU
{BFEBE295-1923-48C5-A922-D9FD997E150C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFEBE295-1923-48C5-A922-D9FD997E150C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFEBE295-1923-48C5-A922-D9FD997E150C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFEBE295-1923-48C5-A922-D9FD997E150C}.Release|Any CPU.Build.0 = Release|Any CPU
{A18E14C1-422E-44F2-A140-397B1F918F45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A18E14C1-422E-44F2-A140-397B1F918F45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A18E14C1-422E-44F2-A140-397B1F918F45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A18E14C1-422E-44F2-A140-397B1F918F45}.Release|Any CPU.Build.0 = Release|Any CPU
{4EE10B8F-EF6C-40A2-A9EB-4FC2D4CF8107}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4EE10B8F-EF6C-40A2-A9EB-4FC2D4CF8107}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4EE10B8F-EF6C-40A2-A9EB-4FC2D4CF8107}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4EE10B8F-EF6C-40A2-A9EB-4FC2D4CF8107}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -131,6 +177,15 @@ Global
{43731C43-2BA9-4F2F-8A0F-2F157F05D5A7} = {D034BC31-002F-4F3D-B2C1-9E81B990C51A} {43731C43-2BA9-4F2F-8A0F-2F157F05D5A7} = {D034BC31-002F-4F3D-B2C1-9E81B990C51A}
{6DC1B759-12F6-415D-BCAC-60E9E15B1074} = {036DC6C2-18A3-49DB-92D3-6DFCDCE3D5F8} {6DC1B759-12F6-415D-BCAC-60E9E15B1074} = {036DC6C2-18A3-49DB-92D3-6DFCDCE3D5F8}
{37A4BCD2-73B9-4CB3-B12A-58B1298FE02C} = {30DDC74A-7529-4462-94CB-4AB77792BCCD} {37A4BCD2-73B9-4CB3-B12A-58B1298FE02C} = {30DDC74A-7529-4462-94CB-4AB77792BCCD}
{332B1C89-06C2-4ED9-845C-DE41F53D15E8} = {BACBF195-FD74-4F57-AF8B-D8752C1EBBF0}
{16F6D8C7-78EE-4800-82E2-951737221958} = {C3D36A31-0F02-4E15-A57D-895E714FE8A7}
{86794C28-E42B-4899-B6FD-0AFA51E204B8} = {9B8DB13A-9595-419D-83F3-7C537B903D9F}
{C72E692E-B96D-4512-BB2E-CA0C9C9E2B28} = {9B8DB13A-9595-419D-83F3-7C537B903D9F}
{1EFD5236-7F81-4AB8-8838-08C3DA7A5306} = {9B8DB13A-9595-419D-83F3-7C537B903D9F}
{5EFDE6FF-BE77-458C-ACD1-0D33346F2245} = {86794C28-E42B-4899-B6FD-0AFA51E204B8}
{BFEBE295-1923-48C5-A922-D9FD997E150C} = {C72E692E-B96D-4512-BB2E-CA0C9C9E2B28}
{A18E14C1-422E-44F2-A140-397B1F918F45} = {86794C28-E42B-4899-B6FD-0AFA51E204B8}
{4EE10B8F-EF6C-40A2-A9EB-4FC2D4CF8107} = {A006C232-0F12-4B56-9EA0-D8771ABE49AA}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4D16F124-695A-4782-BEFB-AAAFA0953732} SolutionGuid = {4D16F124-695A-4782-BEFB-AAAFA0953732}

View File

@ -5,8 +5,21 @@
<!--<link rel="icon" type="image/svg+xml" href="/vite.svg" />--> <!--<link rel="icon" type="image/svg+xml" href="/vite.svg" />-->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My bookmark</title> <title>My bookmark</title>
<link rel="stylesheet" href="plugins/themify-icons/themify-icons.css">
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> -->
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> -->
<link href="./node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet">
<link href="./node_modules/bootstrap/dist/css/bootstrap.js" rel="stylesheet">
<!-- <link href="./src/assets/css/custom.css" rel="stylesheet"> -->
</head> </head>
<body> <!-- <body style="height: 100%; overflow-x: hidden; overflow-y: hidden;"> -->
<!-- <body style="height: 100vh; overflow: hidden;"> -->
<!-- <body class="container-xxl" style="height: 100vh;"> -->
<!-- <body class="overflow-hidden"> -->
<!-- <body class="overflow-hidden" data-bs-theme="dark"> -->
<body class="overflow-hidden">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>

File diff suppressed because it is too large Load Diff

View File

@ -10,8 +10,15 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.7",
"bootstrap": "^5.3.3",
"jotai": "^2.10.0",
"npm": "^10.8.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-query": "^3.39.3",
"react-router-dom": "^6.26.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.9.0", "@eslint/js": "^9.9.0",

View File

@ -0,0 +1,32 @@
// @import "../node_modules/bootstrap/scss/functions";
// // // 2. Include any default variable overrides here
@import "./themes/Cosmo/variables";
@import "../node_modules/bootstrap/scss/bootstrap";
// @import "../node_modules/bootstrap/scss/variables";
// @import "../node_modules/bootstrap/scss/variables-dark";
// @import "../node_modules/bootstrap/scss/mixins";
@import "./themes/Cosmo/bootswatch";
// @import "../node_modules/bootstrap/scss/maps";
// @import "../node_modules/bootstrap/scss/root";
// @import "variables";
// Then have Bootstrap do it's magic with these new values
// $tertiary-bg-rgb: rgba(102, 102, 153, 1);
// $tertiary-bg-rgb: rgb(102 102 153 / 1);
// $body-tertiary-color: rgba($body-color, .5) !default;
// $body-tertiary-bg: $gray-100 !default;
// $body-tertiary-color: rgba(102, 102, 0, 1) !default;
// $body-tertiary-bg: rgba(0, 102, 153, 1) !default;
// $white: #0000 !default;

View File

@ -0,0 +1,35 @@
// Cosmo 5.3.3
// Bootswatch
// Variables
$web-font-path: "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;700&display=swap" !default;
@if $web-font-path {
@import url("#{$web-font-path}");
}
// Typography
body {
-webkit-font-smoothing: antialiased;
}
// Indicators
.badge {
&.bg-light {
color: $dark;
}
}
// Progress bars
.progress {
@include box-shadow(none);
.progress-bar {
font-size: 8px;
line-height: 8px;
}
}

View File

@ -0,0 +1,69 @@
// Cosmo 5.3.3
// Bootswatch
$theme: "cosmo" !default;
//
// Color system
//
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #868e96 !default;
$gray-700: #495057 !default;
$gray-800: #373a3c !default;
$gray-900: #212529 !default;
$black: #000 !default;
$blue: #2780e3 !default;
$indigo: #6610f2 !default;
$purple: #613d7c !default;
$pink: #e83e8c !default;
$red: #ff0039 !default;
$orange: #f0ad4e !default;
$yellow: #ff7518 !default;
$green: #3fb618 !default;
$teal: #20c997 !default;
$cyan: #9954bb !default;
$primary: $blue !default;
$secondary: $gray-800 !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-100 !default;
$dark: $gray-800 !default;
$min-contrast-ratio: 2.6 !default;
// Options
$enable-rounded: false !default;
// Body
$body-color: $gray-800 !default;
// Fonts
// stylelint-disable-next-line value-keyword-case
$font-family-sans-serif: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
$headings-font-weight: 400 !default;
// Navbar
$navbar-dark-hover-color: rgba($white, 1) !default;
$navbar-light-hover-color: rgba($black, .9) !default;
// Alerts
$alert-border-width: 0 !default;
// Progress bars
$progress-height: .5rem !default;

View File

@ -1,8 +1,12 @@
/*var*/
#root { #root {
max-width: 1280px; /* max-width: 1280px; */
margin: 0 auto; /* margin: 0 auto; */
padding: 2rem; /* padding: 2rem; */
text-align: center; /* text-align: center; */
--navbar-height: 3.5rem;
} }
.logo { .logo {
@ -40,3 +44,64 @@
.read-the-docs { .read-the-docs {
color: #888; color: #888;
} }
/* .menu{
position: sticky;
display: inline-block;
vertical-align: top;
max-height: 100vh;
overflow-y: auto;
border-right: thick double red;
box-shadow: .5rem, 0, 0, DarkSlateGray;
height: 100%;
position: sticky important!;
} */
.fixed-navbar{
position: sticky;
height: var(--navbar-height);
z-index: 1200;
}
.fixed-sidebar{
position: sticky;
top: var(--navbar-height);
/* left: 0; */
/* width: 300px; */
height: 100%;
/* background: #042331; */
/* overflow-x: hidden; */
/* overflow-y: scroll; */
/* border-right: thick double red; */
/* box-shadow: .5rem, 0, 0, DarkSlateGray; */
}
.content {
/* position: sticky; */
/* padding-top: var(--navbar-height); */
/* height: calc(100% - var(--navbar-height)); */
height: calc(100vh - var(--navbar-height));
/* min-height: calc(100% - var(--navbar-height)); */
min-height: calc(100vh - var(--navbar-height));
display:inline-block;
}
.progress-container:before {
content: attr(data-text);
position: absolute;
left: 0;
right: 0;
top: 0;
line-height:1rem;
}
.progress-container {
text-align: center;
position: relative;
width: 100%;
}
.rateStar {
/* background-image: url(./assets/thumbs_up_0i8ids3f6gah.svg); */
}

View File

@ -1,34 +1,61 @@
import { useState } from 'react' // import { useState } from 'react'
import reactLogo from './assets/react.svg' // import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg' // import viteLogo from '/vite.svg'
import './App.css' import './App.css'
function App() { // import 'bootstrap/dist/css/bootstrap.min.css'
const [count, setCount] = useState(0) // import './assets/css/custom.css'
// import './assets/scss/custom.scss'
// import 'bootstrap/dist/css/bootstrap.css';
// import 'bootstrap/dist/js/bootstrap.js';
// import './assets/css/custom.css'
import HomePage from './pages/HomePage';
import { QueryClient, QueryClientProvider } from 'react-query';
import Layout from './common/Layout';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Dictionaries from './pages/library/edit/Dictionaries';
import AdminLayout from './common/AdminLayout';
import CreateAnimeTitle from './pages/library/edit/CreateAnimeTitle';
import AnimeTitleDetail from './pages/library/anime/AnimeTitleDetail';
import AnimeTitleEdit from './pages/library/edit/AnimeTitleEdit';
function App() {
//const [count, setCount] = useState(0)
return ( return (
<BrowserRouter>
<Routes>
<Route path="/" element={
<> <>
<div> <QueryClientProvider client={new QueryClient()}>
<a href="https://vitejs.dev" target="_blank"> <Layout />
<img src={viteLogo} className="logo" alt="Vite logo" /> </QueryClientProvider>
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</> </>
}>
<Route index element={<HomePage />} />
<Route path="library">
<Route path="anime/:id" element={<AnimeTitleDetail />} />
</Route>
{/* <Route path="about" element={<About />} /> */}
{/* <Route path="contact" element={<Contact />} /> */}
</Route>
<Route path="/admin" element={
<>
<QueryClientProvider client={new QueryClient()}>
<AdminLayout />
</QueryClientProvider>
</>
}>
<Route path='library/dictionaries' element={<Dictionaries />} />
<Route path='library/anime/title' element={<CreateAnimeTitle />} />
<Route path='library/anime/anime/title/:id/edit' element={<AnimeTitleEdit />} />
{/* <Route path="about" element={<About />} /> */}
{/* <Route path="contact" element={<Contact />} /> */}
</Route>
{/* <Route path="/connect" element={<Connect />} /> */}
{/* <Route path="*" element={<Error />} /> */}
</Routes>
</BrowserRouter>
) )
} }

View File

@ -0,0 +1,392 @@
import axios from "axios";
import { AnimeTitle, CreateDescriptionModel, CreateMediaInfoModel, CreateNameModel, DeleteDescriptionModel, DeleteMediaInfoModel, DeleteNameModel, EditDateModel, EditDescriptionModel, EditNameModel, RateDeleteModel, RateEditModel, TitleCreateModel } from "./models/Types";
import { CatchError } from "./common";
const controllerUrl = 'https://localhost:7057/MediaContent/Anime/Title/';
export async function GetAnimeTitleList() {
try {
// const { data, status } = await axios.get<Array<AnimeTitle>>(`${controllerUrl}List`,
const { data } = await axios.get<Array<AnimeTitle>>(`${controllerUrl}List`,
{
headers: {
Accept: 'text/plain',
},
},
);
//console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function GetAnimeTitleDetail(id: string) {
try {
// const { data, status } = await axios.get<Array<AnimeTitle>>(`${controllerUrl}List`,
const { data } = await axios.get<AnimeTitle>(`${controllerUrl}?TitleId=${id}`,
{
headers: {
Accept: 'text/plain',
},
},
);
//console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function RateAnimeTitle(model: RateEditModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}Rate`, model, axiosConfig);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function UnrateAnimeTitle(model: RateDeleteModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}Unrate`, model, axiosConfig);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function CreateAnimeTitle(model: TitleCreateModel) {
try {
// 👇️ const data: GetUsersResponse
// const { data, status } = await axios.post<string>(`${controllerUrl}Create`,
// {
// // headers: {
// // // Accept: 'text/plain',
// // },
// // body: model
// },
// );
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}Create`, model, axiosConfig);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
//- - - - - - - - - - COMMON PROPERTIES - - - - - - - - - -
export async function CreateAnimeTitleName(model: CreateNameModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}AddName`, model, axiosConfig);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function EditAnimeTitleName(model: EditNameModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}EditName`, model, axiosConfig);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function DeleteAnimeTitleName(model: DeleteNameModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}DeleteName`, model, axiosConfig);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function SetAnimeTitlePreview(model: CreateMediaInfoModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}SetPreview`, model, axiosConfig);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function ClearAnimeTitlePreview(model: DeleteMediaInfoModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}ClearPreview`, model, axiosConfig);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function CreateAnimeTitleDescription(model: CreateDescriptionModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}AddDescription`, model, axiosConfig);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function EditAnimeTitleDescription(model: EditDescriptionModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}EditDescription`, model, axiosConfig);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function DeleteAnimeTitleDescription(model: DeleteDescriptionModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}DeleteDescription`, model, axiosConfig);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function SetAnimeTitleAnnouncementDate(model: EditDateModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}SetAnnouncementDate`, model, axiosConfig);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function SetAnimeTitleEstimatedReleaseDate(model: EditDateModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}SetEstimatedReleaseDate`, model, axiosConfig);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function SetAnimeTitleReleaseDate(model: EditDateModel) {
try {
const axiosConfig = {
headers:{
Accept: 'text/plain',
ContentType: 'application/json'
}
}
const { data, status } = await axios.post<string>(`${controllerUrl}SetReleaseDate`, model, axiosConfig);
return data;
}
catch (error)
{
CatchError(error);
}
}
// export async function EditGenre(id : string, value: string) {
// try {
// // 👇️ const data: GetUsersResponse
// const { data, status } = await axios.post<Array<Genre>>(`${controllerUrl}EditGenre?id=${id}&genreName=${value}`,
// {
// headers: {
// // Accept: 'text/plain',
// },
// // body: {}
// },
// );
// console.log(JSON.stringify(data, null, 4));
// // 👇️ "response status is: 200"
// console.log('response status is: ', status);
// // return data;
// } catch (error) {
// if (axios.isAxiosError(error)) {
// console.log('error message: ', error.message);
// // return [];
// //return error.message;
// } else {
// console.log('unexpected error: ', error);
// // return [];
// //return 'An unexpected error occurred';
// }
// }
// }

View File

@ -0,0 +1,108 @@
import axios from "axios";
import { Genre } from "./models/Types";
import { CatchError } from "./common";
const controllerUrl = 'https://localhost:7057/Dictionaries/Genre/';
export async function GetGenreList() {
try {
// 👇️ const data: GetUsersResponse
// const { data, status } = await axios.get<Array<Genre>>(`${controllerUrl}GenreList`,
const { data, } = await axios.get<Array<Genre>>(`${controllerUrl}GenreList`,
{
headers: {
Accept: 'text/plain',
},
},
);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function CreateGenre(value: string) {
try {
// 👇️ const data: GetUsersResponse
// const { data, status } = await axios.post<Array<Genre>>(`${controllerUrl}CreateGenre?genreName=${value}`,
await axios.post(`${controllerUrl}CreateGenre?genreName=${value}`,
{
headers: {
// Accept: 'text/plain',
},
// body: {}
},
);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
// return data;
}
catch (error)
{
CatchError(error);
}
}
export async function EditGenre(id : string, value: string) {
try {
// 👇️ const data: GetUsersResponse
const { status } = await axios.post(`${controllerUrl}EditGenre?id=${id}&genreName=${value}`,
{
headers: {
// Accept: 'text/plain',
},
// body: {}
},
);
//console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
console.log('response status is: ', status);
// return Promise.resolve();
// Promise.resolve();
}
catch (error)
{
CatchError(error);
}
}
export async function DeleteGenre(id : string) {
try {
// 👇️ const data: GetUsersResponse
const { status } = await axios.post(`${controllerUrl}DeleteGenre?id=${id}`,
{
headers: {
// Accept: 'text/plain',
},
// body: {}
},
);
//console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
console.log('response status is: ', status);
// return Promise.resolve();
// Promise.resolve();
}
catch (error)
{
CatchError(error);
}
}

View File

@ -0,0 +1,109 @@
import axios from "axios";
import { Language } from "./models/Types";
import { CatchError } from "./common";
const controllerUrl = 'https://localhost:7057/Dictionaries/Language/';
export async function GetLanguageList() {
try {
// 👇️ const data: GetUsersResponse
// const { data, status } = await axios.get<Array<Language>>(`${controllerUrl}List`,
const { data} = await axios.get<Array<Language>>(`${controllerUrl}List`,
{
headers: {
Accept: 'text/plain',
},
},
);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
return data;
}
catch (error)
{
CatchError(error);
}
}
export async function CreateLanguage(codeIso3: string, name: string) {
try {
// 👇️ const data: GetUsersResponse
// const { data, status } = await axios.post<Array<Genre>>(`${controllerUrl}CreateGenre?genreName=${value}`,
await axios.post(`${controllerUrl}Create?codeIso3=${codeIso3}&name=${name}`,
{
headers: {
// Accept: 'text/plain',
},
// body: {}
},
);
// console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
// return data;
}
catch (error)
{
CatchError(error);
}
}
export async function EditLanguage(id : string, codeIso3: string, name: string) {
try {
// 👇️ const data: GetUsersResponse
// const { status } = await axios.post(`${controllerUrl}Edit?id=${id}&codeIso3=${codeIso3}&name=${name}`,
await axios.post(`${controllerUrl}Edit?id=${id}&codeIso3=${codeIso3}&name=${name}`,
{
headers: {
// Accept: 'text/plain',
},
// body: {}
},
);
//console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
// console.log('response status is: ', status);
// return Promise.resolve();
// Promise.resolve();
}
catch (error)
{
CatchError(error);
}
}
export async function DeleteLanguage(id : string) {
try {
// 👇️ const data: GetUsersResponse
const { status } = await axios.post(`${controllerUrl}Delete?id=${id}`,
{
headers: {
// Accept: 'text/plain',
},
// body: {}
},
);
//console.log(JSON.stringify(data, null, 4));
// 👇️ "response status is: 200"
console.log('response status is: ', status);
// return Promise.resolve();
// Promise.resolve();
}
catch (error)
{
CatchError(error);
}
}

View File

@ -0,0 +1,14 @@
import axios, { AxiosError } from "axios";
import { ProblemDetails } from "./models/Types";
export function CatchError(error: unknown){
if (axios.isAxiosError(error)) {
console.error('error message: ', error.message);
const problemDetails = (error as AxiosError)?.response?.data as ProblemDetails;
if (problemDetails?.title) throw new Error(problemDetails.title);
}
console.error('unexpected error: ', error);
// return [];
//return 'An unexpected error occurred';
throw new Error(`unexpected error: ${(error as Error)?.message ?? JSON.stringify(error, null, 4)}`);
}

View File

@ -0,0 +1,193 @@
export type Entity = {
id : string,
deleted: boolean,
};
export enum MediaInfoType
{
Image,
Video,
Link,
OtherFile
}
export type MediaInfo = {
type: MediaInfoType,
contentType: string,
url: string,
}
export type Language = {
codeIso3: string,
name: string,
icon?: MediaInfo,
} & Entity
export type Genre = {
name: string,
} & Entity
export enum NameType
{
Original,
OriginalInAnotherLanguage,
Translation,
Abbreviation,
}
export type NameItem = {
value: string,
type: NameType,
language: Language,
}
export type Description = {
value: string,
isOriginal: boolean,
language: Language,
}
export type GenreProportion = {
proportion: number,
genre: Genre,
}
export type CommonProperties = {
names: Array<NameItem>,
preview?: MediaInfo,
descriptions: Array<Description>,
genres: Array<GenreProportion>,
relatedContent: Array<MediaInfo>,
// announcementDate?: string | Date | null,
announcementDate?: string | null,
// estimatedReleaseDate?: Date | null,
estimatedReleaseDate?: string | null,
// releaseDate?: Date | null,
releaseDate?: string | null,
}
export type AnimeItem = {
completed: boolean,
commonProperties: CommonProperties,
number?: number,
order: number,
expirationTime: Date,
} & Entity
export enum AnimeEpisodeType
{
Regilar,
FullLength,
Ova,
}
export type Episode = {
variant: number,
type: AnimeEpisodeType,
duration: Date,
} & AnimeItem
export type Season = {
episodes: Array<Episode>,
director?: string,
originCountry?: string,
} & AnimeItem
export type AnimeTitle = {
commonProperties: CommonProperties,
rate?: number,
myRate?: number,
seasons: Array<Season>,
episodes: Array<Episode>,
completed: boolean,
episodesInsideSeasonsCount: number,
expirationTime: Date,
} & Entity
export type ProblemDetails = {
type?: string,
title?: string,
status?: number,
detail?: string,
instance?: string,
extensions: Map<string, object>
}
/* POST Models */
export type TitleCreateModel = {
originalName: CreateNameModel,
preview?: MediaInfo | null
}
export type CreateNameModel = {
animeTitleId?: string | null,
animeSeasonId?: string | null,
animeEpisodeId?: string | null,
languageId: string,
value: string,
type: NameType
}
export type EditNameModel = {
newLanguageId?: string | null,
newValue?: string | null,
} & CreateNameModel
export type DeleteNameModel = { } & CreateNameModel
export type CreateDescriptionModel = {
animeTitleId?: string | null,
animeSeasonId?: string | null,
animeEpisodeId?: string | null,
languageId: string,
value: string,
isOriginal: boolean
}
export type EditDescriptionModel = {
newLanguageId?: string | null,
newValue?: string | null,
} & CreateDescriptionModel
export type DeleteDescriptionModel = { } & CreateDescriptionModel
export type CreateMediaInfoModel = {
animeTitleId?: string | null,
animeSeasonId?: string | null,
animeEpisodeId?: string | null,
type: MediaInfoType,
url: string,
contentType: string
}
export type EditMediaInfoModel = {
type?: MediaInfoType | null,
url?: string | null,
contentType?: string | null,
} & CreateMediaInfoModel
export type DeleteMediaInfoModel = { } & CreateMediaInfoModel
export type EditDateModel = {
animeTitleId?: string | null,
animeSeasonId?: string | null,
animeEpisodeId?: string | null,
value?: Date | null,
}
export type RateEditModel = {
animeTitleId?: string | null,
animeSeasonId?: string | null,
animeEpisodeId?: string | null,
ratePercentage: number,
}
export type RateDeleteModel = {
animeTitleId?: string | null,
animeSeasonId?: string | null,
animeEpisodeId?: string | null,
}

View File

@ -1,5 +1,7 @@
//API Urls //API Urls
export const baseUrl = 'https://localhost:7057';
export const apiUrls = { export const apiUrls = {
auth: "/auth/v1/", auth: "/auth/v1/",
product: "/customerapi/v1/Product", product: "/customerapi/v1/Product",

Some files were not shown because too many files have changed in this diff Show More