Website UI redesign and QOL changes (#601)

* Initial support for leaderboards and some refactoring

* Start of UI redesign

* Finish slot and user redesign, added deletion of comments, reviews, scores, and photos

* Remove leftover debug print

* Fix bug in permission check

* Simplify sidebar code and add hearted and queued levels

* Fix navbar scrolling on mobile and refactor SlotCardPartial
This commit is contained in:
Josh 2022-12-19 17:20:49 -06:00 committed by GitHub
parent 37b0925cba
commit f4cad21061
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 779 additions and 255 deletions

View file

@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Controllers.Admin;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Controllers.Moderator;
[ApiController]
[Route("moderation/case/{id:int}")]

View file

@ -0,0 +1,122 @@
using LBPUnion.ProjectLighthouse.Administration;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.PlayerData.Reviews;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Controllers.Moderator;
[ApiController]
[Route("moderation")]
public class ModerationRemovalController : ControllerBase
{
private readonly Database database;
public ModerationRemovalController(Database database)
{
this.database = database;
}
private async Task<IActionResult> Delete<T>(DbSet<T> dbSet, int id, string? callbackUrl, Func<User, int, Task<T?>> getHandler) where T: class
{
User? user = this.database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
T? item = await getHandler(user, id);
if (item == null) return this.Redirect("~/404");
dbSet.Remove(item);
await this.database.SaveChangesAsync();
return this.Redirect(callbackUrl ?? "~/");
}
[HttpGet("deleteScore/{scoreId:int}")]
public async Task<IActionResult> DeleteScore(int scoreId, [FromQuery] string? callbackUrl)
{
return await this.Delete<Score>(this.database.Scores, scoreId, callbackUrl, async (user, id) =>
{
Score? score = await this.database.Scores.Include(s => s.Slot).FirstOrDefaultAsync(s => s.ScoreId == id);
if (score == null) return null;
if (!user.IsModerator && score.Slot.CreatorId != user.UserId) return null;
return score;
});
}
[HttpGet("deleteComment/{commentId:int}")]
public async Task<IActionResult> DeleteComment(int commentId, [FromQuery] string? callbackUrl)
{
User? user = this.database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
Comment? comment = await this.database.Comments.FirstOrDefaultAsync(c => c.CommentId == commentId);
if (comment == null) return this.Redirect("~/404");
if (comment.Deleted) return this.Redirect(callbackUrl ?? "~/");
bool canDelete;
switch (comment.Type)
{
case CommentType.Level:
int slotCreatorId = await this.database.Slots.Where(s => s.SlotId == comment.TargetId)
.Select(s => s.CreatorId)
.FirstOrDefaultAsync();
canDelete = user.UserId == comment.PosterUserId || user.UserId == slotCreatorId;
break;
case CommentType.Profile:
canDelete = user.UserId == comment.PosterUserId || user.UserId == comment.TargetId;
break;
default: throw new ArgumentOutOfRangeException();
}
if (!canDelete && !user.IsModerator) return this.Redirect(callbackUrl ?? "~/");
comment.Deleted = true;
comment.DeletedBy = user.Username;
comment.DeletedType = !canDelete && user.IsModerator ? "moderator" : "user";
await this.database.SaveChangesAsync();
return this.Redirect(callbackUrl ?? "~/");
}
[HttpGet("deleteReview/{reviewId:int}")]
public async Task<IActionResult> DeleteReview(int reviewId, [FromQuery] string? callbackUrl)
{
User? user = this.database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
Review? review = await this.database.Reviews.Include(r => r.Slot).FirstOrDefaultAsync(c => c.ReviewId == reviewId);
if (review == null) return this.Redirect("~/404");
if (review.Deleted) return this.Redirect(callbackUrl ?? "~/");
bool canDelete = review.Slot?.CreatorId == user.UserId;
if (!canDelete && !user.IsModerator) return this.Redirect(callbackUrl ?? "~/");
review.Deleted = true;
review.DeletedBy = !canDelete && user.IsModerator ? DeletedBy.Moderator : DeletedBy.LevelAuthor;
await this.database.SaveChangesAsync();
return this.Redirect(callbackUrl ?? "~/");
}
[HttpGet("deletePhoto/{photoId:int}")]
public async Task<IActionResult> DeletePhoto(int photoId, [FromQuery] string? callbackUrl)
{
return await this.Delete<Photo>(this.database.Photos, photoId, callbackUrl, async (user, id) =>
{
Photo? photo = await this.database.Photos.Include(p => p.Slot).FirstOrDefaultAsync(p => p.PhotoId == id);
if (photo == null) return null;
if (!user.IsModerator && photo.Slot?.CreatorId != user.UserId) return null;
return photo;
});
}
}

View file

@ -1,12 +1,12 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using LBPUnion.ProjectLighthouse.Helpers;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Controllers.Admin;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Controllers.Moderator;
[ApiController]
[Route("moderation/slot/{id:int}")]

View file

@ -1,3 +1,4 @@
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using Microsoft.AspNetCore.Html;
@ -13,7 +14,9 @@ public static class PartialExtensions
public static ViewDataDictionary<T> WithTime<T>(this ViewDataDictionary<T> viewData, string timeZone) => WithKeyValue(viewData, "TimeZone", timeZone);
private static ViewDataDictionary<T> WithKeyValue<T>(this ViewDataDictionary<T> viewData, string key, object value)
public static ViewDataDictionary<T> CanDelete<T>(this ViewDataDictionary<T> viewData, bool canDelete) => WithKeyValue(viewData, "CanDelete", canDelete);
private static ViewDataDictionary<T> WithKeyValue<T>(this ViewDataDictionary<T> viewData, string key, object? value)
{
try
{
@ -33,6 +36,25 @@ public static class PartialExtensions
public static Task<IHtmlContent> ToLink<T>(this User user, IHtmlHelper<T> helper, ViewDataDictionary<T> viewData, string language, string timeZone = "", bool includeStatus = false)
=> helper.PartialAsync("Partials/Links/UserLinkPartial", user, viewData.WithLang(language).WithTime(timeZone).WithKeyValue("IncludeStatus", includeStatus));
public static Task<IHtmlContent> ToHtml<T>(this Photo photo, IHtmlHelper<T> helper, ViewDataDictionary<T> viewData, string language, string timeZone)
=> helper.PartialAsync("Partials/PhotoPartial", photo, viewData.WithLang(language).WithTime(timeZone));
public static Task<IHtmlContent> ToHtml<T>
(
this Slot slot,
IHtmlHelper<T> helper,
ViewDataDictionary<T> viewData,
User? user,
string callbackUrl,
string language = "",
string timeZone = "",
bool isMobile = false,
bool showLink = false,
bool isMini = false
) =>
helper.PartialAsync("Partials/SlotCardPartial", slot, viewData.WithLang(language).WithTime(timeZone)
.WithKeyValue("User", user)
.WithKeyValue("CallbackUrl", callbackUrl)
.WithKeyValue("ShowLink", showLink)
.WithKeyValue("IsMobile", isMobile));
public static Task<IHtmlContent> ToHtml<T>(this Photo photo, IHtmlHelper<T> helper, ViewDataDictionary<T> viewData, string language, string timeZone, bool canDelete = false)
=> helper.PartialAsync("Partials/PhotoPartial", photo, viewData.WithLang(language).WithTime(timeZone).CanDelete(canDelete));
}

View file

@ -1,5 +1,5 @@
@page "/verifyEmail"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.CompleteEmailVerificationPage
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email.CompleteEmailVerificationPage
@{
Layout = "Layouts/BaseLayout";

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email;
public class CompleteEmailVerificationPage : BaseLayout
{

View file

@ -1,5 +1,5 @@
@page "/login/sendVerificationEmail"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SendVerificationEmailPage
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email.SendVerificationEmailPage
@{
Layout = "Layouts/BaseLayout";

View file

@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email;
public class SendVerificationEmailPage : BaseLayout
{

View file

@ -1,7 +1,7 @@
@page "/login/setEmail"
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SetEmailForm
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email.SetEmailForm
@{
Layout = "Layouts/BaseLayout";

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email;
public class SetEmailForm : BaseLayout
{

View file

@ -63,7 +63,7 @@ else
<div class="ui left aligned segment">
@foreach (Slot slot in Model.LatestTeamPicks!) @* Can't reach a point where this is null *@
{
@await Html.PartialAsync("Partials/SlotCardPartial", slot, Model.GetSlotViewData(slot.SlotId, isMobile))
@await slot.ToHtml(Html, ViewData, Model.User, $"~/slot/{slot.SlotId}", language, timeZone, isMobile, true, true)
<br>
}
</div>
@ -80,7 +80,7 @@ else
<div class="ui left aligned segment">
@foreach (Slot slot in Model.NewestLevels!) @* Can't reach a point where this is null *@
{
@await Html.PartialAsync("Partials/SlotCardPartial", slot, Model.GetSlotViewData(slot.SlotId, isMobile))
@await slot.ToHtml(Html, ViewData, Model.User, $"~/slot/{slot.SlotId}", language, timeZone, isMobile, true, true)
<br>
}
</div>

View file

@ -5,7 +5,6 @@ using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
@ -53,24 +52,4 @@ public class LandingPage : BaseLayout
return this.Page();
}
public ViewDataDictionary GetSlotViewData(int slotId, bool isMobile = false)
=> new(ViewData)
{
{
"User", this.User
},
{
"CallbackUrl", $"~/slot/{slotId}"
},
{
"ShowLink", true
},
{
"IsMini", true
},
{
"IsMobile", isMobile
},
};
}

View file

@ -1,7 +1,7 @@
@page "/login"
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.LoginForm
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login.LoginForm
@{
Layout = "Layouts/BaseLayout";

View file

@ -13,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login;
public class LoginForm : BaseLayout
{

View file

@ -1,6 +1,6 @@
@page "/logout"
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.LogoutPage
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login.LogoutPage
@{
Layout = "Layouts/BaseLayout";

View file

@ -1,10 +1,9 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login;
public class LogoutPage : BaseLayout
{

View file

@ -1,6 +1,6 @@
@page "/passwordReset"
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetPage
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login.PasswordResetPage
@{
Layout = "Layouts/BaseLayout";

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login;
public class PasswordResetPage : BaseLayout
{

View file

@ -1,6 +1,6 @@
@page "/passwordResetRequest"
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetRequestForm
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login.PasswordResetRequestForm
@{
Layout = "Layouts/BaseLayout";

View file

@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login;
public class PasswordResetRequestForm : BaseLayout
{

View file

@ -1,5 +1,5 @@
@page "/passwordResetRequired"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetRequiredPage
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login.PasswordResetRequiredPage
@{
Layout = "Layouts/BaseLayout";

View file

@ -1,10 +1,9 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login;
public class PasswordResetRequiredPage : BaseLayout
{

View file

@ -1,5 +1,5 @@
@page "/pirate"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PirateSignupPage
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login.PirateSignupPage
@{
Layout = "Layouts/BaseLayout";

View file

@ -2,7 +2,7 @@ using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login;
public class PirateSignupPage : BaseLayout
{

View file

@ -1,7 +1,7 @@
@page "/register"
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.RegisterForm
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login.RegisterForm
@{
Layout = "Layouts/BaseLayout";

View file

@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login;
public class RegisterForm : BaseLayout
{

View file

@ -2,12 +2,15 @@
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Levels
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.HiddenLevelsPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = Model.Translate(ModPanelStrings.HiddenLevels);
bool isMobile = Model.Request.IsMobile();
string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
}
<p>There are @Model.SlotCount hidden levels.</p>
@ -15,21 +18,7 @@
@foreach (Slot slot in Model.Slots)
{
<div class="ui segment">
@await Html.PartialAsync("Partials/SlotCardPartial", slot, new ViewDataDictionary(ViewData)
{
{
"User", Model.User
},
{
"CallbackUrl", $"~/moderation/hiddenLevels/{Model.PageNumber}"
},
{
"ShowLink", true
},
{
"IsMobile", isMobile
},
})
@await slot.ToHtml(Html, ViewData, Model.User, $"~/moderation/hiddenLevels/{Model.PageNumber}", language, timeZone, isMobile, true)
</div>
}

View file

@ -8,10 +8,10 @@
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
int pageOwnerId = (int?)ViewData["PageOwner"] ?? 0;
}
<div class="ui yellow segment" id="comments">
<h2>Comments</h2>
@if (Model.Comments.Count == 0 && Model.CommentsEnabled)
{
<p>There are no comments.</p>
@ -86,6 +86,12 @@
{
<span>@decodedMessage</span>
}
@if (((Model.User?.IsModerator ?? false) || Model.User?.UserId == comment.PosterUserId || Model.User?.UserId == pageOwnerId) && !comment.Deleted)
{
<button class="ui red icon button" style="display:inline-flex; float: right" onclick="deleteComment(@comment.CommentId)">
<i class="trash icon"></i>
</button>
}
<p>
<i>@TimeZoneInfo.ConvertTime(timestamp, timeZoneInfo).ToString("M/d/yyyy @ h:mm:ss tt")</i>
</p>
@ -96,4 +102,12 @@
</div>
</div>
}
<script>
function deleteComment(commentId){
if (window.confirm("Are you sure you want to delete this?\nThis action cannot be undone.")){
window.location.hash = "comments";
window.location.href = "/moderation/deleteComment/" + commentId + "?callbackUrl=" + this.window.location;
}
}
</script>
</div>

View file

@ -0,0 +1,70 @@
@using LBPUnion.ProjectLighthouse
@using LBPUnion.ProjectLighthouse.Localization
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@using Microsoft.EntityFrameworkCore
@{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
bool canDelete = (bool?)ViewData["CanDelete"] ?? false;
}
<div class="ui blue segment" id="scores">
@if (Model.Scores.Count == 0)
{
<p>There are no scores.</p>
}
else
{
int count = Model.Scores.Count;
<p>There @(count == 1 ? "is" : "are") @count score@(count == 1 ? "" : "s").</p>
<div class="ui divider"></div>
}
<div class="ui list">
@for(int i = 0; i < Model.Scores.Count; i++)
{
Score score = Model.Scores[i];
string[] playerIds = score.PlayerIds;
Database database = Model.Database;
<div class="item">
<span class="ui large text">
@if(canDelete)
{
<button class="ui red icon button" style="display: inline-block; position: absolute; right: 1em" onclick="deleteScore(@score.ScoreId)">
<i class="trash icon"></i>
</button>
}
<b>@(i+1):</b>
<span class="ui text">@score.Points points</span>
</span>
<div class="content">
<div class="list" style="padding-top: 0">
@for (int j = 0; j < playerIds.Length; j++)
{
User? user = await database.Users.FirstOrDefaultAsync(u => u.Username == playerIds[j]);
if (user == null) continue;
<div class="item">
<i class="minus icon" style="padding-top: 9px"></i>
<div class="content" style="padding-left: 0">
@await user.ToLink(Html, ViewData, language, timeZone)
</div>
</div>
}
</div>
</div>
</div>
@if (i != Model.Scores.Count - 1)
{
<div class="ui divider"></div>
}
}
</div>
<script>
function deleteScore(scoreId){
if (window.confirm("Are you sure you want to delete this?\nThis action cannot be undone.")){
window.location.hash = "scores";
window.location.href = "/moderation/deleteScore/" + scoreId + "?callbackUrl=" + this.window.location;
}
}
</script>
</div>

View file

@ -1,4 +1,3 @@
@using System.Globalization
@using System.Web
@using LBPUnion.ProjectLighthouse.Levels
@using LBPUnion.ProjectLighthouse.Localization
@ -9,15 +8,30 @@
@{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
bool canDelete = (bool?)ViewData["CanDelete"] ?? false;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
}
<div style="position: relative">
@if (canDelete)
{
<button class="ui red icon button" style="position: absolute; right: 0.5em; top: 0.5em" onclick="deletePhoto(@Model.PhotoId)">
<i class="trash icon"></i>
</button>
<script>
function deletePhoto(photoId){
if (window.confirm("Are you sure you want to delete this?\nThis action cannot be undone.")){
window.location.hash = "photos";
window.location.href = "/moderation/deletePhoto/" + photoId + "?callbackUrl=" + this.window.location;
}
}
</script>
}
<canvas class="hide-subjects" id="canvas-subjects-@Model.PhotoId" width="1920" height="1080"
style="position: absolute; transform: rotate(180deg)">
</canvas>
<img id="game-image-@Model.PhotoId" src="/gameAssets/@Model.LargeHash"
style="width: 100%; height: auto; border-radius: .28571429rem;">
style="width: 100%; height: auto; border-radius: .28571429rem;" alt="Photo">
</div>
<br>
@ -50,6 +64,12 @@
case SlotType.Local:
<span>in a level on the moon</span>
break;
case SlotType.Moon:
case SlotType.Unknown:
case SlotType.Unknown2:
case SlotType.DLC:
default:
throw new ArgumentOutOfRangeException();
}
}
at @TimeZoneInfo.ConvertTime(DateTime.UnixEpoch.AddSeconds(Model.Timestamp), timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt")

View file

@ -0,0 +1,129 @@
@using System.Web
@using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Files
@using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.PlayerData.Reviews
@{
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool canDelete = (bool?)ViewData["CanDelete"] ?? false;
}
<div class="eight wide column" id="reviews">
<div class="ui purple segment">
@if (Model.Reviews.Count == 0 && Model.ReviewsEnabled)
{
<p>There are no reviews.</p>
}
else if (!Model.ReviewsEnabled)
{
<b>
<i>Reviews are disabled on this level.</i>
</b>
}
else
{
int count = Model.Reviews.Count;
<p>There @(count == 1 ? "is" : "are") @count review@(count == 1 ? "" : "s").</p>
<div class="ui divider"></div>
}
@for(int i = 0; i < Model.Reviews.Count; i++)
{
Review review = Model.Reviews[i];
string faceHash = (review.Thumb switch {
-1 => review.Reviewer?.BooHash,
0 => review.Reviewer?.MehHash,
1 => review.Reviewer?.YayHash,
_ => throw new ArgumentOutOfRangeException(),
}) ?? "";
if (string.IsNullOrWhiteSpace(faceHash) || !FileHelper.ResourceExists(faceHash))
{
faceHash = ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash;
}
string faceAlt = review.Thumb switch {
-1 => "Boo!",
0 => "Meh.",
1 => "Yay!",
_ => throw new ArgumentOutOfRangeException(),
};
int size = isMobile ? 50 : 100;
<div class="card">
<div>
<img class="cardIcon slotCardIcon" src="@ServerConfiguration.Instance.ExternalUrl/gameAssets/@faceHash" alt="@faceAlt" title="@faceAlt" style="min-width: @(size)px; width: @(size)px; height: @(size)px">
</div>
<div class="cardStats">
<h3 style="margin-bottom: 5px;">@review.Reviewer?.Username</h3>
@if (review.Deleted)
{
if (review.DeletedBy == DeletedBy.LevelAuthor)
{
<p>
<i>This review has been deleted by the level author.</i>
</p>
}
else
{
<p>
<i>This review has been deleted by a moderator.</i>
</p>
}
}
else
{
@if (review.Labels.Length > 1)
{
@foreach (string reviewLabel in review.Labels)
{
<div class="ui blue label">@LabelHelper.TranslateTag(reviewLabel)</div>
}
}
@if (string.IsNullOrWhiteSpace(review.Text))
{
<p>
<i>This review contains no text.</i>
</p>
}
else
{
{
<p>@HttpUtility.HtmlDecode(review.Text)</p>
}
}
}
</div>
@if (canDelete && !review.Deleted)
{
<div style="display: inline-block; right: 1em; position: absolute;">
<button class="ui red icon button" onclick="deleteReview(@review.ReviewId)">
<i class="trash icon"></i>
</button>
<script>
function deleteReview(reviewId){
if (window.confirm("Are you sure you want to delete this?\nThis action cannot be undone.")){
window.location.hash = "reviews";
window.location.href = "/moderation/deleteReview/" + reviewId + "?callbackUrl=" + this.window.location;
}
}
</script>
</div>
}
</div>
@if (i != Model.Reviews.Count - 1)
{
<div class="ui divider"></div>
}
}
</div>
@if (isMobile)
{
<br/>
}
</div>

View file

@ -24,8 +24,9 @@
@foreach (Photo photo in Model.Photos)
{
bool canDelete = Model.User != null && (Model.User.IsModerator || Model.User.UserId == photo.CreatorId);
<div class="ui segment">
@await photo.ToHtml(Html, ViewData, language, timeZone)
@await photo.ToHtml(Html, ViewData, language, timeZone, canDelete)
</div>
}

View file

@ -1,12 +1,10 @@
@page "/slot/{id:int}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Files
@using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.PlayerData.Reviews
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotPage
@ -17,7 +15,7 @@
Model.Title = HttpUtility.HtmlDecode(Model.Slot?.Name ?? "");
Model.Description = HttpUtility.HtmlDecode(Model.Slot?.Description ?? "");
bool isMobile = this.Request.IsMobile();
bool isMobile = Request.IsMobile();
string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
}
@ -34,21 +32,7 @@
</div>
}
@await Html.PartialAsync("Partials/SlotCardPartial", Model.Slot, new ViewDataDictionary(ViewData)
{
{
"User", Model.User
},
{
"CallbackUrl", $"~/slot/{Model.Slot?.SlotId}"
},
{
"ShowLink", false
},
{
"IsMobile", Model.Request.IsMobile()
},
})
@await Model.Slot.ToHtml(Html, ViewData, Model.User, $"~/slot/{Model.Slot?.SlotId}", language, timeZone, isMobile)
<br>
<div class="@(isMobile ? "" : "ui grid")">
@ -66,7 +50,6 @@
<div class="ui red segment">
<h2>Tags</h2>
@{
string[] authorLabels;
if (Model.Slot?.GameVersion == GameVersion.LittleBigPlanet1)
{
@ -96,137 +79,91 @@
{
<br/>
}
<div class="eight wide column">
@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone))
</div>
@if (isMobile)
{
<br/>
}
<div class="eight wide column">
<div class="ui purple segment">
<h2>Reviews</h2>
@if (Model.Reviews.Count == 0 && Model.ReviewsEnabled)
{
<p>There are no reviews.</p>
}
else if (!Model.ReviewsEnabled)
{
<b>
<i>Reviews are disabled on this level.</i>
</b>
}
else
{
int count = Model.Reviews.Count;
<p>There @(count == 1 ? "is" : "are") @count review@(count == 1 ? "" : "s").</p>
<div class="ui divider"></div>
}
@for(int i = 0; i < Model.Reviews.Count; i++)
{
Review review = Model.Reviews[i];
string faceHash = (review.Thumb switch {
-1 => review.Reviewer?.BooHash,
0 => review.Reviewer?.MehHash,
1 => review.Reviewer?.YayHash,
_ => throw new ArgumentOutOfRangeException(),
}) ?? "";
if (string.IsNullOrWhiteSpace(faceHash) || !FileHelper.ResourceExists(faceHash))
{
faceHash = ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash;
}
string faceAlt = review.Thumb switch {
-1 => "Boo!",
0 => "Meh.",
1 => "Yay!",
_ => throw new ArgumentOutOfRangeException(),
};
int size = isMobile ? 50 : 100;
<div class="card">
<div>
<img class="cardIcon slotCardIcon" src="@ServerConfiguration.Instance.ExternalUrl/gameAssets/@faceHash" alt="@faceAlt" title="@faceAlt" style="min-width: @(size)px; width: @(size)px; height: @(size)px">
</div>
<div class="cardStats">
<h3 style="margin-bottom: 5px;">@review.Reviewer?.Username</h3>
@if (review.Deleted)
{
if (review.DeletedBy == DeletedBy.LevelAuthor)
{
<p>
<i>This review has been deleted by the level author.</i>
</p>
}
else
{
<p>
<i>This review has been deleted by a moderator.</i>
</p>
}
}
else
{
@if (review.Labels.Length > 1)
{
@foreach (string reviewLabel in review.Labels)
{
<div class="ui blue label">@LabelHelper.TranslateTag(reviewLabel)</div>
}
}
@if (string.IsNullOrWhiteSpace(review.Text))
{
<p>
<i>This review contains no text.</i>
</p>
}
else
{
{
<p>@HttpUtility.HtmlDecode(review.Text)</p>
}
}
}
</div>
</div>
@if (i != Model.Reviews.Count - 1)
{
<div class="ui divider"></div>
}
}
</div>
@if (isMobile)
{
<br/>
}
</div>
</div>
@if (Model.Photos.Count != 0)
{
<div class="ui purple segment">
<h2>Most recent photos</h2>
<div class="ui grid">
@{
string outerDiv = isMobile ?
"horizontal-scroll" :
"three wide column";
string innerDiv = isMobile ?
"ui top attached tabular menu horizontal-scroll" :
"ui vertical fluid tabular menu";
}
<div class="@outerDiv">
<div class="@innerDiv">
<a class="item active lh-sidebar" target="lh-comments">
Comments
</a>
<a class="item lh-sidebar" target="lh-photos">
@Model.Translate(BaseLayoutStrings.HeaderPhotos)
</a>
<a class="item lh-sidebar" target="lh-reviews">
Reviews
</a>
<a class="item lh-sidebar" target="lh-scores">
Scores
</a>
</div>
</div>
@{
string divLength = isMobile ? "sixteen" : "thirteen";
}
<div class="@divLength wide stretched column">
<div class="lh-content" id="lh-comments">
@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone))
</div>
<div class="lh-content" id="lh-photos">
<div class="ui purple segment" id="photos">
@if (Model.Photos.Count != 0)
{
<div class="ui center aligned grid">
@foreach (Photo photo in Model.Photos)
{
<div class="eight wide column">
@await photo.ToHtml(Html, ViewData, language, timeZone)
string width = isMobile ? "sixteen" : "eight";
bool canDelete = Model.User != null && (Model.User.IsModerator || Model.User.UserId == photo.CreatorId || Model.User.UserId == Model.Slot?.SlotId);
<div class="@width wide column">
@await photo.ToHtml(Html, ViewData, language, timeZone, canDelete)
</div>
}
</div>
</div>
@if (isMobile)
{
<br/>
}
}
}
else
{
<p>This level has no photos yet.</p>
}
</div>
</div>
<div class="lh-content" id="lh-reviews">
@await Html.PartialAsync("Partials/ReviewPartial", new ViewDataDictionary(ViewData)
{
{
"isMobile", isMobile
},
{
"CanDelete", (Model.User?.IsModerator ?? false) || Model.Slot?.CreatorId == Model.User?.UserId
},
})
</div>
<div class="lh-content" id="lh-scores">
<div class="eight wide column">
@await Html.PartialAsync("Partials/LeaderboardPartial",
ViewData.WithLang(language).WithTime(timeZone).CanDelete(Model.User?.IsModerator ?? false))
</div>
</div>
</div>
</div>
@if (isMobile)
{
<br/>
}
@if (Model.User != null && Model.User.IsModerator)
{
@ -282,3 +219,50 @@
<br/>
}
}
<script>
const sidebarElements = Array.from(document.querySelectorAll(".lh-sidebar"));
const contentElements = Array.from(document.querySelectorAll(".lh-content"));
let selectedId = window.location.hash;
if (selectedId.startsWith("#"))
selectedId = selectedId.substring(1);
let selectedElement = document.getElementById(selectedId);
// id = lh-sidebar element
function setVisible(e){
let eTarget = document.getElementById(e.target);
if (!e || !eTarget) return;
// make all active elements not active
for (let active of document.getElementsByClassName("active")) {
active.classList.remove("active");
}
// hide all content divs
for (let i = 0; i < contentElements.length; i++){
contentElements[i].style.display = "none";
}
// unhide content
eTarget.style.display = "";
e.classList.add("active");
}
sidebarElements.forEach(el => {
if (el.classList.contains("active")){
setVisible(el);
}
el.addEventListener('click', event => {
if (!event.target.target) return;
setVisible(event.target)
})
})
// set the active content window based on url
if (selectedElement != null) {
while (selectedElement != null && !selectedElement.classList.contains("lh-content")){
selectedElement = selectedElement.parentElement;
}
let sidebarEle = document.querySelector("[target=" + selectedElement.id + "]")
setVisible(sidebarEle);
}
</script>

View file

@ -16,6 +16,7 @@ public class SlotPage : BaseLayout
public List<Comment> Comments = new();
public List<Review> Reviews = new();
public List<Photo> Photos = new();
public List<Score> Scores = new();
public bool CommentsEnabled;
public readonly bool ReviewsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelReviewsEnabled;
@ -93,6 +94,12 @@ public class SlotPage : BaseLayout
.Take(10)
.ToListAsync();
this.Scores = await this.Database.Scores.OrderByDescending(s => s.Points)
.ThenByDescending(s => s.ScoreId)
.Where(s => s.SlotId == id)
.Take(10)
.ToListAsync();
if (this.User == null) return this.Page();
foreach (Comment c in this.Comments)

View file

@ -2,6 +2,7 @@
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Levels
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotsPage
@{
@ -9,6 +10,8 @@
Model.Title = Model.Translate(BaseLayoutStrings.HeaderSlots);
bool isMobile = Model.Request.IsMobile();
string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
}
<p>There are @Model.SlotCount total levels!</p>
@ -24,21 +27,7 @@
@foreach (Slot slot in Model.Slots)
{
<div class="ui segment">
@await Html.PartialAsync("Partials/SlotCardPartial", slot, new ViewDataDictionary(ViewData)
{
{
"User", Model.User
},
{
"CallbackUrl", $"~/slots/{Model.PageNumber}"
},
{
"ShowLink", true
},
{
"IsMobile", isMobile
},
})
@await slot.ToHtml(Html, ViewData, Model.User, $"~/slots/{Model.PageNumber}", language, timeZone, isMobile, true)
</div>
}

View file

@ -2,6 +2,7 @@
@using System.Web
@using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Levels
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@ -123,29 +124,133 @@
}
</div>
<div class="ui grid">
@{
string outerDiv = isMobile ?
"horizontal-scroll" :
"three wide column";
string innerDiv = isMobile ?
"ui top attached tabular menu horizontal-scroll" :
"ui vertical fluid tabular menu";
}
<div class="@outerDiv">
<div class="@innerDiv">
<a class="item active lh-sidebar" target="lh-comments">
Comments
</a>
<a class="item lh-sidebar" target="lh-photos">
@Model.Translate(BaseLayoutStrings.HeaderPhotos)
</a>
@if (Model.Photos != null && Model.Photos.Count != 0)
{
<div class="ui purple segment">
<h2>@Model.Translate(GeneralStrings.RecentPhotos)</h2>
<a class="item lh-sidebar" target="lh-levels">
@Model.Translate(BaseLayoutStrings.HeaderSlots)
</a>
<a class="item lh-sidebar" target="lh-playlists">
Playlists
</a>
@if (Model.User == Model.ProfileUser)
{
<a class="item lh-sidebar" target="lh-hearted">
Hearted Levels
</a>
<a class="item lh-sidebar" target="lh-queued">
Queued Levels
</a>
}
</div>
</div>
@{
string divLength = isMobile ? "sixteen" : "thirteen";
}
<div class="@divLength wide stretched column">
<div class="lh-content" id="lh-comments">
@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone))
</div>
<div class="lh-content" id="lh-photos">
<div class="ui purple segment" id="photos">
@if (Model.Photos != null && Model.Photos.Count != 0)
{
<div class="ui center aligned grid">
@foreach (Photo photo in Model.Photos)
{
string width = isMobile ? "sixteen" : "eight";
bool canDelete = Model.User != null && (Model.User.IsModerator || Model.User.UserId == photo.CreatorId);
<div class="@width wide column">
@await photo.ToHtml(Html, ViewData, language, timeZone)
@await photo.ToHtml(Html, ViewData, language, timeZone, canDelete)
</div>
}
</div>
</div>
@if (isMobile)
{
<br/>
}
}
@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone))
}
else
{
<p>This user hasn't uploaded any photos</p>
}
</div>
</div>
<div class="lh-content" id="lh-levels">
<div class="ui green segment" id="levels">
@if (Model.HeartedSlots?.Count == 0)
{
<p>This user hasn't published any levels</p>
}
@foreach (Slot slot in Model.Slots ?? new List<Slot>())
{
<div class="ui segment">
@await slot.ToHtml(Html, ViewData, Model.User, $"~/user/{Model.ProfileUser.UserId}#levels", language, timeZone, isMobile, true)
</div>
}
</div>
</div>
<div class="lh-content" id="lh-playlists">
<div class="ui purple segment">
<p>@Model.Translate(GeneralStrings.Soon)</p>
</div>
</div>
@if (Model.User == Model.ProfileUser)
{
<div class="lh-content" id="lh-hearted">
<div class="ui pink segment" id="hearted">
@if (Model.HeartedSlots?.Count == 0)
{
<p>You haven't hearted any levels</p>
}
else
{
<p>You have hearted @(Model.HeartedSlots?.Count) levels</p>
}
@foreach (Slot slot in Model.HeartedSlots ?? new List<Slot>())
{
<div class="ui segment">
@await slot.ToHtml(Html, ViewData, Model.User, $"~/user/{Model.ProfileUser.UserId}#hearted", language, timeZone, isMobile, true)
</div>
}
</div>
</div>
<div class="lh-content" id="lh-queued">
<div class="ui yellow segment" id="queued">
@if (Model.QueuedSlots?.Count == 0)
{
<p>You haven't queued any levels</p>
}
else
{
<p>There are @(Model.QueuedSlots?.Count) levels in your queue</p>
}
@foreach (Slot slot in Model.QueuedSlots ?? new List<Slot>())
{
<div class="ui segment">
@await slot.ToHtml(Html, ViewData, Model.User, $"~/user/{Model.ProfileUser.UserId}#queued", language, timeZone, isMobile, true)
</div>
}
</div>
</div>
}
</div>
</div>
@if (Model.User != null && Model.User.IsModerator)
{
@ -192,3 +297,50 @@
<br/>
}
}
<script>
const sidebarElements = Array.from(document.querySelectorAll(".lh-sidebar"));
const contentElements = Array.from(document.querySelectorAll(".lh-content"));
let selectedId = window.location.hash;
if (selectedId.startsWith("#"))
selectedId = selectedId.substring(1);
let selectedElement = document.getElementById(selectedId);
// id = lh-sidebar element
function setVisible(e){
let eTarget = document.getElementById(e.target);
if (!e || !eTarget) return;
// make all active elements not active
for (let active of document.getElementsByClassName("active")) {
active.classList.remove("active");
}
// hide all content divs
for (let i = 0; i < contentElements.length; i++){
contentElements[i].style.display = "none";
}
// unhide content
eTarget.style.display = "";
e.classList.add("active");
}
sidebarElements.forEach(el => {
if (el.classList.contains("active")){
setVisible(el);
}
el.addEventListener('click', event => {
if (!event.target.target) return;
setVisible(event.target)
})
})
// set the active content window based on url
if (selectedElement != null) {
while (selectedElement != null && !selectedElement.classList.contains("lh-content")){
selectedElement = selectedElement.parentElement;
}
let sidebarEle = document.querySelector("[target=" + selectedElement.id + "]")
setVisible(sidebarEle);
}
</script>

View file

@ -1,5 +1,6 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
@ -18,6 +19,10 @@ public class UserPage : BaseLayout
public bool IsProfileUserHearted;
public List<Photo>? Photos;
public List<Slot>? Slots;
public List<Slot>? HeartedSlots;
public List<Slot>? QueuedSlots;
public User? ProfileUser;
public UserPage(Database database) : base(database)
@ -50,7 +55,33 @@ public class UserPage : BaseLayout
}
}
this.Photos = await this.Database.Photos.Include(p => p.Slot).OrderByDescending(p => p.Timestamp).Where(p => p.CreatorId == userId).Take(6).ToListAsync();
this.Photos = await this.Database.Photos.Include(p => p.Slot)
.OrderByDescending(p => p.Timestamp)
.Where(p => p.CreatorId == userId)
.Take(6)
.ToListAsync();
this.Slots = await this.Database.Slots.Include(p => p.Creator)
.OrderByDescending(s => s.LastUpdated)
.Where(p => p.CreatorId == userId)
.Take(10)
.ToListAsync();
if (this.User == this.ProfileUser)
{
this.QueuedSlots = await this.Database.QueuedLevels.Include(h => h.Slot)
.Where(h => this.User != null && h.UserId == this.User.UserId)
.Select(h => h.Slot)
.Where(s => s.Type == SlotType.User)
.Take(10)
.ToListAsync();
this.HeartedSlots = await this.Database.HeartedLevels.Include(h => h.Slot)
.Where(h => this.User != null && h.UserId == this.User.UserId)
.Select(h => h.Slot)
.Where(s => s.Type == SlotType.User)
.Take(10)
.ToListAsync();
}
this.CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled && this.ProfileUser.CommentsEnabled;
if (this.CommentsEnabled)
@ -70,12 +101,16 @@ public class UserPage : BaseLayout
foreach (Comment c in this.Comments)
{
Reaction? reaction = await this.Database.Reactions.FirstOrDefaultAsync(r => r.UserId == this.User.UserId && r.TargetId == c.CommentId);
Reaction? reaction = await this.Database.Reactions.Where(r => r.TargetId == c.TargetId)
.Where(r => r.UserId == this.User.UserId)
.FirstOrDefaultAsync();
if (reaction != null) c.YourThumb = reaction.Rating;
}
this.IsProfileUserHearted = await this.Database.HeartedProfiles.FirstOrDefaultAsync
(u => u.UserId == this.User.UserId && u.HeartedUserId == this.ProfileUser.UserId) !=
null;
this.IsProfileUserHearted = await this.Database.HeartedProfiles
.Where(h => h.HeartedUserId == this.ProfileUser.UserId)
.Where(h => h.UserId == this.User.UserId)
.AnyAsync();
return this.Page();
}

View file

@ -68,7 +68,6 @@ public class CleanupBrokenPhotosMaintenanceJob : IMaintenanceJob
}
LbpFile? file = LbpFile.FromHash(photo.LargeHash);
// Console.WriteLine(file.FileType, );
if (file == null || file.FileType != LbpFileType.Jpeg && file.FileType != LbpFileType.Png)
{
largeHashIsInvalidFile = true;

View file

@ -49,10 +49,12 @@ public static class RequestExtensions
private static readonly HttpClient client;
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
private static async Task<bool> verifyCaptcha(string token)
private static async Task<bool> verifyCaptcha(string? token)
{
if (!ServerConfiguration.Instance.Captcha.CaptchaEnabled) return true;
if (token == null) return false;
List<KeyValuePair<string, string>> payload = new()
{
new("secret", ServerConfiguration.Instance.Captcha.Secret),
@ -84,7 +86,7 @@ public static class RequestExtensions
bool gotCaptcha = request.Form.TryGetValue(keyName, out StringValues values);
if (!gotCaptcha) return false;
if (!await verifyCaptcha(values[0] ?? string.Empty)) return false;
if (!await verifyCaptcha(values[0])) return false;
}
return true;

View file

@ -72,6 +72,18 @@ canvas.hide-subjects {
pointer-events: none;
}
.horizontal-scroll::-webkit-scrollbar {
display: none;
}
.horizontal-scroll {
overflow-x: scroll;
overflow-y: hidden;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
width: 100%;
}
/*#region Cards*/
.card {