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