Fix community creator search and avatars

This commit is contained in:
Isabelle Nachbaur 2026-05-30 16:08:40 +02:00
parent d3b1d01058
commit 863cf3e0ef
8 changed files with 84 additions and 16 deletions

View File

@ -1,5 +1,5 @@
namespace OnlyPrompt.Backend.ApiModels.UserProfile
{
public record ApiUserProfile(string DisplayName, string Slug, string? Bio, string AvatarUrl, string? Specialities, double AverageRating, int Subscribers);
public record ApiCreatorCard(Guid UserId, string DisplayName, string Slug, string? Bio, double AverageRating, int Subscribers, int PromptCount, bool IsFollowing);
public record ApiCreatorCard(Guid UserId, string DisplayName, string Slug, string? Bio, string AvatarUrl, double AverageRating, int Subscribers, int PromptCount, bool IsFollowing);
}

View File

@ -81,6 +81,7 @@ namespace OnlyPrompt.Backend.Controllers
AvatarUrl = avatarUrl,
DisplayName = request.DisplayName,
Slug = slug,
IsPublic = true,
},
Roles = [ModelConstants.UserRole],
PasswordHash = null,

View File

@ -76,17 +76,25 @@ namespace OnlyPrompt.Backend.Controllers
public async Task<ApiCreatorCard[]> GetCreatorsAsync(
[Range(0, int.MaxValue)] int offset = 0,
[Range(1, 100)] int limit = 20,
[FromQuery] string sort = "popular"
[FromQuery] string sort = "popular",
[FromQuery] string? search = null
)
{
var userId = User.GetUserId();
var query = _db.UserProfiles.Where(up => up.IsPublic && up.Id != userId);
var query = _db.UserProfiles.Where(up => up.Id != userId);
if (string.IsNullOrWhiteSpace(search) == false)
query = query.Where(up =>
up.DisplayName.Contains(search) ||
up.Slug.Contains(search) ||
up.User.UserName.Contains(search) ||
(up.Bio != null && up.Bio.Contains(search)));
var projected = query.Select(up => new ApiCreatorCard(
up.Id,
up.DisplayName,
up.Slug,
up.Bio,
up.AvatarUrl,
_db.Reviews.Where(r => r.Prompt.CreatorId == up.Id).Average(r => (double?)r.Rating) ?? 0.0,
_db.Subscriptions.Count(s => s.SubscribedToId == up.Id),
_db.Prompts.Count(p => p.CreatorId == up.Id),

View File

@ -24,6 +24,6 @@ namespace OnlyPrompt.Backend.Database.Models
public string? Specialities { get; set; }
public virtual UserModel User { get; set; }
public bool IsPublic { get; set; } = false;
public bool IsPublic { get; set; } = true;
}
}

View File

@ -43,8 +43,8 @@
<div id="creators-empty" style="display:none; text-align:center; padding:60px 20px; color:#64748b;">
<i class="bi bi-people" style="font-size:3rem; display:block; margin-bottom:16px;"></i>
<h3 style="margin-bottom:8px;">No creators found</h3>
<p>Check back later for new creators to follow.</p>
<h3 id="creators-empty-title" style="margin-bottom:8px;">No creators found</h3>
<p id="creators-empty-text">Check back later for new creators to follow.</p>
</div>
<div id="creators-error" style="display:none; text-align:center; padding:60px 20px; color:#ef4444;">
@ -72,10 +72,6 @@
.then(data => document.getElementById('topbar-container').innerHTML = data);
// ── Helpers ──────────────────────────────────────────────────────
function creatorImg(id) {
return `/images/content/creator${(parseInt(id.slice(-1), 16) % 6) + 1}.png`;
}
function renderStars(rating) {
if (!rating) return '';
const stars = Math.round(rating);
@ -86,7 +82,7 @@
return `
<div class="creator-card">
<img class="creator-avatar"
src="${creatorImg(c.userId)}"
src="${c.avatarUrl || '../images/content/cat.png'}"
alt="${c.displayName}"
style="cursor:pointer"
onclick="location.href='/profile?id=${c.userId}'">
@ -136,21 +132,46 @@
// ── Load Creators ────────────────────────────────────────────────
const grid = document.getElementById('creators-grid');
const emptyEl = document.getElementById('creators-empty');
const emptyTitle = document.getElementById('creators-empty-title');
const emptyText = document.getElementById('creators-empty-text');
const errorEl = document.getElementById('creators-error');
const errorMsg = document.getElementById('creators-error-msg');
async function loadCreators(sort = 'popular') {
let activeSort = 'popular';
let currentSearch = new URLSearchParams(location.search).get('search') || '';
function getSearchTerm() {
return currentSearch.trim();
}
async function loadCreators(sort = activeSort) {
activeSort = sort;
grid.innerHTML = '';
emptyEl.style.display = 'none';
errorEl.style.display = 'none';
try {
const res = await fetch(`/api/v1/profiles?sort=${sort}&limit=50`);
const params = new URLSearchParams({
sort,
limit: '50'
});
const search = getSearchTerm();
if (search) params.set('search', search);
const res = await fetch(`/api/v1/profiles?${params}`);
if (res.status === 401) { location.href = '/login'; return; }
if (!res.ok) throw new Error(`Server error ${res.status}`);
const creators = await res.json();
if (creators.length === 0) { emptyEl.style.display = 'block'; return; }
if (creators.length === 0) {
const search = getSearchTerm();
emptyTitle.textContent = search ? 'No matching creators' : 'No creators found';
emptyText.textContent = search
? `No creator matches "${search}". Try another name or clear the search.`
: 'Create another local user to see creators here.';
emptyEl.style.display = 'block';
return;
}
grid.innerHTML = creators.map(renderCard).join('');
@ -172,6 +193,11 @@
});
});
window.applyCreatorSearch = (value) => {
currentSearch = value.trim();
loadCreators(activeSort);
};
loadCreators();
</script>
</body>

View File

@ -35,6 +35,38 @@
avatar.title = profile.displayName || profile.user?.userName || "Profile";
}
function wireTopbarSearch() {
const input = document.getElementById("topbarSearchInput");
if (!input || input.dataset.searchReady === "true") return;
input.dataset.searchReady = "true";
const params = new URLSearchParams(location.search);
if (location.pathname.includes("community") && params.has("search")) {
input.value = params.get("search") || "";
}
let searchTimeout;
input.addEventListener("input", () => {
if (!location.pathname.includes("community")) return;
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
if (typeof window.applyCreatorSearch === "function") {
window.applyCreatorSearch(input.value);
}
}, 300);
});
input.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
const search = input.value.trim();
location.href = search
? `/community?search=${encodeURIComponent(search)}`
: "/community";
});
}
async function hydrateTopbarProfile() {
try {
const profile = currentProfile || (await loadCurrentProfile());
@ -56,6 +88,7 @@
const observer = new MutationObserver(() => {
if (document.getElementById("topbarAvatar")) hydrateTopbarProfile();
wireTopbarSearch();
});
if (document.body) {

View File

@ -213,7 +213,7 @@
bio: document.getElementById('bio').value.trim(),
avatarUrl: currentAvatarUrl,
specialities: null,
isPublic: false
isPublic: true
})
});

View File

@ -8,7 +8,7 @@
<header class="topbar-shell">
<div class="topbar-search">
<i class="bi bi-search"></i>
<input type="text" placeholder="Search">
<input id="topbarSearchInput" type="search" placeholder="Search">
</div>
<div class="topbar-actions">