399 lines
16 KiB
HTML
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>
|