399 lines
16 KiB
HTML

<!-- OnlyPrompt - Profile page:
- User profile display with avatar, bio, stats, and prompt cards (personal prompts) -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OnlyPrompt - Profile</title>
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/base.css">
<link rel="stylesheet" href="../css/sidebar.css">
<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>
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
<div id="sidebar-container"></div>
<div style="flex:1; margin:40px auto; max-width:950px;">
<div id="topbar-container"></div>
<main class="login-card profile-main" style="background:#fff;border-radius:18px;box-shadow:0 2px 8px rgba(59,130,246,0.06);padding:24px;">
<section class="profile-header" style="display:flex;align-items:center;gap:32px;border-bottom:1px solid #e5e7eb;padding-bottom:24px;">
<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 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 id="profileBio" style="margin-bottom:8px;">
Loading profile...
</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 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>
<nav class="profile-tabs" style="display:flex;gap:24px;border-bottom:2px solid #e5e7eb;margin:32px 0 18px 0;flex-wrap:wrap;">
<button type="button" class="profile-tab active" data-tab="mine" id="myPromptsTab">My Prompts</button>
<button type="button" class="profile-tab" data-tab="favorites" id="favoritesTab">Favorites</button>
<button type="button" class="profile-tab" data-tab="saved" id="savedTab">Saved</button>
</nav>
<section id="profile-prompts-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:24px;">
<div style="grid-column:1/-1;color:#64748b;text-align:center;padding:28px;">Loading prompts...</div>
</section>
</main>
</div>
</div>
<script>
fetch('/sidebar.html')
.then(r => r.text())
.then(data => {
document.getElementById('sidebar-container').innerHTML = data;
// Remove 'active' from all sidebar links
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
link.classList.remove('active');
});
// Then set 'active' only on the My Profile link
const profileLink = document.querySelector('#sidebar-container a[href="profile.html"]');
if (profileLink) profileLink.classList.add('active');
});
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');
const profilePromptsGrid = document.getElementById('profile-prompts-grid');
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;
try {
const creators = await fetchJson('/api/v1/profiles?limit=100');
const creator = creators.find((item) => item.userId?.toLowerCase() === profileId.toLowerCase());
if (!creator) return;
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) {
if (isOwnProfile) {
profileDisplayName.textContent = 'Profile unavailable';
profileBio.textContent = error.message;
} else {
profileDisplayName.textContent = 'Loading creator...';
profileBio.textContent = '';
}
}
}
function isPromptMarked(type, id) {
if (type === 'liked') {
const prompt = allPrompts.find((item) => item.id === id);
return prompt?.isLiked === true;
}
if (type === 'saved') {
const prompt = allPrompts.find((item) => item.id === id);
return prompt?.isSaved === true;
}
return false;
}
function renderProfilePrompt(prompt, options = {}) {
const image = prompt.exampleImageUrl || '../images/content/post1.png';
const showEdit = options.showEdit === true;
const rating = prompt.averageRating == null ? 'No ratings' : prompt.averageRating.toFixed(1);
return `
<div onclick="location.href='/post-detail?id=${prompt.id}'" style="background:#fff;border-radius:18px;box-shadow:0 2px 8px rgba(59,130,246,0.06);padding:18px;display:flex;gap:16px;cursor:pointer;">
<img src="${image}" alt="${prompt.title}" style="width:72px;height:72px;border-radius:12px;object-fit:cover;">
<div style="flex:1;min-width:0;">
<div style="font-weight:700;">${prompt.title}</div>
<div style="color:#64748b;margin-bottom:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${prompt.description || 'No description yet.'}</div>
<div style="display:flex;gap:16px;color:#64748b;align-items:center;flex-wrap:wrap;">
<span><i class="bi bi-star"></i> ${rating}</span>
${prompt.creatorName ? `<span>@${prompt.creatorName}</span>` : ''}
${showEdit ? `<button onclick="event.stopPropagation(); location.href='/create?id=${prompt.id}'" style="border:none;background:#f1f5f9;color:#334155;border-radius:10px;padding:6px 10px;font-weight:700;cursor:pointer;">Edit</button>` : ''}
</div>
</div>
</div>`;
}
function renderPromptList(prompts, emptyText, options = {}) {
if (!prompts.length) {
profilePromptsGrid.innerHTML = `<div style="grid-column:1/-1;color:#64748b;text-align:center;padding:28px;">${emptyText}</div>`;
return;
}
profilePromptsGrid.innerHTML = prompts.map((prompt) => renderProfilePrompt(prompt, options)).join('');
}
function updateTabs() {
document.querySelectorAll('.profile-tab').forEach((tab) => {
tab.classList.toggle('active', tab.dataset.tab === activeProfileTab);
});
const liked = allPrompts.filter((prompt) => isPromptMarked('liked', prompt.id));
const saved = allPrompts.filter((prompt) => isPromptMarked('saved', prompt.id));
myPromptsTab.textContent = `My Prompts (${ownPrompts.length})`;
favoritesTab.textContent = `Favorites (${liked.length})`;
savedTab.textContent = `Saved (${saved.length})`;
if (activeProfileTab === 'favorites') {
renderPromptList(liked, 'No liked prompts yet.');
} else if (activeProfileTab === 'saved') {
renderPromptList(saved, 'No saved prompts yet.');
} else {
renderPromptList(ownPrompts, 'No prompts yet.', { showEdit: true });
}
}
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');
if (response.status === 401) {
location.href = '/login';
return;
}
if (!response.ok) throw new Error('Prompts could not be loaded.');
ownPrompts = await response.json();
updateTabs();
} catch (error) {
profilePromptsGrid.innerHTML = `<div style="grid-column:1/-1;color:#ef4444;text-align:center;padding:28px;">${error.message}</div>`;
}
}
async function loadAllPromptReferences() {
try {
const response = await fetch('/api/v1/prompts?limit=100');
if (!response.ok) return;
allPrompts = await response.json();
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.
}
}
document.querySelectorAll('.profile-tab').forEach((tab) => {
tab.addEventListener('click', () => {
if (!isOwnProfile) {
updateProfileMode();
return;
}
activeProfileTab = tab.dataset.tab;
updateTabs();
});
});
(async function initProfilePage() {
await loadProfile();
await loadCreatorCardFallback();
await loadFollowState();
updateProfileMode();
if (isOwnProfile) {
loadOwnPrompts();
}
loadAllPromptReferences();
})();
</script>
</body>
</html>