227 lines
10 KiB
HTML
227 lines
10 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 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>
|
|
|
|
</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');
|
|
let ownPrompts = [];
|
|
let allPrompts = [];
|
|
let activeProfileTab = 'mine';
|
|
|
|
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;
|
|
|
|
if (profile.avatarUrl) {
|
|
profileAvatar.src = profile.avatarUrl;
|
|
}
|
|
} catch (error) {
|
|
profileDisplayName.textContent = 'Profile unavailable';
|
|
profileBio.textContent = error.message;
|
|
}
|
|
}
|
|
|
|
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 });
|
|
}
|
|
}
|
|
|
|
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();
|
|
updateTabs();
|
|
} catch {
|
|
// Favorites and saved stay empty if prompts cannot be loaded.
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('.profile-tab').forEach((tab) => {
|
|
tab.addEventListener('click', () => {
|
|
activeProfileTab = tab.dataset.tab;
|
|
updateTabs();
|
|
});
|
|
});
|
|
|
|
loadOwnProfile();
|
|
loadOwnPrompts();
|
|
loadAllPromptReferences();
|
|
</script>
|
|
</body>
|
|
</html>
|