Connect profile settings to user data
This commit is contained in:
parent
4f7e9bac68
commit
d3b1d01058
@ -3,6 +3,6 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace OnlyPrompt.Backend.ApiModels.UserProfile
|
||||
{
|
||||
public record ApiUpdateProfileRequest([MaxLength(100)] string? DisplayName, [MaxLength(100)][NoWhitespace] string? Slug, string? Bio, string? AvatarUrl, string? Specialities, bool IsPublic);
|
||||
public record ApiUpdateProfileRequest([MaxLength(100)] string? DisplayName, [MaxLength(100)][NoWhitespace] string? UserName, [MaxLength(100)][NoWhitespace] string? Slug, string? Bio, string? AvatarUrl, string? Specialities, bool IsPublic);
|
||||
public record ApiCreateReviewRequest(string? Comment, [Range(1, 5)] int Rating);
|
||||
}
|
||||
|
||||
@ -22,11 +22,41 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
{
|
||||
{ nameof(UserProfileModel.Slug), new[] { "Slug already exists." } }
|
||||
});
|
||||
private static ValidationProblem UserNameExistsProblem = TypedResults.ValidationProblem(new Dictionary<string, string[]>
|
||||
{
|
||||
{ nameof(UserModel.UserName), new[] { "Username is already taken." } }
|
||||
});
|
||||
|
||||
public ProfileController(OnlyPromptContext db, IMapper mapper) : base(db, mapper)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("self")]
|
||||
public async Task<Results<NotFound<string>, Ok<ApiUserProfile>>> GetSelfProfileAsync()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId is null)
|
||||
return TypedResults.NotFound("Profile not found.");
|
||||
|
||||
var profile = await _db.UserProfiles
|
||||
.Where(up => up.Id == userId.Value)
|
||||
.Select(up => new ApiUserProfile(
|
||||
up.DisplayName,
|
||||
up.Slug,
|
||||
up.Bio,
|
||||
up.AvatarUrl,
|
||||
up.Specialities,
|
||||
_db.Reviews.Where(r => r.Prompt.CreatorId == up.Id).Average(r => (double?)r.Rating) ?? 0.0,
|
||||
_db.Subscriptions.Count(s => s.SubscribedToId == up.Id)
|
||||
))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (profile is null)
|
||||
return TypedResults.NotFound("Profile not found.");
|
||||
|
||||
return TypedResults.Ok(profile);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<Results<NotFound<string>, Ok<ApiUserProfile>>> GetProfileAsync(Identifier id)
|
||||
{
|
||||
@ -81,6 +111,18 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
if (self is null)
|
||||
return TypedResults.NotFound("Profile not found.");
|
||||
|
||||
var user = await GetUserAsync();
|
||||
if (user is null)
|
||||
return TypedResults.NotFound("User not found.");
|
||||
|
||||
if (string.IsNullOrEmpty(request.UserName) == false)
|
||||
{
|
||||
if (await _db.Users.AnyAsync(u => u.UserName == request.UserName && u.Id != user.Id))
|
||||
return UserNameExistsProblem;
|
||||
|
||||
user.UserName = request.UserName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.Slug) == false)
|
||||
{
|
||||
if (await _db.UserProfiles.AnyAsync(up => up.Slug == request.Slug && up.Id != self.Id))
|
||||
@ -89,13 +131,13 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
self.Slug = request.Slug;
|
||||
}
|
||||
|
||||
if(string.IsNullOrEmpty(request.AvatarUrl) == false)
|
||||
if(request.AvatarUrl is not null)
|
||||
self.AvatarUrl = request.AvatarUrl;
|
||||
|
||||
if(string.IsNullOrEmpty(request.Bio) == false)
|
||||
if(request.Bio is not null)
|
||||
self.Bio = request.Bio;
|
||||
|
||||
if(string.IsNullOrEmpty(request.Specialities) == false)
|
||||
if(request.Specialities is not null)
|
||||
self.Specialities = request.Specialities;
|
||||
|
||||
if (string.IsNullOrEmpty(request.DisplayName) == false)
|
||||
@ -103,7 +145,15 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
|
||||
self.IsPublic = request.IsPublic;
|
||||
await _db.SaveChangesAsync();
|
||||
var result = _mapper.Map<ApiUserProfile>(self);
|
||||
var result = new ApiUserProfile(
|
||||
self.DisplayName,
|
||||
self.Slug,
|
||||
self.Bio,
|
||||
self.AvatarUrl,
|
||||
self.Specialities,
|
||||
await _db.Reviews.Where(r => r.Prompt.CreatorId == self.Id).AverageAsync(r => (double?)r.Rating) ?? 0.0,
|
||||
await _db.Subscriptions.CountAsync(s => s.SubscribedToId == self.Id)
|
||||
);
|
||||
return TypedResults.Ok(result);
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/chats.css">
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
@ -126,4 +127,4 @@
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/community.css">
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
@ -174,4 +175,4 @@
|
||||
loadCreators();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/create.css">
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
@ -178,4 +179,4 @@
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<link rel="stylesheet" href="../css/login.css" />
|
||||
<link rel="stylesheet" href="../css/topbar.css" />
|
||||
<link rel="stylesheet" href="../css/dashboard.css" />
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||
@ -25,7 +26,7 @@
|
||||
>
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex: 1; margin: 40px auto; max-width: 950px">
|
||||
<div style="flex: 1; display: flex; flex-direction: column">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="feed-main">
|
||||
|
||||
68
OnlyPrompt.Frontend/js/profile-shared.js
Normal file
68
OnlyPrompt.Frontend/js/profile-shared.js
Normal file
@ -0,0 +1,68 @@
|
||||
(function () {
|
||||
let currentProfilePromise = null;
|
||||
let currentProfile = null;
|
||||
|
||||
async function fetchJson(url) {
|
||||
const response = await fetch(url, { credentials: "same-origin" });
|
||||
if (response.status === 401) {
|
||||
location.href = "/login";
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) throw new Error(`${url} returned ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function loadCurrentProfile(force = false) {
|
||||
if (!force && currentProfilePromise) return currentProfilePromise;
|
||||
|
||||
currentProfilePromise = Promise.all([
|
||||
fetchJson("/api/v1/profiles/self"),
|
||||
fetchJson("/api/v1/auth/me"),
|
||||
]).then(([profile, user]) => {
|
||||
currentProfile = { ...profile, user };
|
||||
return currentProfile;
|
||||
});
|
||||
|
||||
return currentProfilePromise;
|
||||
}
|
||||
|
||||
function applyTopbarProfile(profile) {
|
||||
const avatar = document.getElementById("topbarAvatar");
|
||||
if (!avatar || !profile) return;
|
||||
|
||||
if (profile.avatarUrl) avatar.src = profile.avatarUrl;
|
||||
avatar.alt = profile.displayName || profile.user?.userName || "Profile";
|
||||
avatar.title = profile.displayName || profile.user?.userName || "Profile";
|
||||
}
|
||||
|
||||
async function hydrateTopbarProfile() {
|
||||
try {
|
||||
const profile = currentProfile || (await loadCurrentProfile());
|
||||
applyTopbarProfile(profile);
|
||||
} catch {
|
||||
// Keep the fallback avatar if the profile cannot be loaded.
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentProfile(profile) {
|
||||
currentProfile = profile;
|
||||
currentProfilePromise = Promise.resolve(profile);
|
||||
applyTopbarProfile(profile);
|
||||
}
|
||||
|
||||
window.loadCurrentProfile = loadCurrentProfile;
|
||||
window.hydrateTopbarProfile = hydrateTopbarProfile;
|
||||
window.setCurrentProfile = setCurrentProfile;
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (document.getElementById("topbarAvatar")) hydrateTopbarProfile();
|
||||
});
|
||||
|
||||
if (document.body) {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
}
|
||||
})();
|
||||
@ -12,6 +12,7 @@
|
||||
<link rel="stylesheet" href="../css/login.css" />
|
||||
<link rel="stylesheet" href="../css/topbar.css" />
|
||||
<link rel="stylesheet" href="../css/marketplace.css" />
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||
@ -65,7 +66,7 @@
|
||||
>
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex: 1; margin: 40px auto; max-width: 950px">
|
||||
<div style="flex: 1; display: flex; flex-direction: column">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="marketplace-main">
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
<link rel="stylesheet" href="../css/login.css" />
|
||||
<link rel="stylesheet" href="../css/topbar.css" />
|
||||
<link rel="stylesheet" href="../css/post-detail.css" />
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/profile.css">
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
@ -28,24 +29,27 @@
|
||||
|
||||
<section class="profile-header" style="display:flex;align-items:center;gap:32px;border-bottom:1px solid #e5e7eb;padding-bottom:24px;">
|
||||
|
||||
<img src="../images/content/cat.png" class="profile-avatar" style="width:110px;height:110px;border-radius:50%;object-fit:cover;">
|
||||
<img id="profileAvatar" src="../images/content/cat.png" class="profile-avatar" style="width:110px;height:110px;border-radius:50%;object-fit:cover;">
|
||||
|
||||
<div class="profile-info" style="flex:1;">
|
||||
<h1 style="font-size:2rem;font-weight:700;margin-bottom:4px;">Sunny the Cat</h1>
|
||||
<div style="color:#64748b;margin-bottom:8px;">
|
||||
@sunny_the_cat <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>
|
||||
<h1 id="profileDisplayName" style="font-size:2rem;font-weight:700;margin-bottom:4px;">Loading...</h1>
|
||||
<div id="profileSlug" style="color:#64748b;margin-bottom:8px;">
|
||||
@profile <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:8px;">
|
||||
🐾 Cat prompt creator | Nap enthusiast | AI Cat Content Curator<br>
|
||||
Chasing lasers and building purrfect prompts.
|
||||
<div id="profileBio" style="margin-bottom:8px;">
|
||||
Loading profile...
|
||||
</div>
|
||||
|
||||
<div style="color:#64748b;">Cat City, Dreamland 🐈</div>
|
||||
<div id="profileSpecialities" style="color:#64748b;"></div>
|
||||
<div id="profileStats" style="display:flex;gap:18px;color:#64748b;margin-top:12px;font-size:0.95rem;">
|
||||
<span><strong id="profileRating" style="color:#111827;">0.0</strong> rating</span>
|
||||
<span><strong id="profileSubscribers" style="color:#111827;">0</strong> subscribers</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<button class="login-button">Edit Profile</button>
|
||||
<button class="login-button" onclick="location.href='settings.html'">Edit Profile</button>
|
||||
<button class="login-button" style="background:#f3f4f6;color:#111;box-shadow:none;">Share Profile</button>
|
||||
</div>
|
||||
|
||||
@ -106,6 +110,35 @@
|
||||
fetch('/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
|
||||
const profileAvatar = document.getElementById('profileAvatar');
|
||||
const profileDisplayName = document.getElementById('profileDisplayName');
|
||||
const profileSlug = document.getElementById('profileSlug');
|
||||
const profileBio = document.getElementById('profileBio');
|
||||
const profileSpecialities = document.getElementById('profileSpecialities');
|
||||
const profileRating = document.getElementById('profileRating');
|
||||
const profileSubscribers = document.getElementById('profileSubscribers');
|
||||
|
||||
async function loadOwnProfile() {
|
||||
try {
|
||||
const profile = await window.loadCurrentProfile();
|
||||
profileDisplayName.textContent = profile.displayName || 'My Profile';
|
||||
profileSlug.innerHTML = `@${profile.user?.userName || profile.slug || 'profile'} <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>`;
|
||||
profileBio.textContent = profile.bio || 'No bio yet.';
|
||||
profileSpecialities.textContent = profile.specialities || 'No specialities added yet.';
|
||||
profileRating.textContent = Number(profile.averageRating || 0).toFixed(1);
|
||||
profileSubscribers.textContent = profile.subscribers || 0;
|
||||
|
||||
if (profile.avatarUrl) {
|
||||
profileAvatar.src = profile.avatarUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
profileDisplayName.textContent = 'Profile unavailable';
|
||||
profileBio.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
loadOwnProfile();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/settings.css">
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
@ -40,7 +41,7 @@
|
||||
|
||||
<!-- Tab Content: Profile -->
|
||||
<div id="profileTab" class="tab-content active">
|
||||
<form class="settings-form">
|
||||
<form class="settings-form" id="profileSettingsForm">
|
||||
<div class="form-group">
|
||||
<label for="avatar">Profile Picture</label>
|
||||
<div class="avatar-upload">
|
||||
@ -51,11 +52,11 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="displayName">Display Name</label>
|
||||
<input type="text" id="displayName" placeholder="Sunny the Cat">
|
||||
<input type="text" id="displayName" placeholder="Display name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" placeholder="@username">
|
||||
<input type="text" id="username" placeholder="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="bio">Bio</label>
|
||||
@ -63,10 +64,11 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" placeholder="your@email.com">
|
||||
<input type="email" id="email" placeholder="your@email.com" readonly>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="save-btn">Save Changes</button>
|
||||
<p id="profileSaveStatus" style="margin-top:10px;color:#64748b;text-align:center;"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -154,7 +156,29 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Avatar preview (simple)
|
||||
let currentAvatarUrl = '';
|
||||
let currentSlug = '';
|
||||
|
||||
function setProfileForm(profile) {
|
||||
document.getElementById('displayName').value = profile.displayName || '';
|
||||
document.getElementById('username').value = profile.user?.userName || '';
|
||||
document.getElementById('email').value = profile.user?.email || '';
|
||||
document.getElementById('bio').value = profile.bio || '';
|
||||
currentAvatarUrl = profile.avatarUrl || '../images/content/cat.png';
|
||||
currentSlug = profile.slug || profile.user?.userName || '';
|
||||
avatarPreview.src = currentAvatarUrl;
|
||||
}
|
||||
|
||||
async function loadProfileSettings() {
|
||||
try {
|
||||
const profile = await window.loadCurrentProfile();
|
||||
setProfileForm(profile);
|
||||
} catch (error) {
|
||||
document.getElementById('profileSaveStatus').textContent = 'Profile could not be loaded.';
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar preview and save as data URL
|
||||
const avatarUpload = document.getElementById('avatarUpload');
|
||||
const avatarPreview = document.getElementById('avatarPreview');
|
||||
if (avatarUpload) {
|
||||
@ -163,15 +187,53 @@
|
||||
if (file && (file.type === 'image/png' || file.type === 'image/jpeg')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(ev) {
|
||||
avatarPreview.src = ev.target.result;
|
||||
currentAvatarUrl = ev.target.result;
|
||||
avatarPreview.src = currentAvatarUrl;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Demo form submits for all forms
|
||||
document.querySelectorAll('.settings-form').forEach(form => {
|
||||
document.getElementById('profileSettingsForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const status = document.getElementById('profileSaveStatus');
|
||||
status.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const userName = document.getElementById('username').value.trim();
|
||||
const response = await fetch('/api/v1/profiles', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
displayName: document.getElementById('displayName').value.trim(),
|
||||
userName,
|
||||
slug: userName || currentSlug,
|
||||
bio: document.getElementById('bio').value.trim(),
|
||||
avatarUrl: currentAvatarUrl,
|
||||
specialities: null,
|
||||
isPublic: false
|
||||
})
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
location.href = '/login';
|
||||
return;
|
||||
}
|
||||
if (!response.ok) throw new Error('Profile could not be saved.');
|
||||
|
||||
const savedProfile = await window.loadCurrentProfile(true);
|
||||
window.setCurrentProfile(savedProfile);
|
||||
setProfileForm(savedProfile);
|
||||
status.textContent = 'Saved.';
|
||||
} catch (error) {
|
||||
status.textContent = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
// Demo form submits for forms that are not connected yet
|
||||
document.querySelectorAll('.settings-form:not(#profileSettingsForm)').forEach(form => {
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
alert('Settings saved (demo)');
|
||||
@ -192,6 +254,8 @@
|
||||
fetch('/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
|
||||
loadProfileSettings();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -45,6 +45,11 @@
|
||||
<input type="text" id="displayName" name="displayName" placeholder="Enter your display name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="userName">Username</label>
|
||||
<input type="text" id="userName" name="userName" placeholder="Choose a username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<!-- Password field with button to show/hide password -->
|
||||
@ -77,4 +82,4 @@
|
||||
<script type="module" src="js/signup.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
<!-- Profile avatar on the right (must be changed with backend) -->
|
||||
<button class="topbar-avatar-btn" aria-label="Profile">
|
||||
<img src="../images/content/cat.png" alt="Profile Picture" class="topbar-avatar">
|
||||
<img id="topbarAvatar" src="../images/content/cat.png" alt="Profile Picture" class="topbar-avatar">
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user