Connect feed posts to creator profiles
This commit is contained in:
parent
58d0c8a15a
commit
6ea5a386cf
@ -62,7 +62,6 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var profile = await _db.UserProfiles.OfIdentifer(id)
|
||||
.Where(up => up.IsPublic || up.Id == userId)
|
||||
.ProjectTo<ApiUserProfile>(_mapper.ConfigurationProvider)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
return TypedResults.NotFound($"No subscription tier found for user {subscribeToId} with level {level.Value}");
|
||||
|
||||
var existingSubscription = await _db.Subscriptions.FirstOrDefaultAsync(
|
||||
sub => subscribeToId.Id.HasValue ? sub.SubscribedToId == subscribeToId.Id.Value : sub.SubscribedTo.Profile.Slug == subscribeToId.Slug
|
||||
sub => (subscribeToId.Id.HasValue ? sub.SubscribedToId == subscribeToId.Id.Value : sub.SubscribedTo.Profile.Slug == subscribeToId.Slug)
|
||||
&& sub.SubscriberId == userId
|
||||
);
|
||||
|
||||
@ -91,7 +91,7 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
var userId = User.GetUserId();
|
||||
var subscription = await _db.Subscriptions
|
||||
.Where(
|
||||
sub => subscribeToId.Id.HasValue ? sub.SubscribedToId == subscribeToId.Id.Value : sub.SubscribedTo.Profile.Slug == subscribeToId.Slug
|
||||
sub => (subscribeToId.Id.HasValue ? sub.SubscribedToId == subscribeToId.Id.Value : sub.SubscribedTo.Profile.Slug == subscribeToId.Slug)
|
||||
&& sub.SubscriberId == userId
|
||||
)
|
||||
.ProjectTo<ApiSubscription>(_mapper.ConfigurationProvider)
|
||||
@ -106,7 +106,7 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
var userId = User.GetUserId();
|
||||
var count = await _db.Subscriptions
|
||||
.Where(
|
||||
sub => subscribeToId.Id.HasValue ? sub.SubscribedToId == subscribeToId.Id.Value : sub.SubscribedTo.Profile.Slug == subscribeToId.Slug
|
||||
sub => (subscribeToId.Id.HasValue ? sub.SubscribedToId == subscribeToId.Id.Value : sub.SubscribedTo.Profile.Slug == subscribeToId.Slug)
|
||||
&& sub.SubscriberId == userId
|
||||
)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
@ -151,12 +151,17 @@
|
||||
function feedImg(id) {
|
||||
return `/images/content/feed${(parseInt(id.slice(-1), 16) % 4) + 1}.png`;
|
||||
}
|
||||
|
||||
function profileUrl(userId) {
|
||||
return `/profile.html?id=${encodeURIComponent(userId)}`;
|
||||
}
|
||||
|
||||
function renderCard(prompt) {
|
||||
const locked = !prompt.canAccess;
|
||||
const liked = prompt.isLiked;
|
||||
const saved = prompt.isSaved;
|
||||
return `
|
||||
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='/post-detail?id=${prompt.id}'">
|
||||
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='${profileUrl(prompt.creatorId)}'">
|
||||
<div class="post-header">
|
||||
<img class="post-avatar" src="${prompt.creatorAvatarUrl || '../images/content/cat.png'}" alt="${prompt.creatorName}">
|
||||
<div class="post-author">
|
||||
|
||||
@ -48,9 +48,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<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 id="profileActions" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<button id="primaryProfileButton" class="login-button" onclick="location.href='settings.html'">Edit Profile</button>
|
||||
<button id="shareProfileButton" class="login-button" style="background:#f3f4f6;color:#111;box-shadow:none;">Share Profile</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@ -98,26 +98,103 @@
|
||||
const myPromptsTab = document.getElementById('myPromptsTab');
|
||||
const favoritesTab = document.getElementById('favoritesTab');
|
||||
const savedTab = document.getElementById('savedTab');
|
||||
const profileActions = document.getElementById('profileActions');
|
||||
const primaryProfileButton = document.getElementById('primaryProfileButton');
|
||||
const shareProfileButton = document.getElementById('shareProfileButton');
|
||||
const profileTabs = document.querySelector('.profile-tabs');
|
||||
const params = new URLSearchParams(location.search);
|
||||
const profileId = params.get('id');
|
||||
let ownPrompts = [];
|
||||
let allPrompts = [];
|
||||
let profilePrompts = [];
|
||||
let activeProfileTab = 'mine';
|
||||
let currentUserId = null;
|
||||
let isOwnProfile = !profileId;
|
||||
let profileLoaded = false;
|
||||
let currentIsFollowing = false;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function renderProfile(profile, fallbackName = 'Profile') {
|
||||
profileDisplayName.textContent = profile.displayName || fallbackName;
|
||||
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;
|
||||
}
|
||||
profileLoaded = true;
|
||||
}
|
||||
|
||||
function renderProfileFromPrompt(prompt) {
|
||||
if (!prompt || profileLoaded) return;
|
||||
|
||||
profileDisplayName.textContent = prompt.creatorName || 'Creator Profile';
|
||||
profileSlug.innerHTML = `@${prompt.creatorName || 'creator'} <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>`;
|
||||
profileBio.textContent = 'No bio yet.';
|
||||
profileSpecialities.textContent = '';
|
||||
profileRating.textContent = Number(prompt.averageRating || 0).toFixed(1);
|
||||
profileSubscribers.textContent = 0;
|
||||
|
||||
if (prompt.creatorAvatarUrl) {
|
||||
profileAvatar.src = prompt.creatorAvatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCreatorCardFallback() {
|
||||
if (isOwnProfile || profileLoaded || !profileId) return;
|
||||
|
||||
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;
|
||||
const creators = await fetchJson('/api/v1/profiles?limit=100');
|
||||
const creator = creators.find((item) => item.userId?.toLowerCase() === profileId.toLowerCase());
|
||||
if (!creator) return;
|
||||
|
||||
if (profile.avatarUrl) {
|
||||
profileAvatar.src = profile.avatarUrl;
|
||||
renderProfile({
|
||||
displayName: creator.displayName,
|
||||
slug: creator.slug,
|
||||
bio: creator.bio,
|
||||
avatarUrl: creator.avatarUrl,
|
||||
specialities: null,
|
||||
averageRating: creator.averageRating,
|
||||
subscribers: creator.subscribers
|
||||
}, 'Creator Profile');
|
||||
} catch {
|
||||
// Prompt data below still provides a minimal fallback if creator cards fail.
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const currentProfile = await window.loadCurrentProfile();
|
||||
currentUserId = currentProfile.user?.id;
|
||||
isOwnProfile = !profileId || profileId.toLowerCase() === currentUserId?.toLowerCase();
|
||||
|
||||
if (isOwnProfile) {
|
||||
renderProfile(currentProfile, 'My Profile');
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await fetchJson(`/api/v1/profiles/${encodeURIComponent(profileId)}`);
|
||||
renderProfile(profile, 'Creator Profile');
|
||||
} catch (error) {
|
||||
profileDisplayName.textContent = 'Profile unavailable';
|
||||
profileBio.textContent = error.message;
|
||||
if (isOwnProfile) {
|
||||
profileDisplayName.textContent = 'Profile unavailable';
|
||||
profileBio.textContent = error.message;
|
||||
} else {
|
||||
profileDisplayName.textContent = 'Loading creator...';
|
||||
profileBio.textContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,6 +261,84 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateProfileMode() {
|
||||
if (isOwnProfile) {
|
||||
profileActions.style.display = 'flex';
|
||||
primaryProfileButton.textContent = 'Edit Profile';
|
||||
primaryProfileButton.disabled = false;
|
||||
primaryProfileButton.onclick = () => location.href = 'settings.html';
|
||||
profileTabs.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
|
||||
profileActions.style.display = 'flex';
|
||||
primaryProfileButton.textContent = currentIsFollowing ? 'Following' : 'Follow';
|
||||
primaryProfileButton.disabled = false;
|
||||
primaryProfileButton.onclick = toggleProfileFollow;
|
||||
favoritesTab.style.display = 'none';
|
||||
savedTab.style.display = 'none';
|
||||
myPromptsTab.textContent = `Prompts (${profilePrompts.length})`;
|
||||
renderPromptList(profilePrompts, 'No prompts yet.');
|
||||
}
|
||||
|
||||
async function loadFollowState() {
|
||||
if (isOwnProfile || !profileId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/subscriptions/${encodeURIComponent(profileId)}`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (response.status === 401) {
|
||||
location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = response.ok ? await response.json() : null;
|
||||
currentIsFollowing = subscription !== null;
|
||||
updateProfileMode();
|
||||
} catch {
|
||||
currentIsFollowing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleProfileFollow() {
|
||||
if (!profileId) return;
|
||||
|
||||
primaryProfileButton.disabled = true;
|
||||
const response = await fetch(`/api/v1/subscriptions/${encodeURIComponent(profileId)}`, {
|
||||
method: currentIsFollowing ? 'DELETE' : 'PUT',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const currentSubscribers = Number(profileSubscribers.textContent || 0);
|
||||
currentIsFollowing = !currentIsFollowing;
|
||||
profileSubscribers.textContent = Math.max(0, currentSubscribers + (currentIsFollowing ? 1 : -1));
|
||||
updateProfileMode();
|
||||
} else {
|
||||
primaryProfileButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
shareProfileButton.addEventListener('click', async () => {
|
||||
const url = isOwnProfile
|
||||
? `${location.origin}/profile.html`
|
||||
: `${location.origin}/profile.html?id=${encodeURIComponent(profileId)}`;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
shareProfileButton.textContent = 'Copied';
|
||||
setTimeout(() => shareProfileButton.textContent = 'Share Profile', 1200);
|
||||
} catch {
|
||||
location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadOwnPrompts() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/prompts/mine');
|
||||
@ -205,7 +360,13 @@
|
||||
const response = await fetch('/api/v1/prompts?limit=100');
|
||||
if (!response.ok) return;
|
||||
allPrompts = await response.json();
|
||||
updateTabs();
|
||||
if (isOwnProfile) {
|
||||
updateTabs();
|
||||
} else {
|
||||
profilePrompts = allPrompts.filter((prompt) => prompt.creatorId?.toLowerCase() === profileId.toLowerCase());
|
||||
renderProfileFromPrompt(profilePrompts[0]);
|
||||
updateProfileMode();
|
||||
}
|
||||
} catch {
|
||||
// Favorites and saved stay empty if prompts cannot be loaded.
|
||||
}
|
||||
@ -213,14 +374,25 @@
|
||||
|
||||
document.querySelectorAll('.profile-tab').forEach((tab) => {
|
||||
tab.addEventListener('click', () => {
|
||||
if (!isOwnProfile) {
|
||||
updateProfileMode();
|
||||
return;
|
||||
}
|
||||
activeProfileTab = tab.dataset.tab;
|
||||
updateTabs();
|
||||
});
|
||||
});
|
||||
|
||||
loadOwnProfile();
|
||||
loadOwnPrompts();
|
||||
loadAllPromptReferences();
|
||||
(async function initProfilePage() {
|
||||
await loadProfile();
|
||||
await loadCreatorCardFallback();
|
||||
await loadFollowState();
|
||||
updateProfileMode();
|
||||
if (isOwnProfile) {
|
||||
loadOwnPrompts();
|
||||
}
|
||||
loadAllPromptReferences();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user