- ModalFrame shared component replaces 8+ inline modal implementations (AdminUsers ×3, User ×2, CosplayerDetail, AlbumForm, PersonForm, AssetPicker, ProfileCropper) — all now use <ModalFrame> with Title/Body/Footer parameters - ModalFrame.razor.css houses modal styles once — deleted from AdminUsers.razor.css, User.razor.css, AssetPicker.razor.css - ModalFrame handles body scroll lock/unlock via JS interop - EmptyState component replaces 7 empty-state/not-found patterns - Removed duplicate .sortable class from AdminUsers.razor.css and Jobs.razor.css (now in _custom.scss as shared utility) - AGENTS.md: fix orphaned SCSS ref, add SCSS docs - .gitignore: ignore *.css.map
210 lines
7.6 KiB
Plaintext
210 lines
7.6 KiB
Plaintext
@page "/User/{UserId:guid}"
|
|
@using Butter.Dtos.User
|
|
@using Butter.Types
|
|
@using MilkStream.Client.Services
|
|
@inject LoginService LoginService
|
|
@inject UserService UserService
|
|
|
|
<div class="d-flex justify-content-center">
|
|
<div class="card mb-3" style="max-width: 600px; width: 100%;">
|
|
<div class="card-header d-flex align-items-center gap-3">
|
|
<div class="profile-avatar">@_initials</div>
|
|
<div class="min-width-0">
|
|
<h5 class="mb-0 text-truncate">@(user?.Username ?? "...")</h5>
|
|
@if (isSelf || AdminAccess) {
|
|
<small class="text-muted text-truncate d-block">@user?.Email</small>
|
|
}
|
|
</div>
|
|
<span class="badge @RoleBadge ms-auto">@(user?.AccessLevel.ToString() ?? "")</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<dl class="row mb-0 g-2">
|
|
@if (isSelf || AdminAccess) {
|
|
<dt class="col-sm-4 text-muted small">Email</dt>
|
|
<dd class="col-sm-8">@user?.Email</dd>
|
|
}
|
|
<dt class="col-sm-4 text-muted small">Role</dt>
|
|
<dd class="col-sm-8">@user?.AccessLevel.ToString()</dd>
|
|
<dt class="col-sm-4 text-muted small">Created</dt>
|
|
<dd class="col-sm-8">@user?.CreatedAt.ToString("D")</dd>
|
|
<dt class="col-sm-4 text-muted small">Last Login</dt>
|
|
<dd class="col-sm-8">@(user?.LastLogin?.ToString("D") ?? "Never")</dd>
|
|
<dt class="col-sm-4 text-muted small">Banned</dt>
|
|
<dd class="col-sm-8">@(user is {IsBanned: true} ? $"Yes ({user.BannedAt.ToString()})" : "No")</dd>
|
|
@if (AdminAccess) {
|
|
<dt class="col-sm-4 text-muted small">Deleted</dt>
|
|
<dd class="col-sm-8">@(user is not {DeletedAt: null} ? user!.DeletedAt.ToString() : "No")</dd>
|
|
}
|
|
</dl>
|
|
</div>
|
|
@if (canEdit) {
|
|
<div class="card-footer d-flex align-items-center gap-2 flex-wrap">
|
|
@if (!string.IsNullOrEmpty(_pageMessage)) {
|
|
<div class="alert alert-success py-1 mb-0 flex-grow-1">@_pageMessage</div>
|
|
}
|
|
<button class="btn btn-outline-primary btn-sm" @onclick="OpenEditModal">
|
|
<i class="bi bi-pencil"></i> Edit Profile
|
|
</button>
|
|
@if (isSelf) {
|
|
<button class="btn btn-outline-primary btn-sm" @onclick="() => _showPasswordModal = true">
|
|
<i class="bi bi-key"></i> Change Password
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<ModalFrame Title="Edit Profile" Show="_showEditModal" OnClose="CloseEditModal">
|
|
<HeaderActions><i class="bi bi-pencil me-1"></i></HeaderActions>
|
|
<Body>
|
|
@if (!string.IsNullOrEmpty(_editError)) {
|
|
<div class="alert alert-danger py-1">@_editError</div>
|
|
}
|
|
<div class="mb-3">
|
|
<label class="form-label">Username</label>
|
|
<input type="text" class="form-control" @bind="_editUsername" @bind:event="oninput" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Email</label>
|
|
<input type="email" class="form-control" @bind="_editEmail" @bind:event="oninput" />
|
|
</div>
|
|
</Body>
|
|
<Footer>
|
|
<button class="btn btn-secondary" @onclick="CloseEditModal">Cancel</button>
|
|
<button class="btn btn-primary" disabled="@(string.IsNullOrWhiteSpace(_editUsername) || string.IsNullOrWhiteSpace(_editEmail))" @onclick="SubmitEditProfile">
|
|
Save
|
|
</button>
|
|
</Footer>
|
|
</ModalFrame>
|
|
|
|
<ModalFrame Title="Change Password" Show="_showPasswordModal" OnClose="ClosePasswordModal">
|
|
<HeaderActions><i class="bi bi-key me-1"></i></HeaderActions>
|
|
<Body>
|
|
@if (!string.IsNullOrEmpty(_passwordError)) {
|
|
<div class="alert alert-danger py-1">@_passwordError</div>
|
|
}
|
|
|
|
<PasswordField @ref="_passwordField"
|
|
ShowOldPassword="true"
|
|
PasswordLabel="New Password"
|
|
ConfirmPasswordLabel="Confirm New Password"
|
|
@bind-OldPassword="_oldPassword"
|
|
@bind-Password="_newPassword"
|
|
@bind-ConfirmPassword="_confirmPassword" />
|
|
</Body>
|
|
<Footer>
|
|
<button class="btn btn-secondary" @onclick="ClosePasswordModal">Cancel</button>
|
|
<button class="btn btn-primary" disabled="@(!_canSubmit)" @onclick="SubmitPasswordChange">
|
|
Update Password
|
|
</button>
|
|
</Footer>
|
|
</ModalFrame>
|
|
|
|
@code {
|
|
|
|
[Parameter]
|
|
public Guid UserId { get; set; }
|
|
|
|
bool isSelf;
|
|
bool AdminAccess => LoginService.LoggedUser?.AccessLevel == EAccessLevel.Admin;
|
|
bool canEdit => isSelf || AdminAccess;
|
|
UserInfoDto? user;
|
|
|
|
string _initials => user?.Username is { Length: > 0 } u ? u[..1].ToUpper() : "?";
|
|
|
|
string RoleBadge => user?.AccessLevel switch {
|
|
EAccessLevel.Admin => "bg-danger",
|
|
EAccessLevel.Curator => "bg-warning text-dark",
|
|
_ => "bg-secondary"
|
|
};
|
|
|
|
bool _showEditModal;
|
|
bool _showPasswordModal;
|
|
string _editUsername = "";
|
|
string _editEmail = "";
|
|
string _editError = "";
|
|
string _pageMessage = "";
|
|
|
|
string _oldPassword = "";
|
|
string _newPassword = "";
|
|
string _confirmPassword = "";
|
|
string _passwordError = "";
|
|
|
|
PasswordField? _passwordField;
|
|
|
|
bool _canSubmit => _passwordField?.IsValid == true
|
|
&& !string.IsNullOrEmpty(_oldPassword)
|
|
&& !string.IsNullOrEmpty(_newPassword);
|
|
|
|
protected async override Task OnInitializedAsync() {
|
|
await base.OnInitializedAsync();
|
|
if (LoginService.IsLoggedIn) await LoadUser();
|
|
LoginService.AuthInfoChanged += async (_, _) => await LoadUser();
|
|
}
|
|
|
|
async Task LoadUser() {
|
|
if (UserId == LoginService.AuthInfo?.UserId) isSelf = true;
|
|
user = await UserService.GetUserAsync();
|
|
StateHasChanged();
|
|
}
|
|
|
|
void OpenEditModal() {
|
|
_editUsername = user?.Username ?? "";
|
|
_editEmail = user?.Email ?? "";
|
|
_editError = "";
|
|
_showEditModal = true;
|
|
}
|
|
|
|
async Task CloseEditModal() {
|
|
_showEditModal = false;
|
|
_editError = "";
|
|
}
|
|
|
|
async Task SubmitEditProfile() {
|
|
_editError = "";
|
|
|
|
var dto = new UserUpdateDto {
|
|
Id = UserId,
|
|
Username = _editUsername,
|
|
Email = _editEmail
|
|
};
|
|
|
|
var (result, error) = await UserService.UpdateUserAsync(dto);
|
|
if (result != null) {
|
|
_pageMessage = "Profile updated successfully.";
|
|
await CloseEditModal();
|
|
await LoadUser();
|
|
} else {
|
|
_editError = error ?? "Failed to update profile.";
|
|
}
|
|
}
|
|
|
|
async Task ClosePasswordModal() {
|
|
_showPasswordModal = false;
|
|
_oldPassword = "";
|
|
_newPassword = "";
|
|
_confirmPassword = "";
|
|
_passwordError = "";
|
|
}
|
|
|
|
async Task SubmitPasswordChange() {
|
|
_passwordError = "";
|
|
|
|
var dto = new UserUpdateDto {
|
|
Id = UserId,
|
|
OldPassword = _oldPassword,
|
|
Password = _newPassword
|
|
};
|
|
|
|
var (result, error) = await UserService.UpdateUserAsync(dto);
|
|
if (result != null) {
|
|
_pageMessage = "Password updated successfully.";
|
|
await ClosePasswordModal();
|
|
} else {
|
|
_passwordError = error ?? "Failed to update password.";
|
|
}
|
|
}
|
|
|
|
}
|