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,65 +1,85 @@
<!-- OnlyPrompt - Chats page: <!-- OnlyPrompt - Chats page:
- Direct messaging interface with conversation list and active chat window --> - Direct messaging interface with conversation list and active chat window -->
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OnlyPrompt - Chats</title> <title>OnlyPrompt - Chats</title>
<link rel="stylesheet" href="../css/variables.css"> <link rel="stylesheet" href="../css/variables.css" />
<link rel="stylesheet" href="../css/base.css"> <link rel="stylesheet" href="../css/base.css" />
<link rel="stylesheet" href="../css/sidebar.css"> <link rel="stylesheet" href="../css/sidebar.css" />
<link rel="stylesheet" href="../css/login.css"> <link rel="stylesheet" href="../css/login.css" />
<link rel="stylesheet" href="../css/topbar.css"> <link rel="stylesheet" href="../css/topbar.css" />
<link rel="stylesheet" href="../css/chats.css"> <link rel="stylesheet" href="../css/chats.css" />
<script src="../js/profile-shared.js"></script> <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"> <link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
</head> </head>
<body> <body>
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);"> <div class="layout">
<div id="sidebar-container"></div> <div id="sidebar-container"></div>
<div style="flex:1; display: flex; flex-direction: column;"> <div class="page-body">
<div id="topbar-container"></div> <div id="topbar-container"></div>
<main class="chats-main"> <main class="chats-main">
<!-- Chat Container: Left column (list) + Right column (active chat) --> <!-- Chat Container: Left column (list) + Right column (active chat) -->
<div class="chat-container"> <div class="chat-container">
<!-- Left Column: Chat Overview --> <!-- Left Column: Chat Overview -->
<div class="chat-list"> <div class="chat-list">
<div class="chat-list-header"> <div class="chat-list-header">
<h2>Messages</h2> <h2>Messages</h2>
<button class="new-chat-btn"><i class="bi bi-pencil-square"></i></button> <button class="new-chat-btn">
<i class="bi bi-pencil-square"></i>
</button>
</div> </div>
<div class="chat-list-items"> <div class="chat-list-items">
<!-- Chat Entry 1 (active) --> <!-- Chat Entry 1 (active) -->
<div class="chat-item active"> <div class="chat-item active">
<img src="../images/content/creator2.png" alt="Alex Chen" class="chat-avatar"> <img
src="../images/content/creator2.png"
alt="Alex Chen"
class="chat-avatar"
/>
<div class="chat-item-info"> <div class="chat-item-info">
<div class="chat-name">Alex Chen</div> <div class="chat-name">Alex Chen</div>
<div class="chat-last-msg">Hey Sarah! Really loved your last video on minimalism...</div> <div class="chat-last-msg">
Hey Sarah! Really loved your last video on minimalism...
</div>
</div> </div>
<div class="chat-time">10:17 AM</div> <div class="chat-time">10:17 AM</div>
</div> </div>
<!-- Chat Entry 2 --> <!-- Chat Entry 2 -->
<div class="chat-item"> <div class="chat-item">
<img src="../images/content/creator3.png" alt="Mia Wong" class="chat-avatar"> <img
src="../images/content/creator3.png"
alt="Mia Wong"
class="chat-avatar"
/>
<div class="chat-item-info"> <div class="chat-item-info">
<div class="chat-name">Mia Wong</div> <div class="chat-name">Mia Wong</div>
<div class="chat-last-msg">Thanks for the prompt tips! They worked perfectly.</div> <div class="chat-last-msg">
Thanks for the prompt tips! They worked perfectly.
</div>
</div> </div>
<div class="chat-time">Yesterday</div> <div class="chat-time">Yesterday</div>
</div> </div>
<!-- Chat Entry 3 --> <!-- Chat Entry 3 -->
<div class="chat-item"> <div class="chat-item">
<img src="../images/content/creator4.png" alt="Tom Rivera" class="chat-avatar"> <img
src="../images/content/creator4.png"
alt="Tom Rivera"
class="chat-avatar"
/>
<div class="chat-item-info"> <div class="chat-item-info">
<div class="chat-name">Tom Rivera</div> <div class="chat-name">Tom Rivera</div>
<div class="chat-last-msg">Let's schedule a call for the collab?</div> <div class="chat-last-msg">
Let's schedule a call for the collab?
</div>
</div> </div>
<div class="chat-time">Yesterday</div> <div class="chat-time">Yesterday</div>
</div> </div>
@ -69,62 +89,87 @@
<!-- Right Column: Active Chat (with Alex Chen) --> <!-- Right Column: Active Chat (with Alex Chen) -->
<div class="chat-active"> <div class="chat-active">
<div class="chat-header"> <div class="chat-header">
<img src="../images/content/creator2.png" alt="Alex Chen" class="chat-avatar-large"> <img
src="../images/content/creator2.png"
alt="Alex Chen"
class="chat-avatar-large"
/>
<div class="chat-header-info"> <div class="chat-header-info">
<div class="chat-header-name">Alex Chen</div> <div class="chat-header-name">Alex Chen</div>
<div class="chat-header-status"><span class="online-dot"></span> Online</div> <div class="chat-header-status">
<span class="online-dot"></span> Online
</div>
</div> </div>
</div> </div>
<div class="chat-messages"> <div class="chat-messages">
<!-- Message from Alex --> <!-- Message from Alex -->
<div class="message received"> <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-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 class="message-time">10:15 AM</div>
</div> </div>
<!-- Reply from Sarah --> <!-- Reply from Sarah -->
<div class="message sent"> <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-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 class="message-time">10:16 AM</div>
</div> </div>
<!-- Alex replies --> <!-- Alex replies -->
<div class="message received"> <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-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 class="message-time">10:17 AM</div>
</div> </div>
<!-- Sarah replies --> <!-- Sarah replies -->
<div class="message sent"> <div class="message sent">
<div class="message-bubble">Awesome! Let me know if you need more tips. Enjoy the process! 😊</div> <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 class="message-time">10:18 AM</div>
</div> </div>
</div> </div>
<div class="chat-input-area"> <div class="chat-input-area">
<input type="text" placeholder="Type your message..."> <input type="text" placeholder="Type your message..." />
<button class="send-btn">Send</button> <button class="send-btn">Send</button>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
</div> </div>
</div> </div>
<script> <script>
fetch('/sidebar.html') fetch("/sidebar.html")
.then(r => r.text()) .then((r) => r.text())
.then(data => { .then((data) => {
document.getElementById('sidebar-container').innerHTML = data; document.getElementById("sidebar-container").innerHTML = data;
// Remove 'active' from all sidebar links // Remove 'active' from all sidebar links
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => { document
link.classList.remove('active'); .querySelectorAll("#sidebar-container .sidebar a")
.forEach((link) => {
link.classList.remove("active");
}); });
// Set 'active' on the Chats link (4th link, index 3) // Set 'active' on the Chats link (4th link, index 3)
const chatsLink = document.querySelectorAll('#sidebar-container .sidebar li a')[3]; const chatsLink = document.querySelectorAll(
if (chatsLink) chatsLink.classList.add('active'); "#sidebar-container .sidebar li a",
)[3];
if (chatsLink) chatsLink.classList.add("active");
}); });
fetch('/topbar.html') fetch("/topbar.html")
.then(r => r.text()) .then((r) => r.text())
.then(data => document.getElementById('topbar-container').innerHTML = data); .then(
(data) =>
(document.getElementById("topbar-container").innerHTML = data),
);
</script> </script>
</body> </body>
</html> </html>

View File

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

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

View File

@ -28,7 +28,7 @@ body {
.form-error ul { .form-error ul {
list-style: none; list-style: none;
padding-left: 0; padding-left: 0;
list-style: '*'; list-style: "*";
} }
.form-error li { .form-error li {
@ -39,3 +39,45 @@ body {
color: red; color: red;
font-style: italic; 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 */ /* 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 { .chats-main {
flex: 1; flex: 1;
padding: 20px 32px; padding: 20px 32px;

View File

@ -1,13 +1,5 @@
/* Creators page - Discover creators, filter buttons, creator cards */ /* 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 { .creators-main {
background: transparent !important; background: transparent !important;
padding: 20px 32px !important; padding: 20px 32px !important;
@ -75,7 +67,9 @@
padding: 20px; padding: 20px;
display: flex; display: flex;
gap: 16px; gap: 16px;
transition: transform 0.2s, box-shadow 0.2s; transition:
transform 0.2s,
box-shadow 0.2s;
} }
.creator-card:hover { .creator-card:hover {
transform: translateY(-2px); transform: translateY(-2px);
@ -178,3 +172,18 @@
width: 100%; 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 */ /* 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 { .create-main {
flex: 1; flex: 1;
display: flex; display: flex;
@ -106,6 +98,25 @@
} }
#priceField { #priceField {
margin-top: 8px; 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 */ /* Buttons */
@ -114,7 +125,8 @@
gap: 16px; gap: 16px;
margin-top: 8px; margin-top: 8px;
} }
.submit-btn, .cancel-btn { .submit-btn,
.cancel-btn {
flex: 1; flex: 1;
border: none; border: none;
padding: 12px; padding: 12px;
@ -132,7 +144,8 @@
background: #f1f5f9; background: #f1f5f9;
color: #475569; color: #475569;
} }
.submit-btn:hover, .cancel-btn:hover { .submit-btn:hover,
.cancel-btn:hover {
opacity: 0.85; opacity: 0.85;
} }

View File

@ -1,13 +1,5 @@
/* Feed page - Multi-column grid, square images, like/comment/save actions */ /* 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 { .feed-main {
background: transparent !important; background: transparent !important;
padding: 20px 32px !important; padding: 20px 32px !important;
@ -125,7 +117,7 @@
.post-title { .post-title {
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 700; font-weight: 700;
margin: 0 0 6px 0; margin: 10px 0 6px 0;
} }
.post-description { .post-description {
color: #334155; color: #334155;

View File

@ -1,13 +1,5 @@
/* Marketplace Page - Prompt cards, filter buttons, full width layout */ /* 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 { .marketplace-main {
background: transparent !important; background: transparent !important;
padding: 20px 32px !important; padding: 20px 32px !important;
@ -251,3 +243,238 @@
border-color: #6366f1; border-color: #6366f1;
background: #f5f3ff; 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 */ /* 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 { .post-detail-main {
flex: 1; flex: 1;
display: flex; display: flex;
@ -319,3 +311,198 @@
padding: 16px; 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 */ /* Profile Page - Full width layout, darker share button, responsive grid */
/* Force main content container to full width, remove centering and max-width */ /* ── Profile header ──────────────────────────────────────────────────── */
.layout > div[style*="flex:1"] { .profile-header {
margin: 0 !important; display: flex;
max-width: 100% !important; align-items: center;
padding: 0 !important; gap: 32px;
width: 100% !important; 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 { .profile-main {
background: transparent !important; background: transparent !important;
border-radius: 0 !important; border-radius: 0 !important;
@ -75,6 +209,9 @@
/* Avatar remains round */ /* Avatar remains round */
.profile-avatar { .profile-avatar {
border-radius: 50% !important; border-radius: 50% !important;
width: 110px;
height: 110px;
object-fit: cover;
} }
/* All outer containers stay square */ /* All outer containers stay square */

View File

@ -1,12 +1,5 @@
/* Settings page - tabs, form styling */ /* 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 { .settings-main {
flex: 1; flex: 1;
display: flex; display: flex;
@ -182,3 +175,10 @@
align-items: flex-start; align-items: flex-start;
} }
} }
/* Save status message */
#profileSaveStatus {
margin-top: 10px;
color: #64748b;
text-align: center;
}

View File

@ -20,13 +20,10 @@
/> />
</head> </head>
<body> <body>
<div <div class="layout">
class="layout"
style="display: flex; min-height: 100vh; background: var(--bg)"
>
<div id="sidebar-container"></div> <div id="sidebar-container"></div>
<div style="flex: 1; display: flex; flex-direction: column"> <div class="page-body">
<div id="topbar-container"></div> <div id="topbar-container"></div>
<main class="feed-main"> <main class="feed-main">
@ -61,38 +58,16 @@
<div class="posts-grid" id="posts-grid"></div> <div class="posts-grid" id="posts-grid"></div>
<!-- Empty State --> <!-- Empty State -->
<div <div id="feed-empty" class="state-empty">
id="feed-empty" <i class="bi bi-inbox state-icon"></i>
style=" <h3 class="state-title">No posts yet</h3>
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>
<p>Follow some creators to see their prompts here.</p> <p>Follow some creators to see their prompts here.</p>
</div> </div>
<!-- Error State --> <!-- Error State -->
<div <div id="feed-error" class="state-error">
id="feed-error" <i class="bi bi-exclamation-circle state-icon"></i>
style=" <h3 class="state-title">Could not load feed</h3>
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>
<p id="feed-error-msg"></p> <p id="feed-error-msg"></p>
</div> </div>
</main> </main>
@ -163,24 +138,24 @@
return ` return `
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='${profileUrl(prompt.creatorId)}'"> <div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='${profileUrl(prompt.creatorId)}'">
<div class="post-header"> <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"> <div class="post-author">
<span class="post-name">${prompt.creatorName}</span> <span class="post-name">${prompt.creatorName}</span>
</div> </div>
<span class="post-date">${timeAgo(prompt.timeStamp)}</span> <span class="post-date">${timeAgo(prompt.timeStamp)}</span>
</div> </div>
<div class="post-content"> <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}">`} ${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> <h3 class="post-title">${prompt.title}</h3>
<p class="post-description">${prompt.description || ''}</p> <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>` : ''} ${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill"></i> ${prompt.tierName ?? "Paid"} tier required</p>` : ""}
${renderStars(prompt.averageRating)} ${renderStars(prompt.averageRating)}
</div> </div>
<div class="post-actions"> <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 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 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>
</div>`; </div>`;
} }
@ -189,7 +164,7 @@
event.stopPropagation(); event.stopPropagation();
const response = await fetch(`/api/v1/prompts/${id}/likes`, { const response = await fetch(`/api/v1/prompts/${id}/likes`, {
method: isLiked ? "DELETE" : "PUT", method: isLiked ? "DELETE" : "PUT",
credentials: "same-origin" credentials: "same-origin",
}); });
if (response.status === 401) { if (response.status === 401) {
@ -200,7 +175,8 @@
if (!response.ok) return; if (!response.ok) return;
loadFeed( loadFeed(
document.querySelector(".filter-btn.active")?.dataset.sort || "date", document.querySelector(".filter-btn.active")?.dataset.sort || "date",
document.querySelector(".filter-btn.active")?.dataset.ascending === "true" document.querySelector(".filter-btn.active")?.dataset.ascending ===
"true",
); );
}; };
@ -211,7 +187,8 @@
localStorage.setItem(key, next); localStorage.setItem(key, next);
loadFeed( loadFeed(
document.querySelector(".filter-btn.active")?.dataset.sort || "date", document.querySelector(".filter-btn.active")?.dataset.sort || "date",
document.querySelector(".filter-btn.active")?.dataset.ascending === "true" document.querySelector(".filter-btn.active")?.dataset.ascending ===
"true",
); );
}; };
@ -219,7 +196,7 @@
event.stopPropagation(); event.stopPropagation();
const response = await fetch(`/api/v1/prompts/${id}/saves`, { const response = await fetch(`/api/v1/prompts/${id}/saves`, {
method: isSaved ? "DELETE" : "PUT", method: isSaved ? "DELETE" : "PUT",
credentials: "same-origin" credentials: "same-origin",
}); });
if (response.status === 401) { if (response.status === 401) {
@ -230,13 +207,16 @@
if (!response.ok) return; if (!response.ok) return;
loadFeed( loadFeed(
document.querySelector(".filter-btn.active")?.dataset.sort || "date", 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(); 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) { async function loadFeed(sortBy = "date", ascending = false) {

View File

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

View File

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

View File

@ -1,134 +1,181 @@
<!-- OnlyPrompt - Profile page: <!-- OnlyPrompt - Profile page:
- User profile display with avatar, bio, stats, and prompt cards (personal prompts) --> - User profile display with avatar, bio, stats, and prompt cards (personal prompts) -->
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OnlyPrompt - Profile</title> <title>OnlyPrompt - Profile</title>
<link rel="stylesheet" href="../css/variables.css"> <link rel="stylesheet" href="../css/variables.css" />
<link rel="stylesheet" href="../css/base.css"> <link rel="stylesheet" href="../css/base.css" />
<link rel="stylesheet" href="../css/sidebar.css"> <link rel="stylesheet" href="../css/sidebar.css" />
<link rel="stylesheet" href="../css/login.css"> <link rel="stylesheet" href="../css/login.css" />
<link rel="stylesheet" href="../css/topbar.css"> <link rel="stylesheet" href="../css/topbar.css" />
<link rel="stylesheet" href="../css/profile.css"> <link rel="stylesheet" href="../css/profile.css" />
<script src="../js/profile-shared.js"></script> <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"> <link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
</head> </head>
<body> <body>
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);"> <div class="layout">
<div id="sidebar-container"></div> <div id="sidebar-container"></div>
<div style="flex:1; margin:40px auto; max-width:950px;"> <div class="page-body">
<div id="topbar-container"></div> <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;"> <main class="login-card profile-main">
<section class="profile-header">
<img
id="profileAvatar"
src="../images/content/cat.png"
class="profile-avatar"
/>
<section class="profile-header" style="display:flex;align-items:center;gap:32px;border-bottom:1px solid #e5e7eb;padding-bottom:24px;"> <div class="profile-info">
<h1 id="profileDisplayName">Loading...</h1>
<img id="profileAvatar" src="../images/content/cat.png" class="profile-avatar" style="width:110px;height:110px;border-radius:50%;object-fit:cover;"> <div id="profileSlug">
@profile
<div class="profile-info" style="flex:1;"> <i class="bi bi-patch-check-fill profile-badge-icon"></i>
<h1 id="profileDisplayName" style="font-size:2rem;font-weight:700;margin-bottom:4px;">Loading...</h1>
<div id="profileSlug" style="color:#64748b;margin-bottom:8px;">
@profile <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>
</div> </div>
<div id="profileBio" style="margin-bottom:8px;"> <div id="profileBio">Loading profile...</div>
Loading profile...
</div>
<div id="profileSpecialities" style="color:#64748b;"></div> <div id="profileSpecialities"></div>
<div id="profileStats" style="display:flex;gap:18px;color:#64748b;margin-top:12px;font-size:0.95rem;"> <div id="profileStats">
<span><strong id="profileRating" style="color:#111827;">0.0</strong> rating</span> <span><strong id="profileRating">0.0</strong> rating</span>
<span><strong id="profileSubscribers" style="color:#111827;">0</strong> subscribers</span> <span
><strong id="profileSubscribers">0</strong> subscribers</span
>
</div> </div>
</div> </div>
<div id="profileActions" style="display:flex;flex-direction:column;gap:10px;"> <div id="profileActions">
<button id="primaryProfileButton" class="login-button" onclick="location.href='settings.html'">Edit Profile</button> <button
<button id="shareProfileButton" class="login-button" style="background:#f3f4f6;color:#111;box-shadow:none;">Share Profile</button> id="primaryProfileButton"
class="login-button"
onclick="location.href = 'settings.html'"
>
Edit Profile
</button>
<button id="shareProfileButton" class="login-button">
Share Profile
</button>
</div> </div>
</section> </section>
<nav class="profile-tabs" style="display:flex;gap:24px;border-bottom:2px solid #e5e7eb;margin:32px 0 18px 0;flex-wrap:wrap;"> <nav class="profile-tabs">
<button type="button" class="profile-tab active" data-tab="mine" id="myPromptsTab">My Prompts</button> <button
<button type="button" class="profile-tab" data-tab="favorites" id="favoritesTab">Favorites</button> type="button"
<button type="button" class="profile-tab" data-tab="saved" id="savedTab">Saved</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> </nav>
<section id="profile-prompts-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:24px;"> <section id="profile-prompts-grid">
<div style="grid-column:1/-1;color:#64748b;text-align:center;padding:28px;">Loading prompts...</div> <div class="profile-grid-loading">Loading prompts...</div>
</section> </section>
</main> </main>
</div> </div>
</div> </div>
<script> <script>
fetch('/sidebar.html') fetch("/sidebar.html")
.then(r => r.text()) .then((r) => r.text())
.then(data => { .then((data) => {
document.getElementById('sidebar-container').innerHTML = data; document.getElementById("sidebar-container").innerHTML = data;
// Remove 'active' from all sidebar links // Remove 'active' from all sidebar links
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => { document
link.classList.remove('active'); .querySelectorAll("#sidebar-container .sidebar a")
.forEach((link) => {
link.classList.remove("active");
}); });
// Then set 'active' only on the My Profile link // Then set 'active' only on the My Profile link
const profileLink = document.querySelector('#sidebar-container a[href="profile.html"]'); const profileLink = document.querySelector(
if (profileLink) profileLink.classList.add('active'); '#sidebar-container a[href="profile.html"]',
);
if (profileLink) profileLink.classList.add("active");
}); });
fetch('/topbar.html') fetch("/topbar.html")
.then(r => r.text()) .then((r) => r.text())
.then(data => document.getElementById('topbar-container').innerHTML = data); .then(
(data) =>
(document.getElementById("topbar-container").innerHTML = data),
);
const profileAvatar = document.getElementById('profileAvatar'); const profileAvatar = document.getElementById("profileAvatar");
const profileDisplayName = document.getElementById('profileDisplayName'); const profileDisplayName = document.getElementById("profileDisplayName");
const profileSlug = document.getElementById('profileSlug'); const profileSlug = document.getElementById("profileSlug");
const profileBio = document.getElementById('profileBio'); const profileBio = document.getElementById("profileBio");
const profileSpecialities = document.getElementById('profileSpecialities'); const profileSpecialities = document.getElementById(
const profileRating = document.getElementById('profileRating'); "profileSpecialities",
const profileSubscribers = document.getElementById('profileSubscribers'); );
const profilePromptsGrid = document.getElementById('profile-prompts-grid'); const profileRating = document.getElementById("profileRating");
const myPromptsTab = document.getElementById('myPromptsTab'); const profileSubscribers = document.getElementById("profileSubscribers");
const favoritesTab = document.getElementById('favoritesTab'); const profilePromptsGrid = document.getElementById(
const savedTab = document.getElementById('savedTab'); "profile-prompts-grid",
const profileActions = document.getElementById('profileActions'); );
const primaryProfileButton = document.getElementById('primaryProfileButton'); const myPromptsTab = document.getElementById("myPromptsTab");
const shareProfileButton = document.getElementById('shareProfileButton'); const favoritesTab = document.getElementById("favoritesTab");
const profileTabs = document.querySelector('.profile-tabs'); 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 params = new URLSearchParams(location.search);
const profileId = params.get('id'); const profileId = params.get("id");
let ownPrompts = []; let ownPrompts = [];
let allPrompts = []; let allPrompts = [];
let profilePrompts = []; let profilePrompts = [];
let activeProfileTab = 'mine'; let activeProfileTab = "mine";
let currentUserId = null; let currentUserId = null;
let isOwnProfile = !profileId; let isOwnProfile = !profileId;
let profileLoaded = false; let profileLoaded = false;
let currentIsFollowing = false; let currentIsFollowing = false;
async function fetchJson(url) { async function fetchJson(url) {
const response = await fetch(url, { credentials: 'same-origin' }); const response = await fetch(url, { credentials: "same-origin" });
if (response.status === 401) { if (response.status === 401) {
location.href = '/login'; location.href = "/login";
return null; return null;
} }
if (!response.ok) throw new Error(`${url} returned ${response.status}`); if (!response.ok) throw new Error(`${url} returned ${response.status}`);
return response.json(); return response.json();
} }
function renderProfile(profile, fallbackName = 'Profile') { function renderProfile(profile, fallbackName = "Profile") {
profileDisplayName.textContent = profile.displayName || fallbackName; profileDisplayName.textContent = profile.displayName || fallbackName;
profileSlug.innerHTML = `@${profile.user?.userName || profile.slug || 'profile'} <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>`; 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.'; profileBio.textContent = profile.bio || "No bio yet.";
profileSpecialities.textContent = profile.specialities || 'No specialities added yet.'; profileSpecialities.textContent =
profileRating.textContent = Number(profile.averageRating || 0).toFixed(1); profile.specialities || "No specialities added yet.";
profileRating.textContent = Number(profile.averageRating || 0).toFixed(
1,
);
profileSubscribers.textContent = profile.subscribers || 0; profileSubscribers.textContent = profile.subscribers || 0;
if (profile.avatarUrl) { if (profile.avatarUrl) {
@ -140,11 +187,14 @@
function renderProfileFromPrompt(prompt) { function renderProfileFromPrompt(prompt) {
if (!prompt || profileLoaded) return; if (!prompt || profileLoaded) return;
profileDisplayName.textContent = prompt.creatorName || 'Creator Profile'; profileDisplayName.textContent =
profileSlug.innerHTML = `@${prompt.creatorName || 'creator'} <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>`; prompt.creatorName || "Creator Profile";
profileBio.textContent = 'No bio yet.'; profileSlug.innerHTML = `@${prompt.creatorName || "creator"} <i class="bi bi-patch-check-fill profile-badge-icon"></i>`;
profileSpecialities.textContent = ''; profileBio.textContent = "No bio yet.";
profileRating.textContent = Number(prompt.averageRating || 0).toFixed(1); profileSpecialities.textContent = "";
profileRating.textContent = Number(prompt.averageRating || 0).toFixed(
1,
);
profileSubscribers.textContent = 0; profileSubscribers.textContent = 0;
if (prompt.creatorAvatarUrl) { if (prompt.creatorAvatarUrl) {
@ -156,19 +206,24 @@
if (isOwnProfile || profileLoaded || !profileId) return; if (isOwnProfile || profileLoaded || !profileId) return;
try { try {
const creators = await fetchJson('/api/v1/profiles?limit=100'); const creators = await fetchJson("/api/v1/profiles?limit=100");
const creator = creators.find((item) => item.userId?.toLowerCase() === profileId.toLowerCase()); const creator = creators.find(
(item) => item.userId?.toLowerCase() === profileId.toLowerCase(),
);
if (!creator) return; if (!creator) return;
renderProfile({ renderProfile(
{
displayName: creator.displayName, displayName: creator.displayName,
slug: creator.slug, slug: creator.slug,
bio: creator.bio, bio: creator.bio,
avatarUrl: creator.avatarUrl, avatarUrl: creator.avatarUrl,
specialities: null, specialities: null,
averageRating: creator.averageRating, averageRating: creator.averageRating,
subscribers: creator.subscribers subscribers: creator.subscribers,
}, 'Creator Profile'); },
"Creator Profile",
);
} catch { } catch {
// Prompt data below still provides a minimal fallback if creator cards fail. // Prompt data below still provides a minimal fallback if creator cards fail.
} }
@ -178,33 +233,37 @@
try { try {
const currentProfile = await window.loadCurrentProfile(); const currentProfile = await window.loadCurrentProfile();
currentUserId = currentProfile.user?.id; currentUserId = currentProfile.user?.id;
isOwnProfile = !profileId || profileId.toLowerCase() === currentUserId?.toLowerCase(); isOwnProfile =
!profileId ||
profileId.toLowerCase() === currentUserId?.toLowerCase();
if (isOwnProfile) { if (isOwnProfile) {
renderProfile(currentProfile, 'My Profile'); renderProfile(currentProfile, "My Profile");
return; return;
} }
const profile = await fetchJson(`/api/v1/profiles/${encodeURIComponent(profileId)}`); const profile = await fetchJson(
renderProfile(profile, 'Creator Profile'); `/api/v1/profiles/${encodeURIComponent(profileId)}`,
);
renderProfile(profile, "Creator Profile");
} catch (error) { } catch (error) {
if (isOwnProfile) { if (isOwnProfile) {
profileDisplayName.textContent = 'Profile unavailable'; profileDisplayName.textContent = "Profile unavailable";
profileBio.textContent = error.message; profileBio.textContent = error.message;
} else { } else {
profileDisplayName.textContent = 'Loading creator...'; profileDisplayName.textContent = "Loading creator...";
profileBio.textContent = ''; profileBio.textContent = "";
} }
} }
} }
function isPromptMarked(type, id) { function isPromptMarked(type, id) {
if (type === 'liked') { if (type === "liked") {
const prompt = allPrompts.find((item) => item.id === id); const prompt = allPrompts.find((item) => item.id === id);
return prompt?.isLiked === true; return prompt?.isLiked === true;
} }
if (type === 'saved') { if (type === "saved") {
const prompt = allPrompts.find((item) => item.id === id); const prompt = allPrompts.find((item) => item.id === id);
return prompt?.isSaved === true; return prompt?.isSaved === true;
} }
@ -213,19 +272,22 @@
} }
function renderProfilePrompt(prompt, options = {}) { function renderProfilePrompt(prompt, options = {}) {
const image = prompt.exampleImageUrl || '../images/content/post1.png'; const image = prompt.exampleImageUrl || "../images/content/post1.png";
const showEdit = options.showEdit === true; const showEdit = options.showEdit === true;
const rating = prompt.averageRating == null ? 'No ratings' : prompt.averageRating.toFixed(1); const rating =
prompt.averageRating == null
? "No ratings"
: prompt.averageRating.toFixed(1);
return ` 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;"> <div onclick="location.href='/post-detail?id=${prompt.id}'" class="profile-prompt-card">
<img src="${image}" alt="${prompt.title}" style="width:72px;height:72px;border-radius:12px;object-fit:cover;"> <img src="${image}" alt="${prompt.title}" class="profile-prompt-img">
<div style="flex:1;min-width:0;"> <div class="profile-prompt-body">
<div style="font-weight:700;">${prompt.title}</div> <div class="profile-prompt-title">${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 class="profile-prompt-desc">${prompt.description || "No description yet."}</div>
<div style="display:flex;gap:16px;color:#64748b;align-items:center;flex-wrap:wrap;"> <div class="profile-prompt-meta">
<span><i class="bi bi-star"></i> ${rating}</span> <span><i class="bi bi-star"></i> ${rating}</span>
${prompt.creatorName ? `<span>@${prompt.creatorName}</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>` : ''} ${showEdit ? `<button onclick="event.stopPropagation(); location.href='/create?id=${prompt.id}'" class="profile-prompt-edit-btn">Edit</button>` : ""}
</div> </div>
</div> </div>
</div>`; </div>`;
@ -233,63 +295,75 @@
function renderPromptList(prompts, emptyText, options = {}) { function renderPromptList(prompts, emptyText, options = {}) {
if (!prompts.length) { if (!prompts.length) {
profilePromptsGrid.innerHTML = `<div style="grid-column:1/-1;color:#64748b;text-align:center;padding:28px;">${emptyText}</div>`; profilePromptsGrid.innerHTML = `<div class="profile-grid-empty">${emptyText}</div>`;
return; return;
} }
profilePromptsGrid.innerHTML = prompts.map((prompt) => renderProfilePrompt(prompt, options)).join(''); profilePromptsGrid.innerHTML = prompts
.map((prompt) => renderProfilePrompt(prompt, options))
.join("");
} }
function updateTabs() { function updateTabs() {
document.querySelectorAll('.profile-tab').forEach((tab) => { document.querySelectorAll(".profile-tab").forEach((tab) => {
tab.classList.toggle('active', tab.dataset.tab === activeProfileTab); tab.classList.toggle("active", tab.dataset.tab === activeProfileTab);
}); });
const liked = allPrompts.filter((prompt) => isPromptMarked('liked', prompt.id)); const liked = allPrompts.filter((prompt) =>
const saved = allPrompts.filter((prompt) => isPromptMarked('saved', prompt.id)); isPromptMarked("liked", prompt.id),
);
const saved = allPrompts.filter((prompt) =>
isPromptMarked("saved", prompt.id),
);
myPromptsTab.textContent = `My Prompts (${ownPrompts.length})`; myPromptsTab.textContent = `My Prompts (${ownPrompts.length})`;
favoritesTab.textContent = `Favorites (${liked.length})`; favoritesTab.textContent = `Favorites (${liked.length})`;
savedTab.textContent = `Saved (${saved.length})`; savedTab.textContent = `Saved (${saved.length})`;
if (activeProfileTab === 'favorites') { if (activeProfileTab === "favorites") {
renderPromptList(liked, 'No liked prompts yet.'); renderPromptList(liked, "No liked prompts yet.");
} else if (activeProfileTab === 'saved') { } else if (activeProfileTab === "saved") {
renderPromptList(saved, 'No saved prompts yet.'); renderPromptList(saved, "No saved prompts yet.");
} else { } else {
renderPromptList(ownPrompts, 'No prompts yet.', { showEdit: true }); renderPromptList(ownPrompts, "No prompts yet.", { showEdit: true });
} }
} }
function updateProfileMode() { function updateProfileMode() {
if (isOwnProfile) { if (isOwnProfile) {
profileActions.style.display = 'flex'; profileActions.style.display = "flex";
primaryProfileButton.textContent = 'Edit Profile'; primaryProfileButton.textContent = "Edit Profile";
primaryProfileButton.disabled = false; primaryProfileButton.disabled = false;
primaryProfileButton.onclick = () => location.href = 'settings.html'; primaryProfileButton.onclick = () =>
profileTabs.style.display = 'flex'; (location.href = "settings.html");
profileTabs.style.display = "flex";
return; return;
} }
profileActions.style.display = 'flex'; profileActions.style.display = "flex";
primaryProfileButton.textContent = currentIsFollowing ? 'Following' : 'Follow'; primaryProfileButton.textContent = currentIsFollowing
? "Following"
: "Follow";
primaryProfileButton.disabled = false; primaryProfileButton.disabled = false;
primaryProfileButton.onclick = toggleProfileFollow; primaryProfileButton.onclick = toggleProfileFollow;
favoritesTab.style.display = 'none'; favoritesTab.style.display = "none";
savedTab.style.display = 'none'; savedTab.style.display = "none";
myPromptsTab.textContent = `Prompts (${profilePrompts.length})`; myPromptsTab.textContent = `Prompts (${profilePrompts.length})`;
renderPromptList(profilePrompts, 'No prompts yet.'); renderPromptList(profilePrompts, "No prompts yet.");
} }
async function loadFollowState() { async function loadFollowState() {
if (isOwnProfile || !profileId) return; if (isOwnProfile || !profileId) return;
try { try {
const response = await fetch(`/api/v1/subscriptions/${encodeURIComponent(profileId)}`, { const response = await fetch(
credentials: 'same-origin' `/api/v1/subscriptions/${encodeURIComponent(profileId)}`,
}); {
credentials: "same-origin",
},
);
if (response.status === 401) { if (response.status === 401) {
location.href = '/login'; location.href = "/login";
return; return;
} }
@ -305,35 +379,46 @@
if (!profileId) return; if (!profileId) return;
primaryProfileButton.disabled = true; primaryProfileButton.disabled = true;
const response = await fetch(`/api/v1/subscriptions/${encodeURIComponent(profileId)}`, { const response = await fetch(
method: currentIsFollowing ? 'DELETE' : 'PUT', `/api/v1/subscriptions/${encodeURIComponent(profileId)}`,
credentials: 'same-origin' {
}); method: currentIsFollowing ? "DELETE" : "PUT",
credentials: "same-origin",
},
);
if (response.status === 401) { if (response.status === 401) {
location.href = '/login'; location.href = "/login";
return; return;
} }
if (response.ok) { if (response.ok) {
const currentSubscribers = Number(profileSubscribers.textContent || 0); const currentSubscribers = Number(
profileSubscribers.textContent || 0,
);
currentIsFollowing = !currentIsFollowing; currentIsFollowing = !currentIsFollowing;
profileSubscribers.textContent = Math.max(0, currentSubscribers + (currentIsFollowing ? 1 : -1)); profileSubscribers.textContent = Math.max(
0,
currentSubscribers + (currentIsFollowing ? 1 : -1),
);
updateProfileMode(); updateProfileMode();
} else { } else {
primaryProfileButton.disabled = false; primaryProfileButton.disabled = false;
} }
} }
shareProfileButton.addEventListener('click', async () => { shareProfileButton.addEventListener("click", async () => {
const url = isOwnProfile const url = isOwnProfile
? `${location.origin}/profile.html` ? `${location.origin}/profile.html`
: `${location.origin}/profile.html?id=${encodeURIComponent(profileId)}`; : `${location.origin}/profile.html?id=${encodeURIComponent(profileId)}`;
try { try {
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(url);
shareProfileButton.textContent = 'Copied'; shareProfileButton.textContent = "Copied";
setTimeout(() => shareProfileButton.textContent = 'Share Profile', 1200); setTimeout(
() => (shareProfileButton.textContent = "Share Profile"),
1200,
);
} catch { } catch {
location.href = url; location.href = url;
} }
@ -341,29 +426,32 @@
async function loadOwnPrompts() { async function loadOwnPrompts() {
try { try {
const response = await fetch('/api/v1/prompts/mine'); const response = await fetch("/api/v1/prompts/mine");
if (response.status === 401) { if (response.status === 401) {
location.href = '/login'; location.href = "/login";
return; return;
} }
if (!response.ok) throw new Error('Prompts could not be loaded.'); if (!response.ok) throw new Error("Prompts could not be loaded.");
ownPrompts = await response.json(); ownPrompts = await response.json();
updateTabs(); updateTabs();
} catch (error) { } catch (error) {
profilePromptsGrid.innerHTML = `<div style="grid-column:1/-1;color:#ef4444;text-align:center;padding:28px;">${error.message}</div>`; profilePromptsGrid.innerHTML = `<div class="profile-grid-error">${error.message}</div>`;
} }
} }
async function loadAllPromptReferences() { async function loadAllPromptReferences() {
try { try {
const response = await fetch('/api/v1/prompts?limit=100'); const response = await fetch("/api/v1/prompts?limit=100");
if (!response.ok) return; if (!response.ok) return;
allPrompts = await response.json(); allPrompts = await response.json();
if (isOwnProfile) { if (isOwnProfile) {
updateTabs(); updateTabs();
} else { } else {
profilePrompts = allPrompts.filter((prompt) => prompt.creatorId?.toLowerCase() === profileId.toLowerCase()); profilePrompts = allPrompts.filter(
(prompt) =>
prompt.creatorId?.toLowerCase() === profileId.toLowerCase(),
);
renderProfileFromPrompt(profilePrompts[0]); renderProfileFromPrompt(profilePrompts[0]);
updateProfileMode(); updateProfileMode();
} }
@ -372,8 +460,8 @@
} }
} }
document.querySelectorAll('.profile-tab').forEach((tab) => { document.querySelectorAll(".profile-tab").forEach((tab) => {
tab.addEventListener('click', () => { tab.addEventListener("click", () => {
if (!isOwnProfile) { if (!isOwnProfile) {
updateProfileMode(); updateProfileMode();
return; return;

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"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head> </head>
<body> <body>
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);"> <div class="layout">
<div id="sidebar-container"></div> <div id="sidebar-container"></div>
<div style="flex:1; display: flex; flex-direction: column;"> <div class="page-body">
<div id="topbar-container"></div> <div id="topbar-container"></div>
@ -68,7 +68,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="save-btn">Save Changes</button> <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> </div>
</form> </form>
</div> </div>