2026-06-02 10:36:02 +02:00

487 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">
<div id="sidebar-container"></div>
<div class="page-body">
<div id="topbar-container"></div>
<main class="login-card profile-main">
<section class="profile-header">
<img
id="profileAvatar"
src="../images/content/cat.png"
class="profile-avatar"
/>
<div class="profile-info">
<h1 id="profileDisplayName">Loading...</h1>
<div id="profileSlug">
@profile
<i class="bi bi-patch-check-fill profile-badge-icon"></i>
</div>
<div id="profileBio">Loading profile...</div>
<div id="profileSpecialities"></div>
<div id="profileStats">
<span><strong id="profileRating">0.0</strong> rating</span>
<span
><strong id="profileSubscribers">0</strong> subscribers</span
>
</div>
</div>
<div id="profileActions">
<button
id="primaryProfileButton"
class="login-button"
onclick="location.href = 'settings.html'"
>
Edit Profile
</button>
<button id="shareProfileButton" class="login-button">
Share Profile
</button>
</div>
</section>
<nav class="profile-tabs">
<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">
<div class="profile-grid-loading">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 profile-badge-icon"></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 profile-badge-icon"></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}'" class="profile-prompt-card">
<img src="${image}" alt="${prompt.title}" class="profile-prompt-img">
<div class="profile-prompt-body">
<div class="profile-prompt-title">${prompt.title}</div>
<div class="profile-prompt-desc">${prompt.description || "No description yet."}</div>
<div class="profile-prompt-meta">
<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}'" class="profile-prompt-edit-btn">Edit</button>` : ""}
</div>
</div>
</div>`;
}
function renderPromptList(prompts, emptyText, options = {}) {
if (!prompts.length) {
profilePromptsGrid.innerHTML = `<div class="profile-grid-empty">${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 class="profile-grid-error">${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>