inline css entfernen

This commit is contained in:
GeNii96 2026-06-02 10:36:02 +02:00
parent e6d54d693f
commit 7a347b093e
17 changed files with 1663 additions and 1155 deletions

View File

@ -1,130 +1,175 @@
<!-- OnlyPrompt - Chats page:
- Direct messaging interface with conversation list and active chat window -->
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OnlyPrompt - Chats</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/chats.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>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OnlyPrompt - Chats</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/chats.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 style="flex:1; display: flex; flex-direction: column;">
<div id="topbar-container"></div>
<div class="page-body">
<div id="topbar-container"></div>
<main class="chats-main">
<!-- Chat Container: Left column (list) + Right column (active chat) -->
<div class="chat-container">
<!-- Left Column: Chat Overview -->
<div class="chat-list">
<div class="chat-list-header">
<h2>Messages</h2>
<button class="new-chat-btn"><i class="bi bi-pencil-square"></i></button>
<main class="chats-main">
<!-- Chat Container: Left column (list) + Right column (active chat) -->
<div class="chat-container">
<!-- Left Column: Chat Overview -->
<div class="chat-list">
<div class="chat-list-header">
<h2>Messages</h2>
<button class="new-chat-btn">
<i class="bi bi-pencil-square"></i>
</button>
</div>
<div class="chat-list-items">
<!-- Chat Entry 1 (active) -->
<div class="chat-item active">
<img
src="../images/content/creator2.png"
alt="Alex Chen"
class="chat-avatar"
/>
<div class="chat-item-info">
<div class="chat-name">Alex Chen</div>
<div class="chat-last-msg">
Hey Sarah! Really loved your last video on minimalism...
</div>
</div>
<div class="chat-time">10:17 AM</div>
</div>
<!-- Chat Entry 2 -->
<div class="chat-item">
<img
src="../images/content/creator3.png"
alt="Mia Wong"
class="chat-avatar"
/>
<div class="chat-item-info">
<div class="chat-name">Mia Wong</div>
<div class="chat-last-msg">
Thanks for the prompt tips! They worked perfectly.
</div>
</div>
<div class="chat-time">Yesterday</div>
</div>
<!-- Chat Entry 3 -->
<div class="chat-item">
<img
src="../images/content/creator4.png"
alt="Tom Rivera"
class="chat-avatar"
/>
<div class="chat-item-info">
<div class="chat-name">Tom Rivera</div>
<div class="chat-last-msg">
Let's schedule a call for the collab?
</div>
</div>
<div class="chat-time">Yesterday</div>
</div>
</div>
</div>
<div class="chat-list-items">
<!-- Chat Entry 1 (active) -->
<div class="chat-item active">
<img src="../images/content/creator2.png" alt="Alex Chen" class="chat-avatar">
<div class="chat-item-info">
<div class="chat-name">Alex Chen</div>
<div class="chat-last-msg">Hey Sarah! Really loved your last video on minimalism...</div>
<!-- Right Column: Active Chat (with Alex Chen) -->
<div class="chat-active">
<div class="chat-header">
<img
src="../images/content/creator2.png"
alt="Alex Chen"
class="chat-avatar-large"
/>
<div class="chat-header-info">
<div class="chat-header-name">Alex Chen</div>
<div class="chat-header-status">
<span class="online-dot"></span> Online
</div>
</div>
<div class="chat-time">10:17 AM</div>
</div>
<!-- Chat Entry 2 -->
<div class="chat-item">
<img src="../images/content/creator3.png" alt="Mia Wong" class="chat-avatar">
<div class="chat-item-info">
<div class="chat-name">Mia Wong</div>
<div class="chat-last-msg">Thanks for the prompt tips! They worked perfectly.</div>
<div class="chat-messages">
<!-- Message from Alex -->
<div class="message received">
<div class="message-bubble">
Hey Sarah! Really loved your last video on minimalism. Quick
question about your workspace layout?
</div>
<div class="message-time">10:15 AM</div>
</div>
<!-- Reply from Sarah -->
<div class="message sent">
<div class="message-bubble">
Thanks Alex! Appreciate it. Yes, happy to share! The desk is
from Article, and the shelving unit is custom-built. Highly
recommend a clean setup!
</div>
<div class="message-time">10:16 AM</div>
</div>
<!-- Alex replies -->
<div class="message received">
<div class="message-bubble">
Thanks so much! Your aesthetic is exactly what I'm aiming
for. Can't wait for your next piece!
</div>
<div class="message-time">10:17 AM</div>
</div>
<!-- Sarah replies -->
<div class="message sent">
<div class="message-bubble">
Awesome! Let me know if you need more tips. Enjoy the
process! 😊
</div>
<div class="message-time">10:18 AM</div>
</div>
<div class="chat-time">Yesterday</div>
</div>
<!-- Chat Entry 3 -->
<div class="chat-item">
<img src="../images/content/creator4.png" alt="Tom Rivera" class="chat-avatar">
<div class="chat-item-info">
<div class="chat-name">Tom Rivera</div>
<div class="chat-last-msg">Let's schedule a call for the collab?</div>
</div>
<div class="chat-time">Yesterday</div>
<div class="chat-input-area">
<input type="text" placeholder="Type your message..." />
<button class="send-btn">Send</button>
</div>
</div>
</div>
<!-- Right Column: Active Chat (with Alex Chen) -->
<div class="chat-active">
<div class="chat-header">
<img src="../images/content/creator2.png" alt="Alex Chen" class="chat-avatar-large">
<div class="chat-header-info">
<div class="chat-header-name">Alex Chen</div>
<div class="chat-header-status"><span class="online-dot"></span> Online</div>
</div>
</div>
<div class="chat-messages">
<!-- Message from Alex -->
<div class="message received">
<div class="message-bubble">Hey Sarah! Really loved your last video on minimalism. Quick question about your workspace layout?</div>
<div class="message-time">10:15 AM</div>
</div>
<!-- Reply from Sarah -->
<div class="message sent">
<div class="message-bubble">Thanks Alex! Appreciate it. Yes, happy to share! The desk is from Article, and the shelving unit is custom-built. Highly recommend a clean setup!</div>
<div class="message-time">10:16 AM</div>
</div>
<!-- Alex replies -->
<div class="message received">
<div class="message-bubble">Thanks so much! Your aesthetic is exactly what I'm aiming for. Can't wait for your next piece!</div>
<div class="message-time">10:17 AM</div>
</div>
<!-- Sarah replies -->
<div class="message sent">
<div class="message-bubble">Awesome! Let me know if you need more tips. Enjoy the process! 😊</div>
<div class="message-time">10:18 AM</div>
</div>
</div>
<div class="chat-input-area">
<input type="text" placeholder="Type your message...">
<button class="send-btn">Send</button>
</div>
</div>
</div>
</main>
</main>
</div>
</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');
<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");
});
// Set 'active' on the Chats link (4th link, index 3)
const chatsLink = document.querySelectorAll(
"#sidebar-container .sidebar li a",
)[3];
if (chatsLink) chatsLink.classList.add("active");
});
// Set 'active' on the Chats link (4th link, index 3)
const chatsLink = document.querySelectorAll('#sidebar-container .sidebar li a')[3];
if (chatsLink) chatsLink.classList.add('active');
});
fetch('/topbar.html')
.then(r => r.text())
.then(data => document.getElementById('topbar-container').innerHTML = data);
</script>
</body>
fetch("/topbar.html")
.then((r) => r.text())
.then(
(data) =>
(document.getElementById("topbar-container").innerHTML = data),
);
</script>
</body>
</html>

View File

@ -1,204 +1,225 @@
<!-- OnlyPrompt - Community page:
- Discover creators, follow/unfollow, dynamic via API -->
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OnlyPrompt - Discover Creators</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/community.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);">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OnlyPrompt - Discover Creators</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/community.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 id="sidebar-container"></div>
<div class="page-body">
<div id="topbar-container"></div>
<div style="flex:1; margin:40px auto; max-width:950px;">
<main class="creators-main">
<div class="creators-header">
<h1>Discover Creators</h1>
<p>Follow your favorite prompt artists and get inspired.</p>
</div>
<div id="topbar-container"></div>
<div class="filter-buttons">
<button class="filter-btn active" data-sort="popular">
Popular
</button>
<button class="filter-btn" data-sort="prompts">Rising</button>
<button class="filter-btn" data-sort="new">New</button>
<button class="filter-btn" data-sort="rating">Top Rated</button>
</div>
<main class="creators-main">
<div class="creators-grid" id="creators-grid"></div>
<div class="creators-header">
<h1>Discover Creators</h1>
<p>Follow your favorite prompt artists and get inspired.</p>
</div>
<div id="creators-empty" class="state-empty">
<i class="bi bi-people state-icon"></i>
<h3 id="creators-empty-title" class="state-title">
No creators found
</h3>
<p id="creators-empty-text">
Check back later for new creators to follow.
</p>
</div>
<div class="filter-buttons">
<button class="filter-btn active" data-sort="popular">Popular</button>
<button class="filter-btn" data-sort="prompts">Rising</button>
<button class="filter-btn" data-sort="new">New</button>
<button class="filter-btn" data-sort="rating">Top Rated</button>
</div>
<div class="creators-grid" id="creators-grid"></div>
<div id="creators-empty" style="display:none; text-align:center; padding:60px 20px; color:#64748b;">
<i class="bi bi-people" style="font-size:3rem; display:block; margin-bottom:16px;"></i>
<h3 id="creators-empty-title" style="margin-bottom:8px;">No creators found</h3>
<p id="creators-empty-text">Check back later for new creators to follow.</p>
</div>
<div id="creators-error" style="display:none; text-align:center; padding:60px 20px; color:#ef4444;">
<i class="bi bi-exclamation-circle" style="font-size:3rem; display:block; margin-bottom:16px;"></i>
<h3 style="margin-bottom:8px;">Could not load creators</h3>
<p id="creators-error-msg"></p>
</div>
</main>
<div id="creators-error" class="state-error">
<i class="bi bi-exclamation-circle state-icon"></i>
<h3 class="state-title">Could not load creators</h3>
<p id="creators-error-msg"></p>
</div>
</main>
</div>
</div>
</div>
<script type="module">
// ── Sidebar & Topbar ─────────────────────────────────────────────
fetch('/sidebar.html')
.then(r => r.text())
.then(data => {
document.getElementById('sidebar-container').innerHTML = data;
document.querySelectorAll('#sidebar-container .sidebar a').forEach(l => l.classList.remove('active'));
const thirdLink = document.querySelectorAll('#sidebar-container .sidebar li a')[2];
if (thirdLink) thirdLink.classList.add('active');
});
fetch('/topbar.html')
.then(r => r.text())
.then(data => document.getElementById('topbar-container').innerHTML = data);
<script type="module">
// ── Sidebar & Topbar ─────────────────────────────────────────────
fetch("/sidebar.html")
.then((r) => r.text())
.then((data) => {
document.getElementById("sidebar-container").innerHTML = data;
document
.querySelectorAll("#sidebar-container .sidebar a")
.forEach((l) => l.classList.remove("active"));
const thirdLink = document.querySelectorAll(
"#sidebar-container .sidebar li a",
)[2];
if (thirdLink) thirdLink.classList.add("active");
});
fetch("/topbar.html")
.then((r) => r.text())
.then(
(data) =>
(document.getElementById("topbar-container").innerHTML = data),
);
// ── Helpers ──────────────────────────────────────────────────────
function renderStars(rating) {
if (!rating) return '';
const stars = Math.round(rating);
return `<span style="color:#f59e0b">${'★'.repeat(stars)}${'☆'.repeat(5 - stars)}</span> <span style="color:#64748b;font-size:0.8rem">${rating.toFixed(1)}</span>`;
}
// ── Helpers ──────────────────────────────────────────────────────
function renderStars(rating) {
if (!rating) return "";
const stars = Math.round(rating);
return `<span class="creator-stars">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> <span class="creator-stars-value">${rating.toFixed(1)}</span>`;
}
function renderCard(c) {
return `
function renderCard(c) {
return `
<div class="creator-card">
<img class="creator-avatar"
src="${c.avatarUrl || '../images/content/cat.png'}"
src="${c.avatarUrl || "../images/content/cat.png"}"
alt="${c.displayName}"
style="cursor:pointer"
onclick="location.href='/profile?id=${c.userId}'">
<div class="creator-info">
<h3 class="creator-name"
style="cursor:pointer"
onclick="location.href='/profile?id=${c.userId}'">${c.displayName}</h3>
<div class="creator-handle">@${c.slug}</div>
<p class="creator-bio">${c.bio ?? 'No bio yet.'}</p>
<p class="creator-bio">${c.bio ?? "No bio yet."}</p>
<div class="creator-stats">
<span><i class="bi bi-puzzle"></i> ${c.promptCount} prompts</span>
<span><i class="bi bi-people"></i> ${c.subscribers}</span>
${c.averageRating > 0 ? `<span>${renderStars(c.averageRating)}</span>` : ''}
${c.averageRating > 0 ? `<span>${renderStars(c.averageRating)}</span>` : ""}
</div>
<button class="follow-btn ${c.isFollowing ? 'following' : ''}"
<button class="follow-btn ${c.isFollowing ? "following" : ""}"
data-userid="${c.userId}"
data-following="${c.isFollowing}">
${c.isFollowing ? 'Following' : 'Follow'}
${c.isFollowing ? "Following" : "Follow"}
</button>
</div>
</div>`;
}
// ── Follow / Unfollow ────────────────────────────────────────────
async function toggleFollow(btn) {
const userId = btn.dataset.userid;
const isFollowing = btn.dataset.following === 'true';
btn.disabled = true;
const res = await fetch(`/api/v1/subscriptions/${userId}`, {
method: isFollowing ? 'DELETE' : 'PUT',
credentials: 'same-origin'
});
if (res.status === 401) { location.href = '/login'; return; }
if (res.ok) {
const nowFollowing = !isFollowing;
btn.dataset.following = nowFollowing;
btn.textContent = nowFollowing ? 'Following' : 'Follow';
btn.classList.toggle('following', nowFollowing);
}
btn.disabled = false;
}
// ── Follow / Unfollow ────────────────────────────────────────────
async function toggleFollow(btn) {
const userId = btn.dataset.userid;
const isFollowing = btn.dataset.following === "true";
btn.disabled = true;
// ── Load Creators ────────────────────────────────────────────────
const grid = document.getElementById('creators-grid');
const emptyEl = document.getElementById('creators-empty');
const emptyTitle = document.getElementById('creators-empty-title');
const emptyText = document.getElementById('creators-empty-text');
const errorEl = document.getElementById('creators-error');
const errorMsg = document.getElementById('creators-error-msg');
let activeSort = 'popular';
let currentSearch = new URLSearchParams(location.search).get('search') || '';
function getSearchTerm() {
return currentSearch.trim();
}
async function loadCreators(sort = activeSort) {
activeSort = sort;
grid.innerHTML = '';
emptyEl.style.display = 'none';
errorEl.style.display = 'none';
try {
const params = new URLSearchParams({
sort,
limit: '50'
const res = await fetch(`/api/v1/subscriptions/${userId}`, {
method: isFollowing ? "DELETE" : "PUT",
credentials: "same-origin",
});
const search = getSearchTerm();
if (search) params.set('search', search);
const res = await fetch(`/api/v1/profiles?${params}`);
if (res.status === 401) { location.href = '/login'; return; }
if (!res.ok) throw new Error(`Server error ${res.status}`);
const creators = await res.json();
if (creators.length === 0) {
const search = getSearchTerm();
emptyTitle.textContent = search ? 'No matching creators' : 'No creators found';
emptyText.textContent = search
? `No creator matches "${search}". Try another name or clear the search.`
: 'Create another local user to see creators here.';
emptyEl.style.display = 'block';
if (res.status === 401) {
location.href = "/login";
return;
}
grid.innerHTML = creators.map(renderCard).join('');
if (res.ok) {
const nowFollowing = !isFollowing;
btn.dataset.following = nowFollowing;
btn.textContent = nowFollowing ? "Following" : "Follow";
btn.classList.toggle("following", nowFollowing);
}
grid.querySelectorAll('.follow-btn').forEach(btn => {
btn.addEventListener('click', () => toggleFollow(btn));
});
} catch (e) {
errorEl.style.display = 'block';
errorMsg.textContent = e.message;
btn.disabled = false;
}
}
// ── Filter buttons ───────────────────────────────────────────────
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
loadCreators(btn.dataset.sort);
// ── Load Creators ────────────────────────────────────────────────
const grid = document.getElementById("creators-grid");
const emptyEl = document.getElementById("creators-empty");
const emptyTitle = document.getElementById("creators-empty-title");
const emptyText = document.getElementById("creators-empty-text");
const errorEl = document.getElementById("creators-error");
const errorMsg = document.getElementById("creators-error-msg");
let activeSort = "popular";
let currentSearch =
new URLSearchParams(location.search).get("search") || "";
function getSearchTerm() {
return currentSearch.trim();
}
async function loadCreators(sort = activeSort) {
activeSort = sort;
grid.innerHTML = "";
emptyEl.style.display = "none";
errorEl.style.display = "none";
try {
const params = new URLSearchParams({
sort,
limit: "50",
});
const search = getSearchTerm();
if (search) params.set("search", search);
const res = await fetch(`/api/v1/profiles?${params}`);
if (res.status === 401) {
location.href = "/login";
return;
}
if (!res.ok) throw new Error(`Server error ${res.status}`);
const creators = await res.json();
if (creators.length === 0) {
const search = getSearchTerm();
emptyTitle.textContent = search
? "No matching creators"
: "No creators found";
emptyText.textContent = search
? `No creator matches "${search}". Try another name or clear the search.`
: "Create another local user to see creators here.";
emptyEl.style.display = "block";
return;
}
grid.innerHTML = creators.map(renderCard).join("");
grid.querySelectorAll(".follow-btn").forEach((btn) => {
btn.addEventListener("click", () => toggleFollow(btn));
});
} catch (e) {
errorEl.style.display = "block";
errorMsg.textContent = e.message;
}
}
// ── Filter buttons ───────────────────────────────────────────────
document.querySelectorAll(".filter-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document
.querySelectorAll(".filter-btn")
.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
loadCreators(btn.dataset.sort);
});
});
});
window.applyCreatorSearch = (value) => {
currentSearch = value.trim();
loadCreators(activeSort);
};
window.applyCreatorSearch = (value) => {
currentSearch = value.trim();
loadCreators(activeSort);
};
loadCreators();
</script>
</body>
loadCreators();
</script>
</body>
</html>

View File

@ -17,11 +17,11 @@
<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 class="layout">
<div id="sidebar-container"></div>
<div style="flex:1; display: flex; flex-direction: column;">
<div class="page-body">
<div id="topbar-container"></div>
@ -76,8 +76,8 @@
<label for="exampleImage">Example Image (optional)</label>
<input type="file" id="exampleImage" name="exampleImage" accept="image/png, image/jpeg, image/jpg">
<small class="form-hint">Upload a PNG or JPG preview will appear below.</small>
<div id="imagePreview" style="margin-top: 10px; display: none;">
<img id="previewImg" src="#" alt="Preview" style="max-width: 100%; max-height: 200px; border-radius: 12px;">
<div id="imagePreview">
<img id="previewImg" src="#" alt="Preview">
</div>
</div>
@ -88,7 +88,7 @@
<button type="button" id="freeBtn" class="price-option active">Free</button>
<button type="button" id="paidBtn" class="price-option">Paid</button>
</div>
<div id="priceField" style="display: none;">
<div id="priceField">
<input type="number" id="price" name="price" step="0.01" min="0" placeholder="Price in USD (e.g., 19.99)">
</div>
<small class="form-hint">You can set a price later or keep it free.</small>
@ -99,7 +99,7 @@
<button type="submit" class="submit-btn" id="submitPromptBtn">Publish Prompt</button>
<button type="button" class="cancel-btn">Cancel</button>
</div>
<p id="create-status" style="text-align:center;color:#64748b;margin:0;"></p>
<p id="create-status"></p></p>
</form>
</div>
</main>

View File

@ -28,7 +28,7 @@ body {
.form-error ul {
list-style: none;
padding-left: 0;
list-style: '*';
list-style: "*";
}
.form-error li {
@ -38,4 +38,46 @@ body {
.form-error li .error {
color: red;
font-style: italic;
}
}
/* ── Layout ──────────────────────────────────────────────────────────── */
.layout {
display: flex;
min-height: 100vh;
background: var(--bg);
}
/* Main content area - flex child that fills remaining space */
.page-body {
flex: 1;
display: flex;
flex-direction: column;
margin: 0;
max-width: 100%;
padding: 0;
width: 100%;
}
/* ── Reusable empty / error state components ─────────────────────────── */
.state-empty,
.state-error {
display: none;
text-align: center;
padding: 60px 20px;
}
.state-empty {
color: #64748b;
}
.state-error {
color: #ef4444;
}
.state-icon {
font-size: 3rem;
display: block;
margin-bottom: 16px;
}
.state-title {
margin-bottom: 8px;
}

View File

@ -1,13 +1,5 @@
/* Chats page - Two column layout: chat list + active chat window */
/* Full width layout */
.layout > div[style*="flex:1"] {
margin: 0 !important;
max-width: 100% !important;
padding: 0 !important;
width: 100% !important;
}
.chats-main {
flex: 1;
padding: 20px 32px;
@ -20,7 +12,7 @@
gap: 24px;
background: #fff;
border-radius: 18px;
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
overflow: hidden;
height: calc(100vh - 120px); /* Adjust based on topbar height */
min-height: 500px;
@ -244,4 +236,4 @@
.chat-active {
height: 500px;
}
}
}

View File

@ -1,13 +1,5 @@
/* Creators page - Discover creators, filter buttons, creator cards */
/* Full width layout */
.layout > div[style*="flex:1"] {
margin: 0 !important;
max-width: 100% !important;
padding: 0 !important;
width: 100% !important;
}
.creators-main {
background: transparent !important;
padding: 20px 32px !important;
@ -71,15 +63,17 @@
.creator-card {
background: #fff;
border-radius: 18px;
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
padding: 20px;
display: flex;
gap: 16px;
transition: transform 0.2s, box-shadow 0.2s;
transition:
transform 0.2s,
box-shadow 0.2s;
}
.creator-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
}
.creator-avatar {
@ -177,4 +171,19 @@
.follow-btn {
width: 100%;
}
}
}
/* Star rating in creator cards */
.creator-stars {
color: #f59e0b;
}
.creator-stars-value {
color: #64748b;
font-size: 0.8rem;
}
/* Clickable elements */
.creator-card .creator-avatar,
.creator-card .creator-name {
cursor: pointer;
}

View File

@ -1,13 +1,5 @@
/* Create page - Form for publishing new AI prompts */
/* Full width layout */
.layout > div[style*="flex:1"] {
margin: 0 !important;
max-width: 100% !important;
padding: 0 !important;
width: 100% !important;
}
.create-main {
flex: 1;
display: flex;
@ -22,12 +14,12 @@
width: 100%;
background: #fff;
border-radius: 18px;
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
padding: 32px;
transition: box-shadow 0.2s;
}
.create-container:hover {
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
}
/* Header */
@ -77,7 +69,7 @@
.form-group select:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 0 3px rgba(124,58,237,0.1);
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
}
.form-hint {
font-size: 0.75rem;
@ -106,6 +98,25 @@
}
#priceField {
margin-top: 8px;
display: none;
}
/* Image preview */
#imagePreview {
margin-top: 10px;
display: none;
}
#imagePreview img {
max-width: 100%;
max-height: 200px;
border-radius: 12px;
}
/* Status message */
#create-status {
text-align: center;
color: #64748b;
margin: 0;
}
/* Buttons */
@ -114,7 +125,8 @@
gap: 16px;
margin-top: 8px;
}
.submit-btn, .cancel-btn {
.submit-btn,
.cancel-btn {
flex: 1;
border: none;
padding: 12px;
@ -132,7 +144,8 @@
background: #f1f5f9;
color: #475569;
}
.submit-btn:hover, .cancel-btn:hover {
.submit-btn:hover,
.cancel-btn:hover {
opacity: 0.85;
}
@ -152,4 +165,4 @@
.form-actions {
flex-direction: column;
}
}
}

View File

@ -1,13 +1,5 @@
/* Feed page - Multi-column grid, square images, like/comment/save actions */
/* Full width layout */
.layout > div[style*="flex:1"] {
margin: 0 !important;
max-width: 100% !important;
padding: 0 !important;
width: 100% !important;
}
.feed-main {
background: transparent !important;
padding: 20px 32px !important;
@ -125,7 +117,7 @@
.post-title {
font-size: 1.1rem;
font-weight: 700;
margin: 0 0 6px 0;
margin: 10px 0 6px 0;
}
.post-description {
color: #334155;

View File

@ -1,13 +1,5 @@
/* Marketplace Page - Prompt cards, filter buttons, full width layout */
/* Full width layout */
.layout > div[style*="flex:1"] {
margin: 0 !important;
max-width: 100% !important;
padding: 0 !important;
width: 100% !important;
}
.marketplace-main {
background: transparent !important;
padding: 20px 32px !important;
@ -251,3 +243,238 @@
border-color: #6366f1;
background: #f5f3ff;
}
/* ── Payment Modal ──────────────────────────────────────────────────── */
#payment-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.payment-modal {
background: #fff;
border-radius: 16px;
padding: 32px;
max-width: 460px;
width: 90%;
position: relative;
max-height: 90vh;
overflow-y: auto;
}
.payment-close-btn {
position: absolute;
top: 14px;
right: 18px;
background: none;
border: none;
font-size: 1.4rem;
cursor: pointer;
color: #64748b;
}
.payment-title {
margin-bottom: 4px;
}
#pay-prompt-title {
color: #6366f1;
font-weight: 600;
margin-bottom: 16px;
}
.payment-intro {
color: #64748b;
margin-bottom: 24px;
}
.payment-method-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.pay-crypto-icon {
font-size: 1.4rem;
}
.pay-method-price {
margin-left: auto;
font-size: 0.85rem;
color: #94a3b8;
}
.payment-disclaimer {
margin-top: 20px;
font-size: 0.8rem;
color: #94a3b8;
text-align: center;
}
#pay-step-2 {
display: none;
}
.payment-back-btn {
background: none;
border: none;
color: #6366f1;
cursor: pointer;
margin-bottom: 16px;
font-size: 0.9rem;
}
#pay-crypto-title {
margin-bottom: 8px;
}
.payment-amount-text {
color: #64748b;
margin-bottom: 8px;
}
.payment-address-box {
background: #f1f5f9;
border-radius: 10px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
#pay-address {
font-size: 0.85rem;
word-break: break-all;
flex: 1;
}
.payment-copy-btn {
background: none;
border: none;
cursor: pointer;
color: #6366f1;
font-size: 1.1rem;
}
.payment-warning-text {
font-size: 0.78rem;
color: #94a3b8;
margin-bottom: 20px;
}
.payment-info-box {
background: #fef9c3;
border: 1px solid #fde68a;
border-radius: 10px;
padding: 12px 14px;
font-size: 0.82rem;
color: #92400e;
margin-bottom: 20px;
}
.payment-confirm-btn {
width: 100%;
padding: 12px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
}
#pay-step-3 {
display: none;
text-align: center;
padding: 20px 0;
}
.payment-success-icon {
font-size: 3.5rem;
color: #10b981;
display: block;
margin-bottom: 16px;
}
.payment-success-title {
margin-bottom: 8px;
}
.payment-success-desc {
color: #64748b;
margin-bottom: 24px;
}
.payment-done-btn {
padding: 12px 28px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
}
.market-card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.market-card-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: #6366f1;
color: #fff;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.95rem;
flex-shrink: 0;
}
.market-card-time {
margin-left: auto;
font-size: 0.75rem;
color: #94a3b8;
}
.market-card-rating {
margin-bottom: 12px;
}
.market-rating-none {
color: #94a3b8;
font-size: 0.8rem;
}
.market-rating-clickable {
cursor: pointer;
}
.market-rating-stars {
color: #f59e0b;
}
.buy-btn-locked {
background: #ef4444 !important;
}
.buy-btn-unlocked {
background: #10b981 !important;
}
.market-price-badge {
background: #fef3c7;
color: #92400e;
border-radius: 20px;
padding: 4px 14px;
font-size: 0.8rem;
font-weight: 600;
}
.market-heart-icon {
color: #ef4444;
}
.market-bookmark-icon {
color: #f59e0b;
}
.market-save-span {
margin-left: 12px;
}
.details-btn[disabled] {
opacity: 0.45;
cursor: not-allowed;
}

View File

@ -1,13 +1,5 @@
/* Post Detail page - Full prompt view, rating, example output, unlock button */
/* Full width layout */
.layout > div[style*="flex:1"] {
margin: 0 !important;
max-width: 100% !important;
padding: 0 !important;
width: 100% !important;
}
.post-detail-main {
flex: 1;
display: flex;
@ -21,12 +13,12 @@
width: 100%;
background: #fff;
border-radius: 18px;
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
padding: 32px;
transition: box-shadow 0.2s;
}
.post-detail-container:hover {
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
}
/* Header */
@ -319,3 +311,198 @@
padding: 16px;
}
}
/* ── Loading / error states ──────────────────────────────────────────── */
#detail-loading {
text-align: center;
padding: 60px 20px;
color: #64748b;
}
/* Smaller state icons for this page */
#detail-loading .state-icon,
#detail-error .state-icon {
font-size: 2.5rem;
margin-bottom: 12px;
}
#detail-error-msg {
color: #64748b;
margin-top: 8px;
}
.detail-back-btn {
margin-top: 20px;
padding: 10px 24px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
}
/* ── Detail body ─────────────────────────────────────────────────────── */
#detail-body {
display: none;
}
.detail-creator-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
#creator-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
background: #6366f1;
color: #fff;
font-weight: 700;
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
#creator-name {
font-weight: 600;
font-size: 0.95rem;
}
#prompt-date {
display: block;
font-size: 0.8rem;
color: #94a3b8;
}
.detail-actions-right {
margin-left: auto;
}
#edit-prompt-btn {
display: none;
margin-left: 10px;
padding: 6px 14px;
border: none;
border-radius: 10px;
background: #f1f5f9;
color: #334155;
font-weight: 700;
cursor: pointer;
}
#prompt-body {
white-space: pre-wrap;
font-family: monospace;
background: #f8fafc;
border-radius: 10px;
padding: 16px;
font-size: 0.9rem;
line-height: 1.7;
}
#example-section {
display: none;
}
#example-output-text {
white-space: pre-wrap;
}
#example-image {
display: none;
}
/* ── Locked section ──────────────────────────────────────────────────── */
#locked-section {
display: none;
text-align: center;
padding: 40px 20px;
background: #f8fafc;
border-radius: 12px;
margin-bottom: 28px;
}
.locked-icon {
font-size: 2.5rem;
color: #94a3b8;
display: block;
margin-bottom: 12px;
}
.locked-title {
margin-bottom: 8px;
}
.locked-desc {
color: #64748b;
margin-bottom: 20px;
}
#locked-subscribe-btn {
padding: 12px 28px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
font-size: 1rem;
}
.detail-heart-icon {
color: #ef4444;
}
.detail-bookmark-span {
margin-left: 12px;
}
.detail-bookmark-icon {
color: #f59e0b;
}
.detail-loading-text {
color: #94a3b8;
}
.detail-error-text {
color: #ef4444;
}
.rating-stars-display {
font-size: 1.1rem;
color: #f59e0b;
}
.rating-value {
margin-left: 8px;
font-weight: 600;
}
.rating-count {
font-size: 0.85rem;
color: #94a3b8;
margin-left: 4px;
}
.rating-none {
font-size: 0.9rem;
color: #94a3b8;
}
.tier-badge-paid {
background: #fef3c7;
color: #92400e;
border-radius: 20px;
padding: 4px 14px;
font-size: 0.8rem;
font-weight: 600;
}
.tier-badge-tier {
background: #f1f5f9;
color: #475569;
border-radius: 20px;
padding: 4px 14px;
font-size: 0.8rem;
font-weight: 600;
}
.tier-badge-free {
background: #dcfce7;
color: #166534;
border-radius: 20px;
padding: 4px 14px;
font-size: 0.8rem;
font-weight: 600;
}

View File

@ -1,14 +1,148 @@
/* Profile Page - Full width layout, darker share button, responsive grid */
/* Force main content container to full width, remove centering and max-width */
.layout > div[style*="flex:1"] {
margin: 0 !important;
max-width: 100% !important;
padding: 0 !important;
width: 100% !important;
/* ── Profile header ──────────────────────────────────────────────────── */
.profile-header {
display: flex;
align-items: center;
gap: 32px;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 24px;
}
/* Inner spacing for the profile card */
/* ── Profile avatar ──────────────────────────────────────────────────── */
.profile-avatar {
width: 110px;
height: 110px;
object-fit: cover;
}
/* ── Profile info column ─────────────────────────────────────────────── */
.profile-info {
flex: 1;
}
#profileDisplayName {
font-size: 2rem;
font-weight: 700;
margin-bottom: 4px;
}
#profileSlug {
color: #64748b;
margin-bottom: 8px;
}
.profile-badge-icon {
color: #3b82f6;
}
#profileBio {
margin-bottom: 8px;
}
#profileSpecialities {
color: #64748b;
}
#profileStats {
display: flex;
gap: 18px;
color: #64748b;
margin-top: 12px;
font-size: 0.95rem;
}
#profileStats strong {
color: #111827;
}
/* ── Profile actions column ──────────────────────────────────────────── */
#profileActions {
display: flex;
flex-direction: column;
gap: 10px;
}
/* ── Profile tabs ────────────────────────────────────────────────────── */
.profile-tabs {
display: flex;
gap: 24px;
border-bottom: 2px solid #e5e7eb;
margin: 32px 0 18px 0;
flex-wrap: wrap;
}
/* ── Prompts grid ────────────────────────────────────────────────────── */
#profile-prompts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 24px;
}
.profile-grid-loading {
grid-column: 1 / -1;
color: #64748b;
text-align: center;
padding: 28px;
}
.profile-grid-empty {
grid-column: 1 / -1;
color: #64748b;
text-align: center;
padding: 28px;
}
.profile-grid-error {
grid-column: 1 / -1;
color: #ef4444;
text-align: center;
padding: 28px;
}
.profile-prompt-card {
background: #fff;
border-radius: 18px;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
padding: 18px;
display: flex;
gap: 16px;
cursor: pointer;
}
.profile-prompt-img {
width: 72px;
height: 72px;
border-radius: 12px;
object-fit: cover;
}
.profile-prompt-body {
flex: 1;
min-width: 0;
}
.profile-prompt-title {
font-weight: 700;
}
.profile-prompt-desc {
color: #64748b;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.profile-prompt-meta {
display: flex;
gap: 16px;
color: #64748b;
align-items: center;
flex-wrap: wrap;
}
.profile-prompt-edit-btn {
border: none;
background: #f1f5f9;
color: #334155;
border-radius: 10px;
padding: 6px 10px;
font-weight: 700;
cursor: pointer;
}
/* ── Inner spacing for the profile card ─────────────────────────────── */
.profile-main {
background: transparent !important;
border-radius: 0 !important;
@ -16,7 +150,7 @@
padding: 20px 32px !important;
margin: 0 auto !important;
width: 100%;
max-width: 1600px; /* Limits content on very large screens, but still wide */
max-width: 1600px; /* Limits content on very large screens, but still wide */
}
/* Make prompts grid use more columns on large screens */
@ -29,7 +163,7 @@
/* Share button: darker background and text */
.profile-header button:last-child {
background: #cbd5e1 !important; /* darker gray */
background: #cbd5e1 !important; /* darker gray */
color: #1e293b !important;
box-shadow: none !important;
border: none !important;
@ -64,7 +198,7 @@
/* Prompt cards: rounded corners */
.profile-main section > div {
border-radius: 18px !important;
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
}
/* Prompt images: rounded corners */
@ -75,6 +209,9 @@
/* Avatar remains round */
.profile-avatar {
border-radius: 50% !important;
width: 110px;
height: 110px;
object-fit: cover;
}
/* All outer containers stay square */

View File

@ -1,12 +1,5 @@
/* Settings page - tabs, form styling */
.layout > div[style*="flex:1"] {
margin: 0 !important;
max-width: 100% !important;
padding: 0 !important;
width: 100% !important;
}
.settings-main {
flex: 1;
display: flex;
@ -21,7 +14,7 @@
width: 100%;
background: #fff;
border-radius: 18px;
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
padding: 32px;
}
@ -101,7 +94,7 @@
.form-group textarea:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 0 3px rgba(124,58,237,0.1);
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
}
.checkbox-label {
display: flex;
@ -181,4 +174,11 @@
flex-direction: column;
align-items: flex-start;
}
}
}
/* Save status message */
#profileSaveStatus {
margin-top: 10px;
color: #64748b;
text-align: center;
}

View File

@ -20,13 +20,10 @@
/>
</head>
<body>
<div
class="layout"
style="display: flex; min-height: 100vh; background: var(--bg)"
>
<div class="layout">
<div id="sidebar-container"></div>
<div style="flex: 1; display: flex; flex-direction: column">
<div class="page-body">
<div id="topbar-container"></div>
<main class="feed-main">
@ -61,38 +58,16 @@
<div class="posts-grid" id="posts-grid"></div>
<!-- Empty State -->
<div
id="feed-empty"
style="
display: none;
text-align: center;
padding: 60px 20px;
color: #64748b;
"
>
<i
class="bi bi-inbox"
style="font-size: 3rem; display: block; margin-bottom: 16px"
></i>
<h3 style="margin-bottom: 8px">No posts yet</h3>
<div id="feed-empty" class="state-empty">
<i class="bi bi-inbox state-icon"></i>
<h3 class="state-title">No posts yet</h3>
<p>Follow some creators to see their prompts here.</p>
</div>
<!-- Error State -->
<div
id="feed-error"
style="
display: none;
text-align: center;
padding: 60px 20px;
color: #ef4444;
"
>
<i
class="bi bi-exclamation-circle"
style="font-size: 3rem; display: block; margin-bottom: 16px"
></i>
<h3 style="margin-bottom: 8px">Could not load feed</h3>
<div id="feed-error" class="state-error">
<i class="bi bi-exclamation-circle state-icon"></i>
<h3 class="state-title">Could not load feed</h3>
<p id="feed-error-msg"></p>
</div>
</main>
@ -163,33 +138,33 @@
return `
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='${profileUrl(prompt.creatorId)}'">
<div class="post-header">
<img class="post-avatar" src="${prompt.creatorAvatarUrl || '../images/content/cat.png'}" alt="${prompt.creatorName}">
<img class="post-avatar" src="${prompt.creatorAvatarUrl || "../images/content/cat.png"}" alt="${prompt.creatorName}">
<div class="post-author">
<span class="post-name">${prompt.creatorName}</span>
</div>
<span class="post-date">${timeAgo(prompt.timeStamp)}</span>
</div>
<div class="post-content">
${prompt.exampleImageUrl ? `<img class="post-image${locked ? ' post-image-locked' : ''}" src="${prompt.exampleImageUrl}" alt="${prompt.title}">` : `<img class="post-image${locked ? ' post-image-locked' : ''}" src="${feedImg(prompt.id)}" alt="${prompt.title}">`}
<h3 class="post-title" style="margin-top:10px">${prompt.title}</h3>
<p class="post-description">${prompt.description || ''}</p>
${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill"></i> ${prompt.tierName ?? 'Paid'} tier required</p>` : ''}
${prompt.exampleImageUrl ? `<img class="post-image${locked ? " post-image-locked" : ""}" src="${prompt.exampleImageUrl}" alt="${prompt.title}">` : `<img class="post-image${locked ? " post-image-locked" : ""}" src="${feedImg(prompt.id)}" alt="${prompt.title}">`}
<h3 class="post-title">${prompt.title}</h3>
<p class="post-description">${prompt.description || ""}</p>
${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill"></i> ${prompt.tierName ?? "Paid"} tier required</p>` : ""}
${renderStars(prompt.averageRating)}
</div>
<div class="post-actions">
<button class="action-btn like-btn ${liked ? 'active' : ''}" onclick="toggleLike(event, '${prompt.id}', ${liked})"><i class="bi ${liked ? 'bi-heart-fill' : 'bi-heart'}"></i> <span>Like (${prompt.likeCount || 0})</span></button>
<button class="action-btn like-btn ${liked ? "active" : ""}" onclick="toggleLike(event, '${prompt.id}', ${liked})"><i class="bi ${liked ? "bi-heart-fill" : "bi-heart"}"></i> <span>Like (${prompt.likeCount || 0})</span></button>
<button class="action-btn comment-btn" onclick="event.stopPropagation(); location.href='/post-detail?id=${prompt.id}#rating-section'"><i class="bi bi-chat"></i> <span>Review</span></button>
<button class="action-btn share-btn" onclick="sharePrompt(event, '${prompt.id}')"><i class="bi bi-share"></i> <span>Share</span></button>
<button class="action-btn save-btn ${saved ? 'active' : ''}" onclick="toggleSave(event, '${prompt.id}', ${saved})"><i class="bi ${saved ? 'bi-bookmark-fill' : 'bi-bookmark'}"></i> <span>Save (${prompt.saveCount || 0})</span></button>
<button class="action-btn save-btn ${saved ? "active" : ""}" onclick="toggleSave(event, '${prompt.id}', ${saved})"><i class="bi ${saved ? "bi-bookmark-fill" : "bi-bookmark"}"></i> <span>Save (${prompt.saveCount || 0})</span></button>
</div>
</div>`;
}
window.toggleLike = async function(event, id, isLiked) {
window.toggleLike = async function (event, id, isLiked) {
event.stopPropagation();
const response = await fetch(`/api/v1/prompts/${id}/likes`, {
method: isLiked ? "DELETE" : "PUT",
credentials: "same-origin"
credentials: "same-origin",
});
if (response.status === 401) {
@ -200,26 +175,28 @@
if (!response.ok) return;
loadFeed(
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
document.querySelector(".filter-btn.active")?.dataset.ascending === "true"
document.querySelector(".filter-btn.active")?.dataset.ascending ===
"true",
);
};
window.toggleFeedState = function(event, type, id) {
window.toggleFeedState = function (event, type, id) {
event.stopPropagation();
const key = `prompt-${type}-${id}`;
const next = localStorage.getItem(key) !== "true";
localStorage.setItem(key, next);
loadFeed(
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
document.querySelector(".filter-btn.active")?.dataset.ascending === "true"
document.querySelector(".filter-btn.active")?.dataset.ascending ===
"true",
);
};
window.toggleSave = async function(event, id, isSaved) {
window.toggleSave = async function (event, id, isSaved) {
event.stopPropagation();
const response = await fetch(`/api/v1/prompts/${id}/saves`, {
method: isSaved ? "DELETE" : "PUT",
credentials: "same-origin"
credentials: "same-origin",
});
if (response.status === 401) {
@ -230,13 +207,16 @@
if (!response.ok) return;
loadFeed(
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
document.querySelector(".filter-btn.active")?.dataset.ascending === "true"
document.querySelector(".filter-btn.active")?.dataset.ascending ===
"true",
);
};
window.sharePrompt = function(event, id) {
window.sharePrompt = function (event, id) {
event.stopPropagation();
navigator.clipboard.writeText(`${location.origin}/post-detail?id=${id}`);
navigator.clipboard.writeText(
`${location.origin}/post-detail?id=${id}`,
);
};
async function loadFeed(sortBy = "date", ascending = false) {

View File

@ -60,13 +60,10 @@
</style>
</head>
<body>
<div
class="layout"
style="display: flex; min-height: 100vh; background: var(--bg)"
>
<div class="layout">
<div id="sidebar-container"></div>
<div style="flex: 1; display: flex; flex-direction: column">
<div class="page-body">
<div id="topbar-container"></div>
<main class="marketplace-main">
@ -100,38 +97,16 @@
<div class="prompts-grid" id="prompts-grid"></div>
<!-- Empty State -->
<div
id="market-empty"
style="
display: none;
text-align: center;
padding: 60px 20px;
color: #64748b;
"
>
<i
class="bi bi-bag-x"
style="font-size: 3rem; display: block; margin-bottom: 16px"
></i>
<h3 style="margin-bottom: 8px">No prompts found</h3>
<div id="market-empty" class="state-empty">
<i class="bi bi-bag-x state-icon"></i>
<h3 class="state-title">No prompts found</h3>
<p>Try a different category or search term.</p>
</div>
<!-- Error State -->
<div
id="market-error"
style="
display: none;
text-align: center;
padding: 60px 20px;
color: #ef4444;
"
>
<i
class="bi bi-exclamation-circle"
style="font-size: 3rem; display: block; margin-bottom: 16px"
></i>
<h3 style="margin-bottom: 8px">Could not load prompts</h3>
<div id="market-error" class="state-error">
<i class="bi bi-exclamation-circle state-icon"></i>
<h3 class="state-title">Could not load prompts</h3>
<p id="market-error-msg"></p>
</div>
</main>
@ -139,213 +114,83 @@
</div>
<!-- ── Payment Modal ─────────────────────────────────────────────── -->
<div
id="payment-overlay"
style="
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
"
>
<div
style="
background: #fff;
border-radius: 16px;
padding: 32px;
max-width: 460px;
width: 90%;
position: relative;
max-height: 90vh;
overflow-y: auto;
"
>
<button
onclick="closePayment()"
style="
position: absolute;
top: 14px;
right: 18px;
background: none;
border: none;
font-size: 1.4rem;
cursor: pointer;
color: #64748b;
"
>
<div id="payment-overlay">
<div class="payment-modal">
<button onclick="closePayment()" class="payment-close-btn">
&#x2715;
</button>
<!-- Step 1: Choose method -->
<div id="pay-step-1">
<h2 style="margin-bottom: 4px">Subscribe to access</h2>
<p
id="pay-prompt-title"
style="color: #6366f1; font-weight: 600; margin-bottom: 16px"
></p>
<p style="color: #64748b; margin-bottom: 24px">
<h2 class="payment-title">Subscribe to access</h2>
<p id="pay-prompt-title"></p>
<p class="payment-intro">
Choose a payment method to unlock this prompt:
</p>
<div style="display: flex; flex-direction: column; gap: 12px">
<div class="payment-method-list">
<button class="pay-method-btn" onclick="selectCrypto('btc')">
<span style="font-size: 1.4rem"></span> Bitcoin (BTC)
<span
id="price-btc"
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
></span>
<span class="pay-crypto-icon"></span> Bitcoin (BTC)
<span id="price-btc" class="pay-method-price"></span>
</button>
<button class="pay-method-btn" onclick="selectCrypto('eth')">
<span style="font-size: 1.4rem">Ξ</span> Ethereum (ETH)
<span
id="price-eth"
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
></span>
<span class="pay-crypto-icon">Ξ</span> Ethereum (ETH)
<span id="price-eth" class="pay-method-price"></span>
</button>
<button class="pay-method-btn" onclick="selectCrypto('sol')">
<span style="font-size: 1.4rem"></span> Solana (SOL)
<span
id="price-sol"
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
></span>
<span class="pay-crypto-icon"></span> Solana (SOL)
<span id="price-sol" class="pay-method-price"></span>
</button>
<button class="pay-method-btn" onclick="selectCrypto('usdt')">
<span style="font-size: 1.4rem"></span> USDT (TRC-20)
<span
id="price-usdt"
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
></span>
<span class="pay-crypto-icon"></span> USDT (TRC-20)
<span id="price-usdt" class="pay-method-price"></span>
</button>
</div>
<p
style="
margin-top: 20px;
font-size: 0.8rem;
color: #94a3b8;
text-align: center;
"
>
<p class="payment-disclaimer">
<i class="bi bi-shield-lock-fill"></i> Payments are processed
on-chain. No account needed.
</p>
</div>
<!-- Step 2: Send payment -->
<div id="pay-step-2" style="display: none">
<button
onclick="backToStep1()"
style="
background: none;
border: none;
color: #6366f1;
cursor: pointer;
margin-bottom: 16px;
font-size: 0.9rem;
"
>
<div id="pay-step-2">
<button onclick="backToStep1()" class="payment-back-btn">
&#8592; Back
</button>
<h2 id="pay-crypto-title" style="margin-bottom: 8px"></h2>
<p style="color: #64748b; margin-bottom: 8px">
<h2 id="pay-crypto-title"></h2>
<p class="payment-amount-text">
Send exactly <strong id="pay-amount"></strong> to:
</p>
<div
style="
background: #f1f5f9;
border-radius: 10px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
"
>
<code
id="pay-address"
style="font-size: 0.85rem; word-break: break-all; flex: 1"
></code>
<div class="payment-address-box">
<code id="pay-address"></code>
<button
onclick="copyAddress()"
title="Copy"
style="
background: none;
border: none;
cursor: pointer;
color: #6366f1;
font-size: 1.1rem;
"
class="payment-copy-btn"
>
<i class="bi bi-clipboard"></i>
</button>
</div>
<p style="font-size: 0.78rem; color: #94a3b8; margin-bottom: 20px">
<p class="payment-warning-text">
⚠️ Only send the exact amount. Payments are non-refundable.
</p>
<div
style="
background: #fef9c3;
border: 1px solid #fde68a;
border-radius: 10px;
padding: 12px 14px;
font-size: 0.82rem;
color: #92400e;
margin-bottom: 20px;
"
>
<div class="payment-info-box">
<i class="bi bi-info-circle-fill"></i> After sending, click the
button below to confirm. Access will be granted once the transaction
is verified.
</div>
<button
onclick="confirmPayment()"
style="
width: 100%;
padding: 12px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
"
>
<button onclick="confirmPayment()" class="payment-confirm-btn">
I've sent the payment ✓
</button>
</div>
<!-- Step 3: Success -->
<div
id="pay-step-3"
style="display: none; text-align: center; padding: 20px 0"
>
<i
class="bi bi-check-circle-fill"
style="
font-size: 3.5rem;
color: #10b981;
display: block;
margin-bottom: 16px;
"
></i>
<h2 style="margin-bottom: 8px">Payment received!</h2>
<p style="color: #64748b; margin-bottom: 24px">
<div id="pay-step-3">
<i class="bi bi-check-circle-fill payment-success-icon"></i>
<h2 class="payment-success-title">Payment received!</h2>
<p class="payment-success-desc">
Your access is being activated. This usually takes 12 minutes.
</p>
<button
onclick="closePayment()"
style="
padding: 12px 28px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
"
>
<button onclick="closePayment()" class="payment-done-btn">
Done
</button>
</div>
@ -387,22 +232,29 @@
return `${Math.floor(h / 24)}d ago`;
}
function renderStars(rating, reviewCount = 0, promptId = null, locked = false) {
const target = promptId && !locked
? ` onclick="location.href='/post-detail?id=${promptId}#rating-section'" title="View reviews" style="cursor:pointer;"`
: "";
function renderStars(
rating,
reviewCount = 0,
promptId = null,
locked = false,
) {
const target =
promptId && !locked
? ` onclick="location.href='/post-detail?id=${promptId}#rating-section'" title="View reviews" class="market-rating-clickable"`
: "";
if (rating == null)
return `<span${target} style="color:#94a3b8;font-size:0.8rem;${promptId && !locked ? 'cursor:pointer;' : ''}">No reviews yet</span>`;
return `<span${target} class="market-rating-none">No reviews yet</span>`;
const stars = Math.round(rating);
const label = reviewCount === 1 ? "review" : "reviews";
return `<span class="prompt-rating"${target}><span style="color:#f59e0b">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> ${rating.toFixed(1)} (${reviewCount} ${label})</span>`;
return `<span class="prompt-rating"${target}><span class="market-rating-stars">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> ${rating.toFixed(1)} (${reviewCount} ${label})</span>`;
}
function promptPrice(prompt) {
if (prompt.price != null && Number(prompt.price) > 0) {
return `$${Number(prompt.price).toFixed(2)}`;
}
if (prompt.tierLevel) return `$${(prompt.tierLevel * 4.99).toFixed(2)}/mo`;
if (prompt.tierLevel)
return `$${(prompt.tierLevel * 4.99).toFixed(2)}/mo`;
if (prompt.canAccess === false) return "Paid";
return "Free";
}
@ -424,7 +276,9 @@
const direction = ascending === "true" ? 1 : -1;
return prompts
.slice()
.sort((a, b) => (getNumericPrice(a) - getNumericPrice(b)) * direction);
.sort(
(a, b) => (getNumericPrice(a) - getNumericPrice(b)) * direction,
);
}
return prompts;
@ -443,29 +297,32 @@
function renderCard(p) {
const paid = p.price != null && Number(p.price) > 0;
const locked = p.canAccess === false || paid || p.tierLevel != null;
const img = p.exampleImageUrl || p._img || MARKET_IMAGES[cardIndex++ % MARKET_IMAGES.length];
const img =
p.exampleImageUrl ||
p._img ||
MARKET_IMAGES[cardIndex++ % MARKET_IMAGES.length];
return `
<div class="prompt-card">
<img src="${img}" alt="${p.title}" class="prompt-img">
<div class="prompt-info">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<div style="width:34px;height:34px;border-radius:50%;background:#6366f1;color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center;font-size:0.95rem;flex-shrink:0;">${p.creatorName.charAt(0).toUpperCase()}</div>
<div class="market-card-header">
<div class="market-card-avatar">${p.creatorName.charAt(0).toUpperCase()}</div>
<span class="prompt-author">@${p.creatorName}</span>
<span style="margin-left:auto;font-size:0.75rem;color:#94a3b8;">${timeAgo(p.timeStamp)}</span>
<span class="market-card-time">${timeAgo(p.timeStamp)}</span>
</div>
<h3 class="prompt-title">${p.title}</h3>
<p class="prompt-description">${p.description || 'No description yet.'}</p>
<div style="margin-bottom:12px;">${renderStars(p.averageRating, p.reviewCount || 0, p.id, locked)}</div>
<p class="prompt-description">${p.description || "No description yet."}</p>
<div class="market-card-rating">${renderStars(p.averageRating, p.reviewCount || 0, p.id, locked)}</div>
<div class="prompt-price">${promptPrice(p)}</div>
<div class="prompt-actions">
${
locked
? `<button class="buy-btn" style="background:#ef4444;" onclick='openPayment(${JSON.stringify(p)})'><i class="bi bi-lock-fill"></i> Pay</button>`
: `<button class="buy-btn" style="background:#10b981;" onclick="location.href='/post-detail?id=${p.id}'">Access <i class="bi bi-unlock-fill"></i></button>`
? `<button class="buy-btn buy-btn-locked" onclick='openPayment(${JSON.stringify(p)})'><i class="bi bi-lock-fill"></i> Pay</button>`
: `<button class="buy-btn buy-btn-unlocked" onclick="location.href='/post-detail?id=${p.id}'">Access <i class="bi bi-unlock-fill"></i></button>`
}
${
locked
? `<button class="details-btn" disabled style="opacity:.45;cursor:not-allowed;"><i class="bi bi-lock-fill"></i> Details</button>`
? `<button class="details-btn" disabled><i class="bi bi-lock-fill"></i> Details</button>`
: `<button class="details-btn" onclick="location.href='/post-detail?id=${p.id}'">View Details</button>`
}
</div>
@ -477,7 +334,8 @@
const grid = document.getElementById("prompts-grid");
const emptyEl = document.getElementById("market-empty");
const errorEl = document.getElementById("market-error");
const search = document.getElementById("topbarSearchInput")?.value.trim() || "";
const search =
document.getElementById("topbarSearchInput")?.value.trim() || "";
const [sortBy, ascending] = document
.getElementById("sort-select")
.value.split("|");
@ -488,8 +346,10 @@
cardIndex = 0;
try {
const apiSortBy = sortBy === "price" || sortBy === "free" ? "date" : sortBy;
const apiAscending = sortBy === "price" || sortBy === "free" ? "false" : ascending;
const apiSortBy =
sortBy === "price" || sortBy === "free" ? "date" : sortBy;
const apiAscending =
sortBy === "price" || sortBy === "free" ? "false" : ascending;
let url = `/api/v1/prompts?sortBy=${apiSortBy}&ascending=${apiAscending}&limit=50`;
if (activeCategory) url += `&category=${activeCategory}`;
if (search) url += `&search=${encodeURIComponent(search)}`;
@ -501,7 +361,11 @@
}
if (!res.ok) throw new Error(`Server error ${res.status}`);
let prompts = applyMarketplaceSort(await res.json(), sortBy, ascending);
let prompts = applyMarketplaceSort(
await res.json(),
sortBy,
ascending,
);
if (prompts.length === 0) {
emptyEl.style.display = "block";
@ -596,9 +460,12 @@
function openPayment(prompt) {
currentPrompt = prompt;
const usd = prompt.price != null && Number(prompt.price) > 0
? Number(prompt.price)
: prompt.tierLevel ? prompt.tierLevel * 4.99 : 0;
const usd =
prompt.price != null && Number(prompt.price) > 0
? Number(prompt.price)
: prompt.tierLevel
? prompt.tierLevel * 4.99
: 0;
document.getElementById("pay-prompt-title").textContent = prompt.title;
document.getElementById("price-btc").textContent =
`≈ ${(usd / 67000).toFixed(6)} BTC`;

View File

@ -19,121 +19,43 @@
/>
</head>
<body>
<div
class="layout"
style="display: flex; min-height: 100vh; background: var(--bg)"
>
<div class="layout">
<div id="sidebar-container"></div>
<div style="flex: 1; display: flex; flex-direction: column">
<div class="page-body">
<div id="topbar-container"></div>
<main class="post-detail-main">
<div class="post-detail-container" id="detail-content">
<!-- Loading -->
<div
id="detail-loading"
style="text-align: center; padding: 60px 20px; color: #64748b"
>
<i
class="bi bi-hourglass-split"
style="font-size: 2.5rem; display: block; margin-bottom: 12px"
></i>
<div id="detail-loading">
<i class="bi bi-hourglass-split state-icon"></i>
<p>Loading prompt...</p>
</div>
<!-- Error -->
<div
id="detail-error"
style="
display: none;
text-align: center;
padding: 60px 20px;
color: #ef4444;
"
>
<i
class="bi bi-exclamation-circle"
style="font-size: 2.5rem; display: block; margin-bottom: 12px"
></i>
<div id="detail-error" class="state-error">
<i class="bi bi-exclamation-circle state-icon"></i>
<h3 id="detail-error-title">Prompt not found</h3>
<p
id="detail-error-msg"
style="color: #64748b; margin-top: 8px"
></p>
<button
onclick="history.back()"
style="
margin-top: 20px;
padding: 10px 24px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
"
>
<p id="detail-error-msg"></p>
<button onclick="history.back()" class="detail-back-btn">
Go Back
</button>
</div>
<!-- Content (hidden until loaded) -->
<div id="detail-body" style="display: none">
<div id="detail-body">
<!-- Header -->
<div class="post-header">
<div
style="
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
"
>
<div
id="creator-avatar"
style="
width: 42px;
height: 42px;
border-radius: 50%;
background: #6366f1;
color: #fff;
font-weight: 700;
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
"
></div>
<div class="detail-creator-row">
<div id="creator-avatar"></div>
<div>
<span
id="creator-name"
style="font-weight: 600; font-size: 0.95rem"
></span>
<span
id="prompt-date"
style="display: block; font-size: 0.8rem; color: #94a3b8"
></span>
<span id="creator-name"></span>
<span id="prompt-date"></span>
</div>
<div style="margin-left: auto">
<div class="detail-actions-right">
<span id="tier-badge"></span>
<button
id="edit-prompt-btn"
style="
display: none;
margin-left: 10px;
padding: 6px 14px;
border: none;
border-radius: 10px;
background: #f1f5f9;
color: #334155;
font-weight: 700;
cursor: pointer;
"
>
Edit
</button>
<button id="edit-prompt-btn">Edit</button>
</div>
</div>
<h1 class="post-title" id="prompt-title"></h1>
@ -157,73 +79,38 @@
<!-- Prompt Content (only if accessible) -->
<div class="prompt-section" id="prompt-content-section">
<h2>PROMPT</h2>
<div
class="prompt-content"
id="prompt-body"
style="
white-space: pre-wrap;
font-family: monospace;
background: #f8fafc;
border-radius: 10px;
padding: 16px;
font-size: 0.9rem;
line-height: 1.7;
"
></div>
<div class="prompt-content" id="prompt-body"></div>
</div>
<!-- Example Output -->
<div class="example-section" id="example-section" style="display: none">
<div class="example-section" id="example-section">
<h2>EXAMPLE OUTPUT</h2>
<div class="example-content">
<div id="example-output-text" class="example-output-text" style="white-space: pre-wrap"></div>
<div id="example-image" class="example-image" style="display: none">
<img id="example-image-img" src="" alt="Example output image">
<div
id="example-output-text"
class="example-output-text"
></div>
<div id="example-image" class="example-image">
<img
id="example-image-img"
src=""
alt="Example output image"
/>
</div>
</div>
</div>
<!-- Locked section (shown instead of prompt if no access) -->
<div
id="locked-section"
style="
display: none;
text-align: center;
padding: 40px 20px;
background: #f8fafc;
border-radius: 12px;
margin-bottom: 28px;
"
>
<i
class="bi bi-lock-fill"
style="
font-size: 2.5rem;
color: #94a3b8;
display: block;
margin-bottom: 12px;
"
></i>
<h3 style="margin-bottom: 8px">
<div id="locked-section">
<i class="bi bi-lock-fill locked-icon"></i>
<h3 class="locked-title">
This prompt requires a subscription
</h3>
<p style="color: #64748b; margin-bottom: 20px">
<p class="locked-desc">
Subscribe to <strong id="locked-creator"></strong> to access
this prompt.
</p>
<button
id="locked-subscribe-btn"
style="
padding: 12px 28px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
font-size: 1rem;
"
>
<button id="locked-subscribe-btn">
Subscribe <span id="locked-tier-name"></span>
</button>
</div>
@ -238,19 +125,30 @@
<h2>REVIEWS</h2>
<div class="review-form" id="review-form">
<h3>Your review</h3>
<div class="review-star-input" id="review-star-input" aria-label="Select rating">
<div
class="review-star-input"
id="review-star-input"
aria-label="Select rating"
>
<button type="button" data-rating="1"></button>
<button type="button" data-rating="2"></button>
<button type="button" data-rating="3"></button>
<button type="button" data-rating="4"></button>
<button type="button" data-rating="5"></button>
</div>
<textarea id="review-comment" maxlength="200" rows="3" placeholder="Write a short comment..."></textarea>
<button type="button" id="submit-review-btn">Submit Review</button>
<textarea
id="review-comment"
maxlength="200"
rows="3"
placeholder="Write a short comment..."
></textarea>
<button type="button" id="submit-review-btn">
Submit Review
</button>
<p id="review-message"></p>
</div>
<div class="reviews-list" id="reviews-list">
<p style="color:#94a3b8;">Loading reviews...</p>
<p class="detail-loading-text">Loading reviews...</p>
</div>
</div>
</div>
@ -331,8 +229,8 @@
p.description;
document.getElementById("prompt-body").textContent = p.content;
document.getElementById("prompt-rating-stat").innerHTML =
`<i class="bi ${p.isLiked ? 'bi-heart-fill' : 'bi-heart'}" style="color:#ef4444;"></i> ${p.likeCount || 0} likes
<span style="margin-left:12px;"><i class="bi ${p.isSaved ? 'bi-bookmark-fill' : 'bi-bookmark'}" style="color:#f59e0b;"></i> ${p.saveCount || 0} saves</span>`;
`<i class="bi ${p.isLiked ? "bi-heart-fill" : "bi-heart"} detail-heart-icon"></i> ${p.likeCount || 0} likes
<span class="detail-bookmark-span"><i class="bi ${p.isSaved ? "bi-bookmark-fill" : "bi-bookmark"} detail-bookmark-icon"></i> ${p.saveCount || 0} saves</span>`;
renderExamples(p);
renderOwnerActions(p);
setupReviewSection(p);
@ -341,27 +239,27 @@
// Tier badge
const badge = document.getElementById("tier-badge");
if (p.price != null && Number(p.price) > 0) {
badge.innerHTML = `<span style="background:#fef3c7;color:#92400e;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">$${Number(p.price).toFixed(2)}</span>`;
badge.innerHTML = `<span class="tier-badge-paid">$${Number(p.price).toFixed(2)}</span>`;
} else if (p.tierName) {
badge.innerHTML = `<span style="background:#f1f5f9;color:#475569;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;"><i class="bi bi-lock-fill"></i> ${p.tierName}</span>`;
badge.innerHTML = `<span class="tier-badge-tier"><i class="bi bi-lock-fill"></i> ${p.tierName}</span>`;
} else {
badge.innerHTML = `<span style="background:#dcfce7;color:#166534;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">Free</span>`;
badge.innerHTML = `<span class="tier-badge-free">Free</span>`;
}
// Rating
if (p.averageRating != null) {
const stars = Math.round(p.averageRating);
document.getElementById("rating-display").innerHTML =
`<span style="color:#f59e0b;font-size:1.1rem;">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span>
<span style="margin-left:8px;font-weight:600;">${p.averageRating.toFixed(1)}</span>
<span style="color:#94a3b8;font-size:0.85rem;margin-left:4px;">/ 5.0 (${p.reviewCount || 0} ${(p.reviewCount || 0) === 1 ? "review" : "reviews"})</span>`;
`<span class="rating-stars-display">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span>
<span class="rating-value">${p.averageRating.toFixed(1)}</span>
<span class="rating-count">/ 5.0 (${p.reviewCount || 0} ${(p.reviewCount || 0) === 1 ? "review" : "reviews"})</span>`;
document.getElementById("prompt-rating-stat").innerHTML =
`<i class="bi ${p.isLiked ? 'bi-heart-fill' : 'bi-heart'}" style="color:#ef4444;"></i> ${p.likeCount || 0} likes
<span style="margin-left:12px;"><i class="bi ${p.isSaved ? 'bi-bookmark-fill' : 'bi-bookmark'}" style="color:#f59e0b;"></i> ${p.saveCount || 0} saves</span>
<span style="margin-left:12px;"><i class="bi bi-star-fill" style="color:#f59e0b;"></i> ${p.averageRating.toFixed(1)} (${p.reviewCount || 0})</span>`;
`<i class="bi ${p.isLiked ? "bi-heart-fill" : "bi-heart"} detail-heart-icon"></i> ${p.likeCount || 0} likes
<span class="detail-bookmark-span"><i class="bi ${p.isSaved ? "bi-bookmark-fill" : "bi-bookmark"} detail-bookmark-icon"></i> ${p.saveCount || 0} saves</span>
<span class="detail-bookmark-span"><i class="bi bi-star-fill detail-bookmark-icon"></i> ${p.averageRating.toFixed(1)} (${p.reviewCount || 0})</span>`;
} else {
document.getElementById("rating-display").innerHTML =
'<span style="color:#94a3b8;font-size:0.9rem;">No ratings yet</span>';
'<span class="rating-none">No ratings yet</span>';
}
// Content visibility
@ -397,10 +295,12 @@
function setReviewRating(rating) {
selectedReviewRating = rating;
document.querySelectorAll("#review-star-input button").forEach((button) => {
const value = Number(button.dataset.rating);
button.textContent = value <= rating ? "★" : "☆";
});
document
.querySelectorAll("#review-star-input button")
.forEach((button) => {
const value = Number(button.dataset.rating);
button.textContent = value <= rating ? "★" : "☆";
});
}
function escapeHtml(value) {
@ -451,9 +351,12 @@
// Keep the review form visible; the API will reject unauthenticated users.
}
document.querySelectorAll("#review-star-input button").forEach((button) => {
button.onclick = () => setReviewRating(Number(button.dataset.rating));
});
document
.querySelectorAll("#review-star-input button")
.forEach((button) => {
button.onclick = () =>
setReviewRating(Number(button.dataset.rating));
});
submitBtn.onclick = async () => {
if (!selectedReviewRating) {
@ -470,7 +373,8 @@
credentials: "same-origin",
body: JSON.stringify({
rating: selectedReviewRating,
comment: document.getElementById("review-comment").value.trim() || null,
comment:
document.getElementById("review-comment").value.trim() || null,
}),
});
@ -505,13 +409,16 @@
const reviews = await response.json();
if (reviews.length === 0) {
list.innerHTML = '<p style="color:#94a3b8;">No reviews yet.</p>';
list.innerHTML =
'<p class="detail-loading-text">No reviews yet.</p>';
return;
}
list.innerHTML = reviews.map((review) => {
const stars = "★".repeat(review.rating) + "☆".repeat(5 - review.rating);
return `
list.innerHTML = reviews
.map((review) => {
const stars =
"★".repeat(review.rating) + "☆".repeat(5 - review.rating);
return `
<article class="review-card">
<div class="review-card-header">
<span class="review-card-user">@${escapeHtml(review.creatorName)}</span>
@ -519,9 +426,10 @@
</div>
<p class="review-card-comment">${escapeHtml(review.comment || "No comment.")}</p>
</article>`;
}).join("");
})
.join("");
} catch (error) {
list.innerHTML = `<p style="color:#ef4444;">${error.message}</p>`;
list.innerHTML = `<p class="detail-error-text">${error.message}</p>`;
}
}

View File

@ -1,398 +1,486 @@
<!-- OnlyPrompt - Profile page:
- User profile display with avatar, bio, stats, and prompt cards (personal prompts) -->
<!DOCTYPE html>
<!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>
<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 style="flex:1; margin:40px auto; max-width:950px;">
<div id="topbar-container"></div>
<div class="page-body">
<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>
<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="profileBio" style="margin-bottom:8px;">
Loading profile...
<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>
<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>
<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>
<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>
<section id="profile-prompts-grid">
<div class="profile-grid-loading">Loading prompts...</div>
</section>
</main>
</div>
</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');
<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");
});
// 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);
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;
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;
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();
}
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 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;
}
}
}
function isPromptMarked(type, id) {
if (type === 'liked') {
const prompt = allPrompts.find((item) => item.id === id);
return prompt?.isLiked === true;
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.
}
}
if (type === 'saved') {
const prompt = allPrompts.find((item) => item.id === id);
return prompt?.isSaved === true;
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 = "";
}
}
}
return false;
}
function isPromptMarked(type, id) {
if (type === "liked") {
const prompt = allPrompts.find((item) => item.id === id);
return prompt?.isLiked === true;
}
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;">
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}'" style="border:none;background:#f1f5f9;color:#334155;border-radius:10px;padding:6px 10px;font-weight:700;cursor:pointer;">Edit</button>` : ''}
${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 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 renderPromptList(prompts, emptyText, options = {}) {
if (!prompts.length) {
profilePromptsGrid.innerHTML = `<div class="profile-grid-empty">${emptyText}</div>`;
return;
}
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;
profilePromptsGrid.innerHTML = prompts
.map((prompt) => renderProfilePrompt(prompt, options))
.join("");
}
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'
function updateTabs() {
document.querySelectorAll(".profile-tab").forEach((tab) => {
tab.classList.toggle("active", tab.dataset.tab === activeProfileTab);
});
if (response.status === 401) {
location.href = '/login';
return;
}
const subscription = response.ok ? await response.json() : null;
currentIsFollowing = subscription !== null;
updateProfileMode();
} catch {
currentIsFollowing = false;
}
}
const liked = allPrompts.filter((prompt) =>
isPromptMarked("liked", prompt.id),
);
const saved = allPrompts.filter((prompt) =>
isPromptMarked("saved", prompt.id),
);
async function toggleProfileFollow() {
if (!profileId) return;
myPromptsTab.textContent = `My Prompts (${ownPrompts.length})`;
favoritesTab.textContent = `Favorites (${liked.length})`;
savedTab.textContent = `Saved (${saved.length})`;
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();
if (activeProfileTab === "favorites") {
renderPromptList(liked, "No liked prompts yet.");
} else if (activeProfileTab === "saved") {
renderPromptList(saved, "No saved prompts yet.");
} else {
profilePrompts = allPrompts.filter((prompt) => prompt.creatorId?.toLowerCase() === profileId.toLowerCase());
renderProfileFromPrompt(profilePrompts[0]);
updateProfileMode();
renderPromptList(ownPrompts, "No prompts yet.", { showEdit: true });
}
} catch {
// Favorites and saved stay empty if prompts cannot be loaded.
}
}
document.querySelectorAll('.profile-tab').forEach((tab) => {
tab.addEventListener('click', () => {
if (!isOwnProfile) {
updateProfileMode();
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;
}
activeProfileTab = tab.dataset.tab;
updateTabs();
});
});
(async function initProfilePage() {
await loadProfile();
await loadCreatorCardFallback();
await loadFollowState();
updateProfileMode();
if (isOwnProfile) {
loadOwnPrompts();
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.");
}
loadAllPromptReferences();
})();
</script>
</body>
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>

View File

@ -17,11 +17,11 @@
<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 class="layout">
<div id="sidebar-container"></div>
<div style="flex:1; display: flex; flex-direction: column;">
<div class="page-body">
<div id="topbar-container"></div>
@ -68,7 +68,7 @@
</div>
<div class="form-actions">
<button type="submit" class="save-btn">Save Changes</button>
<p id="profileSaveStatus" style="margin-top:10px;color:#64748b;text-align:center;"></p>
<p id="profileSaveStatus"></p></p>
</div>
</form>
</div>