Fix community creator search and avatars
This commit is contained in:
parent
d3b1d01058
commit
863cf3e0ef
@ -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);
|
||||
}
|
||||
|
||||
@ -81,6 +81,7 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
AvatarUrl = avatarUrl,
|
||||
DisplayName = request.DisplayName,
|
||||
Slug = slug,
|
||||
IsPublic = true,
|
||||
},
|
||||
Roles = [ModelConstants.UserRole],
|
||||
PasswordHash = null,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -213,7 +213,7 @@
|
||||
bio: document.getElementById('bio').value.trim(),
|
||||
avatarUrl: currentAvatarUrl,
|
||||
specialities: null,
|
||||
isPublic: false
|
||||
isPublic: true
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user