inline css entfernen
This commit is contained in:
parent
e6d54d693f
commit
7a347b093e
@ -1,130 +1,175 @@
|
||||
<!-- OnlyPrompt - Chats page:
|
||||
- Direct messaging interface with conversation list and active chat window -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OnlyPrompt - Chats</title>
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
<link rel="stylesheet" href="../css/base.css">
|
||||
<link rel="stylesheet" href="../css/sidebar.css">
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/chats.css">
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OnlyPrompt - Chats</title>
|
||||
<link rel="stylesheet" href="../css/variables.css" />
|
||||
<link rel="stylesheet" href="../css/base.css" />
|
||||
<link rel="stylesheet" href="../css/sidebar.css" />
|
||||
<link rel="stylesheet" href="../css/login.css" />
|
||||
<link rel="stylesheet" href="../css/topbar.css" />
|
||||
<link rel="stylesheet" href="../css/chats.css" />
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex:1; display: flex; flex-direction: column;">
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="chats-main">
|
||||
<!-- Chat Container: Left column (list) + Right column (active chat) -->
|
||||
<div class="chat-container">
|
||||
|
||||
<!-- Left Column: Chat Overview -->
|
||||
<div class="chat-list">
|
||||
<div class="chat-list-header">
|
||||
<h2>Messages</h2>
|
||||
<button class="new-chat-btn"><i class="bi bi-pencil-square"></i></button>
|
||||
<main class="chats-main">
|
||||
<!-- Chat Container: Left column (list) + Right column (active chat) -->
|
||||
<div class="chat-container">
|
||||
<!-- Left Column: Chat Overview -->
|
||||
<div class="chat-list">
|
||||
<div class="chat-list-header">
|
||||
<h2>Messages</h2>
|
||||
<button class="new-chat-btn">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chat-list-items">
|
||||
<!-- Chat Entry 1 (active) -->
|
||||
<div class="chat-item active">
|
||||
<img
|
||||
src="../images/content/creator2.png"
|
||||
alt="Alex Chen"
|
||||
class="chat-avatar"
|
||||
/>
|
||||
<div class="chat-item-info">
|
||||
<div class="chat-name">Alex Chen</div>
|
||||
<div class="chat-last-msg">
|
||||
Hey Sarah! Really loved your last video on minimalism...
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-time">10:17 AM</div>
|
||||
</div>
|
||||
<!-- Chat Entry 2 -->
|
||||
<div class="chat-item">
|
||||
<img
|
||||
src="../images/content/creator3.png"
|
||||
alt="Mia Wong"
|
||||
class="chat-avatar"
|
||||
/>
|
||||
<div class="chat-item-info">
|
||||
<div class="chat-name">Mia Wong</div>
|
||||
<div class="chat-last-msg">
|
||||
Thanks for the prompt tips! They worked perfectly.
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-time">Yesterday</div>
|
||||
</div>
|
||||
<!-- Chat Entry 3 -->
|
||||
<div class="chat-item">
|
||||
<img
|
||||
src="../images/content/creator4.png"
|
||||
alt="Tom Rivera"
|
||||
class="chat-avatar"
|
||||
/>
|
||||
<div class="chat-item-info">
|
||||
<div class="chat-name">Tom Rivera</div>
|
||||
<div class="chat-last-msg">
|
||||
Let's schedule a call for the collab?
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-time">Yesterday</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-list-items">
|
||||
<!-- Chat Entry 1 (active) -->
|
||||
<div class="chat-item active">
|
||||
<img src="../images/content/creator2.png" alt="Alex Chen" class="chat-avatar">
|
||||
<div class="chat-item-info">
|
||||
<div class="chat-name">Alex Chen</div>
|
||||
<div class="chat-last-msg">Hey Sarah! Really loved your last video on minimalism...</div>
|
||||
|
||||
<!-- Right Column: Active Chat (with Alex Chen) -->
|
||||
<div class="chat-active">
|
||||
<div class="chat-header">
|
||||
<img
|
||||
src="../images/content/creator2.png"
|
||||
alt="Alex Chen"
|
||||
class="chat-avatar-large"
|
||||
/>
|
||||
<div class="chat-header-info">
|
||||
<div class="chat-header-name">Alex Chen</div>
|
||||
<div class="chat-header-status">
|
||||
<span class="online-dot"></span> Online
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-time">10:17 AM</div>
|
||||
</div>
|
||||
<!-- Chat Entry 2 -->
|
||||
<div class="chat-item">
|
||||
<img src="../images/content/creator3.png" alt="Mia Wong" class="chat-avatar">
|
||||
<div class="chat-item-info">
|
||||
<div class="chat-name">Mia Wong</div>
|
||||
<div class="chat-last-msg">Thanks for the prompt tips! They worked perfectly.</div>
|
||||
<div class="chat-messages">
|
||||
<!-- Message from Alex -->
|
||||
<div class="message received">
|
||||
<div class="message-bubble">
|
||||
Hey Sarah! Really loved your last video on minimalism. Quick
|
||||
question about your workspace layout?
|
||||
</div>
|
||||
<div class="message-time">10:15 AM</div>
|
||||
</div>
|
||||
<!-- Reply from Sarah -->
|
||||
<div class="message sent">
|
||||
<div class="message-bubble">
|
||||
Thanks Alex! Appreciate it. Yes, happy to share! The desk is
|
||||
from Article, and the shelving unit is custom-built. Highly
|
||||
recommend a clean setup!
|
||||
</div>
|
||||
<div class="message-time">10:16 AM</div>
|
||||
</div>
|
||||
<!-- Alex replies -->
|
||||
<div class="message received">
|
||||
<div class="message-bubble">
|
||||
Thanks so much! Your aesthetic is exactly what I'm aiming
|
||||
for. Can't wait for your next piece!
|
||||
</div>
|
||||
<div class="message-time">10:17 AM</div>
|
||||
</div>
|
||||
<!-- Sarah replies -->
|
||||
<div class="message sent">
|
||||
<div class="message-bubble">
|
||||
Awesome! Let me know if you need more tips. Enjoy the
|
||||
process! 😊
|
||||
</div>
|
||||
<div class="message-time">10:18 AM</div>
|
||||
</div>
|
||||
<div class="chat-time">Yesterday</div>
|
||||
</div>
|
||||
<!-- Chat Entry 3 -->
|
||||
<div class="chat-item">
|
||||
<img src="../images/content/creator4.png" alt="Tom Rivera" class="chat-avatar">
|
||||
<div class="chat-item-info">
|
||||
<div class="chat-name">Tom Rivera</div>
|
||||
<div class="chat-last-msg">Let's schedule a call for the collab?</div>
|
||||
</div>
|
||||
<div class="chat-time">Yesterday</div>
|
||||
<div class="chat-input-area">
|
||||
<input type="text" placeholder="Type your message..." />
|
||||
<button class="send-btn">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Active Chat (with Alex Chen) -->
|
||||
<div class="chat-active">
|
||||
<div class="chat-header">
|
||||
<img src="../images/content/creator2.png" alt="Alex Chen" class="chat-avatar-large">
|
||||
<div class="chat-header-info">
|
||||
<div class="chat-header-name">Alex Chen</div>
|
||||
<div class="chat-header-status"><span class="online-dot"></span> Online</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-messages">
|
||||
<!-- Message from Alex -->
|
||||
<div class="message received">
|
||||
<div class="message-bubble">Hey Sarah! Really loved your last video on minimalism. Quick question about your workspace layout?</div>
|
||||
<div class="message-time">10:15 AM</div>
|
||||
</div>
|
||||
<!-- Reply from Sarah -->
|
||||
<div class="message sent">
|
||||
<div class="message-bubble">Thanks Alex! Appreciate it. Yes, happy to share! The desk is from Article, and the shelving unit is custom-built. Highly recommend a clean setup!</div>
|
||||
<div class="message-time">10:16 AM</div>
|
||||
</div>
|
||||
<!-- Alex replies -->
|
||||
<div class="message received">
|
||||
<div class="message-bubble">Thanks so much! Your aesthetic is exactly what I'm aiming for. Can't wait for your next piece!</div>
|
||||
<div class="message-time">10:17 AM</div>
|
||||
</div>
|
||||
<!-- Sarah replies -->
|
||||
<div class="message sent">
|
||||
<div class="message-bubble">Awesome! Let me know if you need more tips. Enjoy the process! 😊</div>
|
||||
<div class="message-time">10:18 AM</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input-area">
|
||||
<input type="text" placeholder="Type your message...">
|
||||
<button class="send-btn">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
fetch('/sidebar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
document.getElementById('sidebar-container').innerHTML = data;
|
||||
// Remove 'active' from all sidebar links
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
<script>
|
||||
fetch("/sidebar.html")
|
||||
.then((r) => r.text())
|
||||
.then((data) => {
|
||||
document.getElementById("sidebar-container").innerHTML = data;
|
||||
// Remove 'active' from all sidebar links
|
||||
document
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((link) => {
|
||||
link.classList.remove("active");
|
||||
});
|
||||
// Set 'active' on the Chats link (4th link, index 3)
|
||||
const chatsLink = document.querySelectorAll(
|
||||
"#sidebar-container .sidebar li a",
|
||||
)[3];
|
||||
if (chatsLink) chatsLink.classList.add("active");
|
||||
});
|
||||
// Set 'active' on the Chats link (4th link, index 3)
|
||||
const chatsLink = document.querySelectorAll('#sidebar-container .sidebar li a')[3];
|
||||
if (chatsLink) chatsLink.classList.add('active');
|
||||
});
|
||||
|
||||
fetch('/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
</script>
|
||||
</body>
|
||||
fetch("/topbar.html")
|
||||
.then((r) => r.text())
|
||||
.then(
|
||||
(data) =>
|
||||
(document.getElementById("topbar-container").innerHTML = data),
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,204 +1,225 @@
|
||||
<!-- OnlyPrompt - Community page:
|
||||
- Discover creators, follow/unfollow, dynamic via API -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OnlyPrompt - Discover Creators</title>
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
<link rel="stylesheet" href="../css/base.css">
|
||||
<link rel="stylesheet" href="../css/sidebar.css">
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/community.css">
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OnlyPrompt - Discover Creators</title>
|
||||
<link rel="stylesheet" href="../css/variables.css" />
|
||||
<link rel="stylesheet" href="../css/base.css" />
|
||||
<link rel="stylesheet" href="../css/sidebar.css" />
|
||||
<link rel="stylesheet" href="../css/login.css" />
|
||||
<link rel="stylesheet" href="../css/topbar.css" />
|
||||
<link rel="stylesheet" href="../css/community.css" />
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<div style="flex:1; margin:40px auto; max-width:950px;">
|
||||
<main class="creators-main">
|
||||
<div class="creators-header">
|
||||
<h1>Discover Creators</h1>
|
||||
<p>Follow your favorite prompt artists and get inspired.</p>
|
||||
</div>
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
<div class="filter-buttons">
|
||||
<button class="filter-btn active" data-sort="popular">
|
||||
Popular
|
||||
</button>
|
||||
<button class="filter-btn" data-sort="prompts">Rising</button>
|
||||
<button class="filter-btn" data-sort="new">New</button>
|
||||
<button class="filter-btn" data-sort="rating">Top Rated</button>
|
||||
</div>
|
||||
|
||||
<main class="creators-main">
|
||||
<div class="creators-grid" id="creators-grid"></div>
|
||||
|
||||
<div class="creators-header">
|
||||
<h1>Discover Creators</h1>
|
||||
<p>Follow your favorite prompt artists and get inspired.</p>
|
||||
</div>
|
||||
<div id="creators-empty" class="state-empty">
|
||||
<i class="bi bi-people state-icon"></i>
|
||||
<h3 id="creators-empty-title" class="state-title">
|
||||
No creators found
|
||||
</h3>
|
||||
<p id="creators-empty-text">
|
||||
Check back later for new creators to follow.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="filter-buttons">
|
||||
<button class="filter-btn active" data-sort="popular">Popular</button>
|
||||
<button class="filter-btn" data-sort="prompts">Rising</button>
|
||||
<button class="filter-btn" data-sort="new">New</button>
|
||||
<button class="filter-btn" data-sort="rating">Top Rated</button>
|
||||
</div>
|
||||
|
||||
<div class="creators-grid" id="creators-grid"></div>
|
||||
|
||||
<div id="creators-empty" style="display:none; text-align:center; padding:60px 20px; color:#64748b;">
|
||||
<i class="bi bi-people" style="font-size:3rem; display:block; margin-bottom:16px;"></i>
|
||||
<h3 id="creators-empty-title" style="margin-bottom:8px;">No creators found</h3>
|
||||
<p id="creators-empty-text">Check back later for new creators to follow.</p>
|
||||
</div>
|
||||
|
||||
<div id="creators-error" style="display:none; text-align:center; padding:60px 20px; color:#ef4444;">
|
||||
<i class="bi bi-exclamation-circle" style="font-size:3rem; display:block; margin-bottom:16px;"></i>
|
||||
<h3 style="margin-bottom:8px;">Could not load creators</h3>
|
||||
<p id="creators-error-msg"></p>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<div id="creators-error" class="state-error">
|
||||
<i class="bi bi-exclamation-circle state-icon"></i>
|
||||
<h3 class="state-title">Could not load creators</h3>
|
||||
<p id="creators-error-msg"></p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
// ── Sidebar & Topbar ─────────────────────────────────────────────
|
||||
fetch('/sidebar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
document.getElementById('sidebar-container').innerHTML = data;
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(l => l.classList.remove('active'));
|
||||
const thirdLink = document.querySelectorAll('#sidebar-container .sidebar li a')[2];
|
||||
if (thirdLink) thirdLink.classList.add('active');
|
||||
});
|
||||
fetch('/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
<script type="module">
|
||||
// ── Sidebar & Topbar ─────────────────────────────────────────────
|
||||
fetch("/sidebar.html")
|
||||
.then((r) => r.text())
|
||||
.then((data) => {
|
||||
document.getElementById("sidebar-container").innerHTML = data;
|
||||
document
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((l) => l.classList.remove("active"));
|
||||
const thirdLink = document.querySelectorAll(
|
||||
"#sidebar-container .sidebar li a",
|
||||
)[2];
|
||||
if (thirdLink) thirdLink.classList.add("active");
|
||||
});
|
||||
fetch("/topbar.html")
|
||||
.then((r) => r.text())
|
||||
.then(
|
||||
(data) =>
|
||||
(document.getElementById("topbar-container").innerHTML = data),
|
||||
);
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
function renderStars(rating) {
|
||||
if (!rating) return '';
|
||||
const stars = Math.round(rating);
|
||||
return `<span style="color:#f59e0b">${'★'.repeat(stars)}${'☆'.repeat(5 - stars)}</span> <span style="color:#64748b;font-size:0.8rem">${rating.toFixed(1)}</span>`;
|
||||
}
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
function renderStars(rating) {
|
||||
if (!rating) return "";
|
||||
const stars = Math.round(rating);
|
||||
return `<span class="creator-stars">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> <span class="creator-stars-value">${rating.toFixed(1)}</span>`;
|
||||
}
|
||||
|
||||
function renderCard(c) {
|
||||
return `
|
||||
function renderCard(c) {
|
||||
return `
|
||||
<div class="creator-card">
|
||||
<img class="creator-avatar"
|
||||
src="${c.avatarUrl || '../images/content/cat.png'}"
|
||||
src="${c.avatarUrl || "../images/content/cat.png"}"
|
||||
alt="${c.displayName}"
|
||||
style="cursor:pointer"
|
||||
onclick="location.href='/profile?id=${c.userId}'">
|
||||
<div class="creator-info">
|
||||
<h3 class="creator-name"
|
||||
style="cursor:pointer"
|
||||
onclick="location.href='/profile?id=${c.userId}'">${c.displayName}</h3>
|
||||
<div class="creator-handle">@${c.slug}</div>
|
||||
<p class="creator-bio">${c.bio ?? 'No bio yet.'}</p>
|
||||
<p class="creator-bio">${c.bio ?? "No bio yet."}</p>
|
||||
<div class="creator-stats">
|
||||
<span><i class="bi bi-puzzle"></i> ${c.promptCount} prompts</span>
|
||||
<span><i class="bi bi-people"></i> ${c.subscribers}</span>
|
||||
${c.averageRating > 0 ? `<span>${renderStars(c.averageRating)}</span>` : ''}
|
||||
${c.averageRating > 0 ? `<span>${renderStars(c.averageRating)}</span>` : ""}
|
||||
</div>
|
||||
<button class="follow-btn ${c.isFollowing ? 'following' : ''}"
|
||||
<button class="follow-btn ${c.isFollowing ? "following" : ""}"
|
||||
data-userid="${c.userId}"
|
||||
data-following="${c.isFollowing}">
|
||||
${c.isFollowing ? 'Following' : 'Follow'}
|
||||
${c.isFollowing ? "Following" : "Follow"}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Follow / Unfollow ────────────────────────────────────────────
|
||||
async function toggleFollow(btn) {
|
||||
const userId = btn.dataset.userid;
|
||||
const isFollowing = btn.dataset.following === 'true';
|
||||
btn.disabled = true;
|
||||
|
||||
const res = await fetch(`/api/v1/subscriptions/${userId}`, {
|
||||
method: isFollowing ? 'DELETE' : 'PUT',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (res.status === 401) { location.href = '/login'; return; }
|
||||
|
||||
if (res.ok) {
|
||||
const nowFollowing = !isFollowing;
|
||||
btn.dataset.following = nowFollowing;
|
||||
btn.textContent = nowFollowing ? 'Following' : 'Follow';
|
||||
btn.classList.toggle('following', nowFollowing);
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
}
|
||||
// ── Follow / Unfollow ────────────────────────────────────────────
|
||||
async function toggleFollow(btn) {
|
||||
const userId = btn.dataset.userid;
|
||||
const isFollowing = btn.dataset.following === "true";
|
||||
btn.disabled = true;
|
||||
|
||||
// ── Load Creators ────────────────────────────────────────────────
|
||||
const grid = document.getElementById('creators-grid');
|
||||
const emptyEl = document.getElementById('creators-empty');
|
||||
const emptyTitle = document.getElementById('creators-empty-title');
|
||||
const emptyText = document.getElementById('creators-empty-text');
|
||||
const errorEl = document.getElementById('creators-error');
|
||||
const errorMsg = document.getElementById('creators-error-msg');
|
||||
|
||||
let activeSort = 'popular';
|
||||
let currentSearch = new URLSearchParams(location.search).get('search') || '';
|
||||
|
||||
function getSearchTerm() {
|
||||
return currentSearch.trim();
|
||||
}
|
||||
|
||||
async function loadCreators(sort = activeSort) {
|
||||
activeSort = sort;
|
||||
grid.innerHTML = '';
|
||||
emptyEl.style.display = 'none';
|
||||
errorEl.style.display = 'none';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
sort,
|
||||
limit: '50'
|
||||
const res = await fetch(`/api/v1/subscriptions/${userId}`, {
|
||||
method: isFollowing ? "DELETE" : "PUT",
|
||||
credentials: "same-origin",
|
||||
});
|
||||
const search = getSearchTerm();
|
||||
if (search) params.set('search', search);
|
||||
|
||||
const res = await fetch(`/api/v1/profiles?${params}`);
|
||||
if (res.status === 401) { location.href = '/login'; return; }
|
||||
if (!res.ok) throw new Error(`Server error ${res.status}`);
|
||||
|
||||
const creators = await res.json();
|
||||
if (creators.length === 0) {
|
||||
const search = getSearchTerm();
|
||||
emptyTitle.textContent = search ? 'No matching creators' : 'No creators found';
|
||||
emptyText.textContent = search
|
||||
? `No creator matches "${search}". Try another name or clear the search.`
|
||||
: 'Create another local user to see creators here.';
|
||||
emptyEl.style.display = 'block';
|
||||
if (res.status === 401) {
|
||||
location.href = "/login";
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = creators.map(renderCard).join('');
|
||||
if (res.ok) {
|
||||
const nowFollowing = !isFollowing;
|
||||
btn.dataset.following = nowFollowing;
|
||||
btn.textContent = nowFollowing ? "Following" : "Follow";
|
||||
btn.classList.toggle("following", nowFollowing);
|
||||
}
|
||||
|
||||
grid.querySelectorAll('.follow-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => toggleFollow(btn));
|
||||
});
|
||||
} catch (e) {
|
||||
errorEl.style.display = 'block';
|
||||
errorMsg.textContent = e.message;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter buttons ───────────────────────────────────────────────
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
loadCreators(btn.dataset.sort);
|
||||
// ── Load Creators ────────────────────────────────────────────────
|
||||
const grid = document.getElementById("creators-grid");
|
||||
const emptyEl = document.getElementById("creators-empty");
|
||||
const emptyTitle = document.getElementById("creators-empty-title");
|
||||
const emptyText = document.getElementById("creators-empty-text");
|
||||
const errorEl = document.getElementById("creators-error");
|
||||
const errorMsg = document.getElementById("creators-error-msg");
|
||||
|
||||
let activeSort = "popular";
|
||||
let currentSearch =
|
||||
new URLSearchParams(location.search).get("search") || "";
|
||||
|
||||
function getSearchTerm() {
|
||||
return currentSearch.trim();
|
||||
}
|
||||
|
||||
async function loadCreators(sort = activeSort) {
|
||||
activeSort = sort;
|
||||
grid.innerHTML = "";
|
||||
emptyEl.style.display = "none";
|
||||
errorEl.style.display = "none";
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
sort,
|
||||
limit: "50",
|
||||
});
|
||||
const search = getSearchTerm();
|
||||
if (search) params.set("search", search);
|
||||
|
||||
const res = await fetch(`/api/v1/profiles?${params}`);
|
||||
if (res.status === 401) {
|
||||
location.href = "/login";
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error(`Server error ${res.status}`);
|
||||
|
||||
const creators = await res.json();
|
||||
if (creators.length === 0) {
|
||||
const search = getSearchTerm();
|
||||
emptyTitle.textContent = search
|
||||
? "No matching creators"
|
||||
: "No creators found";
|
||||
emptyText.textContent = search
|
||||
? `No creator matches "${search}". Try another name or clear the search.`
|
||||
: "Create another local user to see creators here.";
|
||||
emptyEl.style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = creators.map(renderCard).join("");
|
||||
|
||||
grid.querySelectorAll(".follow-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => toggleFollow(btn));
|
||||
});
|
||||
} catch (e) {
|
||||
errorEl.style.display = "block";
|
||||
errorMsg.textContent = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter buttons ───────────────────────────────────────────────
|
||||
document.querySelectorAll(".filter-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
document
|
||||
.querySelectorAll(".filter-btn")
|
||||
.forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
loadCreators(btn.dataset.sort);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
window.applyCreatorSearch = (value) => {
|
||||
currentSearch = value.trim();
|
||||
loadCreators(activeSort);
|
||||
};
|
||||
window.applyCreatorSearch = (value) => {
|
||||
currentSearch = value.trim();
|
||||
loadCreators(activeSort);
|
||||
};
|
||||
|
||||
loadCreators();
|
||||
</script>
|
||||
</body>
|
||||
loadCreators();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -17,11 +17,11 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
||||
<div class="layout">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex:1; display: flex; flex-direction: column;">
|
||||
<div class="page-body">
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
@ -76,8 +76,8 @@
|
||||
<label for="exampleImage">Example Image (optional)</label>
|
||||
<input type="file" id="exampleImage" name="exampleImage" accept="image/png, image/jpeg, image/jpg">
|
||||
<small class="form-hint">Upload a PNG or JPG – preview will appear below.</small>
|
||||
<div id="imagePreview" style="margin-top: 10px; display: none;">
|
||||
<img id="previewImg" src="#" alt="Preview" style="max-width: 100%; max-height: 200px; border-radius: 12px;">
|
||||
<div id="imagePreview">
|
||||
<img id="previewImg" src="#" alt="Preview">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
<button type="button" id="freeBtn" class="price-option active">Free</button>
|
||||
<button type="button" id="paidBtn" class="price-option">Paid</button>
|
||||
</div>
|
||||
<div id="priceField" style="display: none;">
|
||||
<div id="priceField">
|
||||
<input type="number" id="price" name="price" step="0.01" min="0" placeholder="Price in USD (e.g., 19.99)">
|
||||
</div>
|
||||
<small class="form-hint">You can set a price later or keep it free.</small>
|
||||
@ -99,7 +99,7 @@
|
||||
<button type="submit" class="submit-btn" id="submitPromptBtn">Publish Prompt</button>
|
||||
<button type="button" class="cancel-btn">Cancel</button>
|
||||
</div>
|
||||
<p id="create-status" style="text-align:center;color:#64748b;margin:0;"></p>
|
||||
<p id="create-status"></p></p>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -28,7 +28,7 @@ body {
|
||||
.form-error ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
list-style: '*';
|
||||
list-style: "*";
|
||||
}
|
||||
|
||||
.form-error li {
|
||||
@ -38,4 +38,46 @@ body {
|
||||
.form-error li .error {
|
||||
color: red;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Layout ──────────────────────────────────────────────────────────── */
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* Main content area - flex child that fills remaining space */
|
||||
.page-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Reusable empty / error state components ─────────────────────────── */
|
||||
.state-empty,
|
||||
.state-error {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
.state-empty {
|
||||
color: #64748b;
|
||||
}
|
||||
.state-error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
font-size: 3rem;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.state-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@ -1,13 +1,5 @@
|
||||
/* Chats page - Two column layout: chat list + active chat window */
|
||||
|
||||
/* Full width layout */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.chats-main {
|
||||
flex: 1;
|
||||
padding: 20px 32px;
|
||||
@ -20,7 +12,7 @@
|
||||
gap: 24px;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
|
||||
overflow: hidden;
|
||||
height: calc(100vh - 120px); /* Adjust based on topbar height */
|
||||
min-height: 500px;
|
||||
@ -244,4 +236,4 @@
|
||||
.chat-active {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,5 @@
|
||||
/* Creators page - Discover creators, filter buttons, creator cards */
|
||||
|
||||
/* Full width layout */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.creators-main {
|
||||
background: transparent !important;
|
||||
padding: 20px 32px !important;
|
||||
@ -71,15 +63,17 @@
|
||||
.creator-card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
.creator-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
.creator-avatar {
|
||||
@ -177,4 +171,19 @@
|
||||
.follow-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Star rating in creator cards */
|
||||
.creator-stars {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.creator-stars-value {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Clickable elements */
|
||||
.creator-card .creator-avatar,
|
||||
.creator-card .creator-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -1,13 +1,5 @@
|
||||
/* Create page - Form for publishing new AI prompts */
|
||||
|
||||
/* Full width layout */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.create-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@ -22,12 +14,12 @@
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
|
||||
padding: 32px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.create-container:hover {
|
||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
@ -77,7 +69,7 @@
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 3px rgba(124,58,237,0.1);
|
||||
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
|
||||
}
|
||||
.form-hint {
|
||||
font-size: 0.75rem;
|
||||
@ -106,6 +98,25 @@
|
||||
}
|
||||
#priceField {
|
||||
margin-top: 8px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Image preview */
|
||||
#imagePreview {
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
}
|
||||
#imagePreview img {
|
||||
max-width: 100%;
|
||||
max-height: 200px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Status message */
|
||||
#create-status {
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
@ -114,7 +125,8 @@
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.submit-btn, .cancel-btn {
|
||||
.submit-btn,
|
||||
.cancel-btn {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
@ -132,7 +144,8 @@
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
.submit-btn:hover, .cancel-btn:hover {
|
||||
.submit-btn:hover,
|
||||
.cancel-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@ -152,4 +165,4 @@
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,5 @@
|
||||
/* Feed page - Multi-column grid, square images, like/comment/save actions */
|
||||
|
||||
/* Full width layout */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.feed-main {
|
||||
background: transparent !important;
|
||||
padding: 20px 32px !important;
|
||||
@ -125,7 +117,7 @@
|
||||
.post-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 6px 0;
|
||||
margin: 10px 0 6px 0;
|
||||
}
|
||||
.post-description {
|
||||
color: #334155;
|
||||
|
||||
@ -1,13 +1,5 @@
|
||||
/* Marketplace Page - Prompt cards, filter buttons, full width layout */
|
||||
|
||||
/* Full width layout */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.marketplace-main {
|
||||
background: transparent !important;
|
||||
padding: 20px 32px !important;
|
||||
@ -251,3 +243,238 @@
|
||||
border-color: #6366f1;
|
||||
background: #f5f3ff;
|
||||
}
|
||||
|
||||
/* ── Payment Modal ──────────────────────────────────────────────────── */
|
||||
#payment-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.payment-modal {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
max-width: 460px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.payment-close-btn {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.payment-title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#pay-prompt-title {
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.payment-intro {
|
||||
color: #64748b;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.payment-method-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pay-crypto-icon {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.pay-method-price {
|
||||
margin-left: auto;
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.payment-disclaimer {
|
||||
margin-top: 20px;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#pay-step-2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment-back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6366f1;
|
||||
cursor: pointer;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#pay-crypto-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.payment-amount-text {
|
||||
color: #64748b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.payment-address-box {
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#pay-address {
|
||||
font-size: 0.85rem;
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.payment-copy-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #6366f1;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.payment-warning-text {
|
||||
font-size: 0.78rem;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.payment-info-box {
|
||||
background: #fef9c3;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 0.82rem;
|
||||
color: #92400e;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.payment-confirm-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
#pay-step-3 {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.payment-success-icon {
|
||||
font-size: 3.5rem;
|
||||
color: #10b981;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.payment-success-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.payment-success-desc {
|
||||
color: #64748b;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.payment-done-btn {
|
||||
padding: 12px 28px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.market-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.market-card-avatar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.95rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.market-card-time {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.market-card-rating {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.market-rating-none {
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.market-rating-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.market-rating-stars {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.buy-btn-locked {
|
||||
background: #ef4444 !important;
|
||||
}
|
||||
.buy-btn-unlocked {
|
||||
background: #10b981 !important;
|
||||
}
|
||||
.market-price-badge {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border-radius: 20px;
|
||||
padding: 4px 14px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.market-heart-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
.market-bookmark-icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.market-save-span {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.details-btn[disabled] {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@ -1,13 +1,5 @@
|
||||
/* Post Detail page - Full prompt view, rating, example output, unlock button */
|
||||
|
||||
/* Full width layout */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.post-detail-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@ -21,12 +13,12 @@
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
|
||||
padding: 32px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.post-detail-container:hover {
|
||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
@ -319,3 +311,198 @@
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Loading / error states ──────────────────────────────────────────── */
|
||||
#detail-loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Smaller state icons for this page */
|
||||
#detail-loading .state-icon,
|
||||
#detail-error .state-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#detail-error-msg {
|
||||
color: #64748b;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.detail-back-btn {
|
||||
margin-top: 20px;
|
||||
padding: 10px 24px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Detail body ─────────────────────────────────────────────────────── */
|
||||
#detail-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.detail-creator-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#creator-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#creator-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
#prompt-date {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.detail-actions-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#edit-prompt-btn {
|
||||
display: none;
|
||||
margin-left: 10px;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#prompt-body {
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
#example-section {
|
||||
display: none;
|
||||
}
|
||||
#example-output-text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#example-image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Locked section ──────────────────────────────────────────────────── */
|
||||
#locked-section {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.locked-icon {
|
||||
font-size: 2.5rem;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.locked-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.locked-desc {
|
||||
color: #64748b;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#locked-subscribe-btn {
|
||||
padding: 12px 28px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.detail-heart-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
.detail-bookmark-span {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.detail-bookmark-icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.detail-loading-text {
|
||||
color: #94a3b8;
|
||||
}
|
||||
.detail-error-text {
|
||||
color: #ef4444;
|
||||
}
|
||||
.rating-stars-display {
|
||||
font-size: 1.1rem;
|
||||
color: #f59e0b;
|
||||
}
|
||||
.rating-value {
|
||||
margin-left: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.rating-count {
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.rating-none {
|
||||
font-size: 0.9rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.tier-badge-paid {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border-radius: 20px;
|
||||
padding: 4px 14px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tier-badge-tier {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
border-radius: 20px;
|
||||
padding: 4px 14px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tier-badge-free {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border-radius: 20px;
|
||||
padding: 4px 14px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -1,14 +1,148 @@
|
||||
/* Profile Page - Full width layout, darker share button, responsive grid */
|
||||
|
||||
/* Force main content container to full width, remove centering and max-width */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
/* ── Profile header ──────────────────────────────────────────────────── */
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Inner spacing for the profile card */
|
||||
/* ── Profile avatar ──────────────────────────────────────────────────── */
|
||||
.profile-avatar {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ── Profile info column ─────────────────────────────────────────────── */
|
||||
.profile-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#profileDisplayName {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#profileSlug {
|
||||
color: #64748b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.profile-badge-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
#profileBio {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#profileSpecialities {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
#profileStats {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
color: #64748b;
|
||||
margin-top: 12px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
#profileStats strong {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
/* ── Profile actions column ──────────────────────────────────────────── */
|
||||
#profileActions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ── Profile tabs ────────────────────────────────────────────────────── */
|
||||
.profile-tabs {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
margin: 32px 0 18px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ── Prompts grid ────────────────────────────────────────────────────── */
|
||||
#profile-prompts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.profile-grid-loading {
|
||||
grid-column: 1 / -1;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
padding: 28px;
|
||||
}
|
||||
.profile-grid-empty {
|
||||
grid-column: 1 / -1;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
padding: 28px;
|
||||
}
|
||||
.profile-grid-error {
|
||||
grid-column: 1 / -1;
|
||||
color: #ef4444;
|
||||
text-align: center;
|
||||
padding: 28px;
|
||||
}
|
||||
.profile-prompt-card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.profile-prompt-img {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.profile-prompt-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.profile-prompt-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
.profile-prompt-desc {
|
||||
color: #64748b;
|
||||
margin-bottom: 8px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.profile-prompt-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: #64748b;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.profile-prompt-edit-btn {
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
border-radius: 10px;
|
||||
padding: 6px 10px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Inner spacing for the profile card ─────────────────────────────── */
|
||||
.profile-main {
|
||||
background: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
@ -16,7 +150,7 @@
|
||||
padding: 20px 32px !important;
|
||||
margin: 0 auto !important;
|
||||
width: 100%;
|
||||
max-width: 1600px; /* Limits content on very large screens, but still wide */
|
||||
max-width: 1600px; /* Limits content on very large screens, but still wide */
|
||||
}
|
||||
|
||||
/* Make prompts grid use more columns on large screens */
|
||||
@ -29,7 +163,7 @@
|
||||
|
||||
/* Share button: darker background and text */
|
||||
.profile-header button:last-child {
|
||||
background: #cbd5e1 !important; /* darker gray */
|
||||
background: #cbd5e1 !important; /* darker gray */
|
||||
color: #1e293b !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
@ -64,7 +198,7 @@
|
||||
/* Prompt cards: rounded corners */
|
||||
.profile-main section > div {
|
||||
border-radius: 18px !important;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
|
||||
}
|
||||
|
||||
/* Prompt images: rounded corners */
|
||||
@ -75,6 +209,9 @@
|
||||
/* Avatar remains round */
|
||||
.profile-avatar {
|
||||
border-radius: 50% !important;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* All outer containers stay square */
|
||||
|
||||
@ -1,12 +1,5 @@
|
||||
/* Settings page - tabs, form styling */
|
||||
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.settings-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@ -21,7 +14,7 @@
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
@ -101,7 +94,7 @@
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 3px rgba(124,58,237,0.1);
|
||||
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
|
||||
}
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
@ -181,4 +174,11 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Save status message */
|
||||
#profileSaveStatus {
|
||||
margin-top: 10px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@ -20,13 +20,10 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
class="layout"
|
||||
style="display: flex; min-height: 100vh; background: var(--bg)"
|
||||
>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex: 1; display: flex; flex-direction: column">
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="feed-main">
|
||||
@ -61,38 +58,16 @@
|
||||
<div class="posts-grid" id="posts-grid"></div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
id="feed-empty"
|
||||
style="
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #64748b;
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-inbox"
|
||||
style="font-size: 3rem; display: block; margin-bottom: 16px"
|
||||
></i>
|
||||
<h3 style="margin-bottom: 8px">No posts yet</h3>
|
||||
<div id="feed-empty" class="state-empty">
|
||||
<i class="bi bi-inbox state-icon"></i>
|
||||
<h3 class="state-title">No posts yet</h3>
|
||||
<p>Follow some creators to see their prompts here.</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div
|
||||
id="feed-error"
|
||||
style="
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #ef4444;
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-exclamation-circle"
|
||||
style="font-size: 3rem; display: block; margin-bottom: 16px"
|
||||
></i>
|
||||
<h3 style="margin-bottom: 8px">Could not load feed</h3>
|
||||
<div id="feed-error" class="state-error">
|
||||
<i class="bi bi-exclamation-circle state-icon"></i>
|
||||
<h3 class="state-title">Could not load feed</h3>
|
||||
<p id="feed-error-msg"></p>
|
||||
</div>
|
||||
</main>
|
||||
@ -163,33 +138,33 @@
|
||||
return `
|
||||
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='${profileUrl(prompt.creatorId)}'">
|
||||
<div class="post-header">
|
||||
<img class="post-avatar" src="${prompt.creatorAvatarUrl || '../images/content/cat.png'}" alt="${prompt.creatorName}">
|
||||
<img class="post-avatar" src="${prompt.creatorAvatarUrl || "../images/content/cat.png"}" alt="${prompt.creatorName}">
|
||||
<div class="post-author">
|
||||
<span class="post-name">${prompt.creatorName}</span>
|
||||
</div>
|
||||
<span class="post-date">${timeAgo(prompt.timeStamp)}</span>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
${prompt.exampleImageUrl ? `<img class="post-image${locked ? ' post-image-locked' : ''}" src="${prompt.exampleImageUrl}" alt="${prompt.title}">` : `<img class="post-image${locked ? ' post-image-locked' : ''}" src="${feedImg(prompt.id)}" alt="${prompt.title}">`}
|
||||
<h3 class="post-title" style="margin-top:10px">${prompt.title}</h3>
|
||||
<p class="post-description">${prompt.description || ''}</p>
|
||||
${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill"></i> ${prompt.tierName ?? 'Paid'} tier required</p>` : ''}
|
||||
${prompt.exampleImageUrl ? `<img class="post-image${locked ? " post-image-locked" : ""}" src="${prompt.exampleImageUrl}" alt="${prompt.title}">` : `<img class="post-image${locked ? " post-image-locked" : ""}" src="${feedImg(prompt.id)}" alt="${prompt.title}">`}
|
||||
<h3 class="post-title">${prompt.title}</h3>
|
||||
<p class="post-description">${prompt.description || ""}</p>
|
||||
${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill"></i> ${prompt.tierName ?? "Paid"} tier required</p>` : ""}
|
||||
${renderStars(prompt.averageRating)}
|
||||
</div>
|
||||
<div class="post-actions">
|
||||
<button class="action-btn like-btn ${liked ? 'active' : ''}" onclick="toggleLike(event, '${prompt.id}', ${liked})"><i class="bi ${liked ? 'bi-heart-fill' : 'bi-heart'}"></i> <span>Like (${prompt.likeCount || 0})</span></button>
|
||||
<button class="action-btn like-btn ${liked ? "active" : ""}" onclick="toggleLike(event, '${prompt.id}', ${liked})"><i class="bi ${liked ? "bi-heart-fill" : "bi-heart"}"></i> <span>Like (${prompt.likeCount || 0})</span></button>
|
||||
<button class="action-btn comment-btn" onclick="event.stopPropagation(); location.href='/post-detail?id=${prompt.id}#rating-section'"><i class="bi bi-chat"></i> <span>Review</span></button>
|
||||
<button class="action-btn share-btn" onclick="sharePrompt(event, '${prompt.id}')"><i class="bi bi-share"></i> <span>Share</span></button>
|
||||
<button class="action-btn save-btn ${saved ? 'active' : ''}" onclick="toggleSave(event, '${prompt.id}', ${saved})"><i class="bi ${saved ? 'bi-bookmark-fill' : 'bi-bookmark'}"></i> <span>Save (${prompt.saveCount || 0})</span></button>
|
||||
<button class="action-btn save-btn ${saved ? "active" : ""}" onclick="toggleSave(event, '${prompt.id}', ${saved})"><i class="bi ${saved ? "bi-bookmark-fill" : "bi-bookmark"}"></i> <span>Save (${prompt.saveCount || 0})</span></button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
window.toggleLike = async function(event, id, isLiked) {
|
||||
window.toggleLike = async function (event, id, isLiked) {
|
||||
event.stopPropagation();
|
||||
const response = await fetch(`/api/v1/prompts/${id}/likes`, {
|
||||
method: isLiked ? "DELETE" : "PUT",
|
||||
credentials: "same-origin"
|
||||
credentials: "same-origin",
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
@ -200,26 +175,28 @@
|
||||
if (!response.ok) return;
|
||||
loadFeed(
|
||||
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
|
||||
document.querySelector(".filter-btn.active")?.dataset.ascending === "true"
|
||||
document.querySelector(".filter-btn.active")?.dataset.ascending ===
|
||||
"true",
|
||||
);
|
||||
};
|
||||
|
||||
window.toggleFeedState = function(event, type, id) {
|
||||
window.toggleFeedState = function (event, type, id) {
|
||||
event.stopPropagation();
|
||||
const key = `prompt-${type}-${id}`;
|
||||
const next = localStorage.getItem(key) !== "true";
|
||||
localStorage.setItem(key, next);
|
||||
loadFeed(
|
||||
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
|
||||
document.querySelector(".filter-btn.active")?.dataset.ascending === "true"
|
||||
document.querySelector(".filter-btn.active")?.dataset.ascending ===
|
||||
"true",
|
||||
);
|
||||
};
|
||||
|
||||
window.toggleSave = async function(event, id, isSaved) {
|
||||
window.toggleSave = async function (event, id, isSaved) {
|
||||
event.stopPropagation();
|
||||
const response = await fetch(`/api/v1/prompts/${id}/saves`, {
|
||||
method: isSaved ? "DELETE" : "PUT",
|
||||
credentials: "same-origin"
|
||||
credentials: "same-origin",
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
@ -230,13 +207,16 @@
|
||||
if (!response.ok) return;
|
||||
loadFeed(
|
||||
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
|
||||
document.querySelector(".filter-btn.active")?.dataset.ascending === "true"
|
||||
document.querySelector(".filter-btn.active")?.dataset.ascending ===
|
||||
"true",
|
||||
);
|
||||
};
|
||||
|
||||
window.sharePrompt = function(event, id) {
|
||||
window.sharePrompt = function (event, id) {
|
||||
event.stopPropagation();
|
||||
navigator.clipboard.writeText(`${location.origin}/post-detail?id=${id}`);
|
||||
navigator.clipboard.writeText(
|
||||
`${location.origin}/post-detail?id=${id}`,
|
||||
);
|
||||
};
|
||||
|
||||
async function loadFeed(sortBy = "date", ascending = false) {
|
||||
|
||||
@ -60,13 +60,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
class="layout"
|
||||
style="display: flex; min-height: 100vh; background: var(--bg)"
|
||||
>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex: 1; display: flex; flex-direction: column">
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="marketplace-main">
|
||||
@ -100,38 +97,16 @@
|
||||
<div class="prompts-grid" id="prompts-grid"></div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
id="market-empty"
|
||||
style="
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #64748b;
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-bag-x"
|
||||
style="font-size: 3rem; display: block; margin-bottom: 16px"
|
||||
></i>
|
||||
<h3 style="margin-bottom: 8px">No prompts found</h3>
|
||||
<div id="market-empty" class="state-empty">
|
||||
<i class="bi bi-bag-x state-icon"></i>
|
||||
<h3 class="state-title">No prompts found</h3>
|
||||
<p>Try a different category or search term.</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div
|
||||
id="market-error"
|
||||
style="
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #ef4444;
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-exclamation-circle"
|
||||
style="font-size: 3rem; display: block; margin-bottom: 16px"
|
||||
></i>
|
||||
<h3 style="margin-bottom: 8px">Could not load prompts</h3>
|
||||
<div id="market-error" class="state-error">
|
||||
<i class="bi bi-exclamation-circle state-icon"></i>
|
||||
<h3 class="state-title">Could not load prompts</h3>
|
||||
<p id="market-error-msg"></p>
|
||||
</div>
|
||||
</main>
|
||||
@ -139,213 +114,83 @@
|
||||
</div>
|
||||
|
||||
<!-- ── Payment Modal ─────────────────────────────────────────────── -->
|
||||
<div
|
||||
id="payment-overlay"
|
||||
style="
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
max-width: 460px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>
|
||||
<button
|
||||
onclick="closePayment()"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
"
|
||||
>
|
||||
<div id="payment-overlay">
|
||||
<div class="payment-modal">
|
||||
<button onclick="closePayment()" class="payment-close-btn">
|
||||
✕
|
||||
</button>
|
||||
|
||||
<!-- Step 1: Choose method -->
|
||||
<div id="pay-step-1">
|
||||
<h2 style="margin-bottom: 4px">Subscribe to access</h2>
|
||||
<p
|
||||
id="pay-prompt-title"
|
||||
style="color: #6366f1; font-weight: 600; margin-bottom: 16px"
|
||||
></p>
|
||||
<p style="color: #64748b; margin-bottom: 24px">
|
||||
<h2 class="payment-title">Subscribe to access</h2>
|
||||
<p id="pay-prompt-title"></p>
|
||||
<p class="payment-intro">
|
||||
Choose a payment method to unlock this prompt:
|
||||
</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 12px">
|
||||
<div class="payment-method-list">
|
||||
<button class="pay-method-btn" onclick="selectCrypto('btc')">
|
||||
<span style="font-size: 1.4rem">₿</span> Bitcoin (BTC)
|
||||
<span
|
||||
id="price-btc"
|
||||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||||
></span>
|
||||
<span class="pay-crypto-icon">₿</span> Bitcoin (BTC)
|
||||
<span id="price-btc" class="pay-method-price"></span>
|
||||
</button>
|
||||
<button class="pay-method-btn" onclick="selectCrypto('eth')">
|
||||
<span style="font-size: 1.4rem">Ξ</span> Ethereum (ETH)
|
||||
<span
|
||||
id="price-eth"
|
||||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||||
></span>
|
||||
<span class="pay-crypto-icon">Ξ</span> Ethereum (ETH)
|
||||
<span id="price-eth" class="pay-method-price"></span>
|
||||
</button>
|
||||
<button class="pay-method-btn" onclick="selectCrypto('sol')">
|
||||
<span style="font-size: 1.4rem">◎</span> Solana (SOL)
|
||||
<span
|
||||
id="price-sol"
|
||||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||||
></span>
|
||||
<span class="pay-crypto-icon">◎</span> Solana (SOL)
|
||||
<span id="price-sol" class="pay-method-price"></span>
|
||||
</button>
|
||||
<button class="pay-method-btn" onclick="selectCrypto('usdt')">
|
||||
<span style="font-size: 1.4rem">₮</span> USDT (TRC-20)
|
||||
<span
|
||||
id="price-usdt"
|
||||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||||
></span>
|
||||
<span class="pay-crypto-icon">₮</span> USDT (TRC-20)
|
||||
<span id="price-usdt" class="pay-method-price"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
style="
|
||||
margin-top: 20px;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<p class="payment-disclaimer">
|
||||
<i class="bi bi-shield-lock-fill"></i> Payments are processed
|
||||
on-chain. No account needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Send payment -->
|
||||
<div id="pay-step-2" style="display: none">
|
||||
<button
|
||||
onclick="backToStep1()"
|
||||
style="
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6366f1;
|
||||
cursor: pointer;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.9rem;
|
||||
"
|
||||
>
|
||||
<div id="pay-step-2">
|
||||
<button onclick="backToStep1()" class="payment-back-btn">
|
||||
← Back
|
||||
</button>
|
||||
<h2 id="pay-crypto-title" style="margin-bottom: 8px"></h2>
|
||||
<p style="color: #64748b; margin-bottom: 8px">
|
||||
<h2 id="pay-crypto-title"></h2>
|
||||
<p class="payment-amount-text">
|
||||
Send exactly <strong id="pay-amount"></strong> to:
|
||||
</p>
|
||||
<div
|
||||
style="
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
"
|
||||
>
|
||||
<code
|
||||
id="pay-address"
|
||||
style="font-size: 0.85rem; word-break: break-all; flex: 1"
|
||||
></code>
|
||||
<div class="payment-address-box">
|
||||
<code id="pay-address"></code>
|
||||
<button
|
||||
onclick="copyAddress()"
|
||||
title="Copy"
|
||||
style="
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #6366f1;
|
||||
font-size: 1.1rem;
|
||||
"
|
||||
class="payment-copy-btn"
|
||||
>
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p style="font-size: 0.78rem; color: #94a3b8; margin-bottom: 20px">
|
||||
<p class="payment-warning-text">
|
||||
⚠️ Only send the exact amount. Payments are non-refundable.
|
||||
</p>
|
||||
<div
|
||||
style="
|
||||
background: #fef9c3;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 0.82rem;
|
||||
color: #92400e;
|
||||
margin-bottom: 20px;
|
||||
"
|
||||
>
|
||||
<div class="payment-info-box">
|
||||
<i class="bi bi-info-circle-fill"></i> After sending, click the
|
||||
button below to confirm. Access will be granted once the transaction
|
||||
is verified.
|
||||
</div>
|
||||
<button
|
||||
onclick="confirmPayment()"
|
||||
style="
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
<button onclick="confirmPayment()" class="payment-confirm-btn">
|
||||
I've sent the payment ✓
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Success -->
|
||||
<div
|
||||
id="pay-step-3"
|
||||
style="display: none; text-align: center; padding: 20px 0"
|
||||
>
|
||||
<i
|
||||
class="bi bi-check-circle-fill"
|
||||
style="
|
||||
font-size: 3.5rem;
|
||||
color: #10b981;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
"
|
||||
></i>
|
||||
<h2 style="margin-bottom: 8px">Payment received!</h2>
|
||||
<p style="color: #64748b; margin-bottom: 24px">
|
||||
<div id="pay-step-3">
|
||||
<i class="bi bi-check-circle-fill payment-success-icon"></i>
|
||||
<h2 class="payment-success-title">Payment received!</h2>
|
||||
<p class="payment-success-desc">
|
||||
Your access is being activated. This usually takes 1–2 minutes.
|
||||
</p>
|
||||
<button
|
||||
onclick="closePayment()"
|
||||
style="
|
||||
padding: 12px 28px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
<button onclick="closePayment()" class="payment-done-btn">
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
@ -387,22 +232,29 @@
|
||||
return `${Math.floor(h / 24)}d ago`;
|
||||
}
|
||||
|
||||
function renderStars(rating, reviewCount = 0, promptId = null, locked = false) {
|
||||
const target = promptId && !locked
|
||||
? ` onclick="location.href='/post-detail?id=${promptId}#rating-section'" title="View reviews" style="cursor:pointer;"`
|
||||
: "";
|
||||
function renderStars(
|
||||
rating,
|
||||
reviewCount = 0,
|
||||
promptId = null,
|
||||
locked = false,
|
||||
) {
|
||||
const target =
|
||||
promptId && !locked
|
||||
? ` onclick="location.href='/post-detail?id=${promptId}#rating-section'" title="View reviews" class="market-rating-clickable"`
|
||||
: "";
|
||||
if (rating == null)
|
||||
return `<span${target} style="color:#94a3b8;font-size:0.8rem;${promptId && !locked ? 'cursor:pointer;' : ''}">No reviews yet</span>`;
|
||||
return `<span${target} class="market-rating-none">No reviews yet</span>`;
|
||||
const stars = Math.round(rating);
|
||||
const label = reviewCount === 1 ? "review" : "reviews";
|
||||
return `<span class="prompt-rating"${target}><span style="color:#f59e0b">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> ${rating.toFixed(1)} (${reviewCount} ${label})</span>`;
|
||||
return `<span class="prompt-rating"${target}><span class="market-rating-stars">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> ${rating.toFixed(1)} (${reviewCount} ${label})</span>`;
|
||||
}
|
||||
|
||||
function promptPrice(prompt) {
|
||||
if (prompt.price != null && Number(prompt.price) > 0) {
|
||||
return `$${Number(prompt.price).toFixed(2)}`;
|
||||
}
|
||||
if (prompt.tierLevel) return `$${(prompt.tierLevel * 4.99).toFixed(2)}/mo`;
|
||||
if (prompt.tierLevel)
|
||||
return `$${(prompt.tierLevel * 4.99).toFixed(2)}/mo`;
|
||||
if (prompt.canAccess === false) return "Paid";
|
||||
return "Free";
|
||||
}
|
||||
@ -424,7 +276,9 @@
|
||||
const direction = ascending === "true" ? 1 : -1;
|
||||
return prompts
|
||||
.slice()
|
||||
.sort((a, b) => (getNumericPrice(a) - getNumericPrice(b)) * direction);
|
||||
.sort(
|
||||
(a, b) => (getNumericPrice(a) - getNumericPrice(b)) * direction,
|
||||
);
|
||||
}
|
||||
|
||||
return prompts;
|
||||
@ -443,29 +297,32 @@
|
||||
function renderCard(p) {
|
||||
const paid = p.price != null && Number(p.price) > 0;
|
||||
const locked = p.canAccess === false || paid || p.tierLevel != null;
|
||||
const img = p.exampleImageUrl || p._img || MARKET_IMAGES[cardIndex++ % MARKET_IMAGES.length];
|
||||
const img =
|
||||
p.exampleImageUrl ||
|
||||
p._img ||
|
||||
MARKET_IMAGES[cardIndex++ % MARKET_IMAGES.length];
|
||||
return `
|
||||
<div class="prompt-card">
|
||||
<img src="${img}" alt="${p.title}" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||||
<div style="width:34px;height:34px;border-radius:50%;background:#6366f1;color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center;font-size:0.95rem;flex-shrink:0;">${p.creatorName.charAt(0).toUpperCase()}</div>
|
||||
<div class="market-card-header">
|
||||
<div class="market-card-avatar">${p.creatorName.charAt(0).toUpperCase()}</div>
|
||||
<span class="prompt-author">@${p.creatorName}</span>
|
||||
<span style="margin-left:auto;font-size:0.75rem;color:#94a3b8;">${timeAgo(p.timeStamp)}</span>
|
||||
<span class="market-card-time">${timeAgo(p.timeStamp)}</span>
|
||||
</div>
|
||||
<h3 class="prompt-title">${p.title}</h3>
|
||||
<p class="prompt-description">${p.description || 'No description yet.'}</p>
|
||||
<div style="margin-bottom:12px;">${renderStars(p.averageRating, p.reviewCount || 0, p.id, locked)}</div>
|
||||
<p class="prompt-description">${p.description || "No description yet."}</p>
|
||||
<div class="market-card-rating">${renderStars(p.averageRating, p.reviewCount || 0, p.id, locked)}</div>
|
||||
<div class="prompt-price">${promptPrice(p)}</div>
|
||||
<div class="prompt-actions">
|
||||
${
|
||||
locked
|
||||
? `<button class="buy-btn" style="background:#ef4444;" onclick='openPayment(${JSON.stringify(p)})'><i class="bi bi-lock-fill"></i> Pay</button>`
|
||||
: `<button class="buy-btn" style="background:#10b981;" onclick="location.href='/post-detail?id=${p.id}'">Access <i class="bi bi-unlock-fill"></i></button>`
|
||||
? `<button class="buy-btn buy-btn-locked" onclick='openPayment(${JSON.stringify(p)})'><i class="bi bi-lock-fill"></i> Pay</button>`
|
||||
: `<button class="buy-btn buy-btn-unlocked" onclick="location.href='/post-detail?id=${p.id}'">Access <i class="bi bi-unlock-fill"></i></button>`
|
||||
}
|
||||
${
|
||||
locked
|
||||
? `<button class="details-btn" disabled style="opacity:.45;cursor:not-allowed;"><i class="bi bi-lock-fill"></i> Details</button>`
|
||||
? `<button class="details-btn" disabled><i class="bi bi-lock-fill"></i> Details</button>`
|
||||
: `<button class="details-btn" onclick="location.href='/post-detail?id=${p.id}'">View Details</button>`
|
||||
}
|
||||
</div>
|
||||
@ -477,7 +334,8 @@
|
||||
const grid = document.getElementById("prompts-grid");
|
||||
const emptyEl = document.getElementById("market-empty");
|
||||
const errorEl = document.getElementById("market-error");
|
||||
const search = document.getElementById("topbarSearchInput")?.value.trim() || "";
|
||||
const search =
|
||||
document.getElementById("topbarSearchInput")?.value.trim() || "";
|
||||
const [sortBy, ascending] = document
|
||||
.getElementById("sort-select")
|
||||
.value.split("|");
|
||||
@ -488,8 +346,10 @@
|
||||
cardIndex = 0;
|
||||
|
||||
try {
|
||||
const apiSortBy = sortBy === "price" || sortBy === "free" ? "date" : sortBy;
|
||||
const apiAscending = sortBy === "price" || sortBy === "free" ? "false" : ascending;
|
||||
const apiSortBy =
|
||||
sortBy === "price" || sortBy === "free" ? "date" : sortBy;
|
||||
const apiAscending =
|
||||
sortBy === "price" || sortBy === "free" ? "false" : ascending;
|
||||
let url = `/api/v1/prompts?sortBy=${apiSortBy}&ascending=${apiAscending}&limit=50`;
|
||||
if (activeCategory) url += `&category=${activeCategory}`;
|
||||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||||
@ -501,7 +361,11 @@
|
||||
}
|
||||
if (!res.ok) throw new Error(`Server error ${res.status}`);
|
||||
|
||||
let prompts = applyMarketplaceSort(await res.json(), sortBy, ascending);
|
||||
let prompts = applyMarketplaceSort(
|
||||
await res.json(),
|
||||
sortBy,
|
||||
ascending,
|
||||
);
|
||||
|
||||
if (prompts.length === 0) {
|
||||
emptyEl.style.display = "block";
|
||||
@ -596,9 +460,12 @@
|
||||
|
||||
function openPayment(prompt) {
|
||||
currentPrompt = prompt;
|
||||
const usd = prompt.price != null && Number(prompt.price) > 0
|
||||
? Number(prompt.price)
|
||||
: prompt.tierLevel ? prompt.tierLevel * 4.99 : 0;
|
||||
const usd =
|
||||
prompt.price != null && Number(prompt.price) > 0
|
||||
? Number(prompt.price)
|
||||
: prompt.tierLevel
|
||||
? prompt.tierLevel * 4.99
|
||||
: 0;
|
||||
document.getElementById("pay-prompt-title").textContent = prompt.title;
|
||||
document.getElementById("price-btc").textContent =
|
||||
`≈ ${(usd / 67000).toFixed(6)} BTC`;
|
||||
|
||||
@ -19,121 +19,43 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
class="layout"
|
||||
style="display: flex; min-height: 100vh; background: var(--bg)"
|
||||
>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex: 1; display: flex; flex-direction: column">
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="post-detail-main">
|
||||
<div class="post-detail-container" id="detail-content">
|
||||
<!-- Loading -->
|
||||
<div
|
||||
id="detail-loading"
|
||||
style="text-align: center; padding: 60px 20px; color: #64748b"
|
||||
>
|
||||
<i
|
||||
class="bi bi-hourglass-split"
|
||||
style="font-size: 2.5rem; display: block; margin-bottom: 12px"
|
||||
></i>
|
||||
<div id="detail-loading">
|
||||
<i class="bi bi-hourglass-split state-icon"></i>
|
||||
<p>Loading prompt...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div
|
||||
id="detail-error"
|
||||
style="
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #ef4444;
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-exclamation-circle"
|
||||
style="font-size: 2.5rem; display: block; margin-bottom: 12px"
|
||||
></i>
|
||||
<div id="detail-error" class="state-error">
|
||||
<i class="bi bi-exclamation-circle state-icon"></i>
|
||||
<h3 id="detail-error-title">Prompt not found</h3>
|
||||
<p
|
||||
id="detail-error-msg"
|
||||
style="color: #64748b; margin-top: 8px"
|
||||
></p>
|
||||
<button
|
||||
onclick="history.back()"
|
||||
style="
|
||||
margin-top: 20px;
|
||||
padding: 10px 24px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
<p id="detail-error-msg"></p>
|
||||
<button onclick="history.back()" class="detail-back-btn">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content (hidden until loaded) -->
|
||||
<div id="detail-body" style="display: none">
|
||||
<div id="detail-body">
|
||||
<!-- Header -->
|
||||
<div class="post-header">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
id="creator-avatar"
|
||||
style="
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
"
|
||||
></div>
|
||||
<div class="detail-creator-row">
|
||||
<div id="creator-avatar"></div>
|
||||
<div>
|
||||
<span
|
||||
id="creator-name"
|
||||
style="font-weight: 600; font-size: 0.95rem"
|
||||
></span>
|
||||
<span
|
||||
id="prompt-date"
|
||||
style="display: block; font-size: 0.8rem; color: #94a3b8"
|
||||
></span>
|
||||
<span id="creator-name"></span>
|
||||
<span id="prompt-date"></span>
|
||||
</div>
|
||||
<div style="margin-left: auto">
|
||||
<div class="detail-actions-right">
|
||||
<span id="tier-badge"></span>
|
||||
<button
|
||||
id="edit-prompt-btn"
|
||||
style="
|
||||
display: none;
|
||||
margin-left: 10px;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button id="edit-prompt-btn">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="post-title" id="prompt-title"></h1>
|
||||
@ -157,73 +79,38 @@
|
||||
<!-- Prompt Content (only if accessible) -->
|
||||
<div class="prompt-section" id="prompt-content-section">
|
||||
<h2>PROMPT</h2>
|
||||
<div
|
||||
class="prompt-content"
|
||||
id="prompt-body"
|
||||
style="
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
"
|
||||
></div>
|
||||
<div class="prompt-content" id="prompt-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- Example Output -->
|
||||
<div class="example-section" id="example-section" style="display: none">
|
||||
<div class="example-section" id="example-section">
|
||||
<h2>EXAMPLE OUTPUT</h2>
|
||||
<div class="example-content">
|
||||
<div id="example-output-text" class="example-output-text" style="white-space: pre-wrap"></div>
|
||||
<div id="example-image" class="example-image" style="display: none">
|
||||
<img id="example-image-img" src="" alt="Example output image">
|
||||
<div
|
||||
id="example-output-text"
|
||||
class="example-output-text"
|
||||
></div>
|
||||
<div id="example-image" class="example-image">
|
||||
<img
|
||||
id="example-image-img"
|
||||
src=""
|
||||
alt="Example output image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Locked section (shown instead of prompt if no access) -->
|
||||
<div
|
||||
id="locked-section"
|
||||
style="
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 28px;
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-lock-fill"
|
||||
style="
|
||||
font-size: 2.5rem;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
"
|
||||
></i>
|
||||
<h3 style="margin-bottom: 8px">
|
||||
<div id="locked-section">
|
||||
<i class="bi bi-lock-fill locked-icon"></i>
|
||||
<h3 class="locked-title">
|
||||
This prompt requires a subscription
|
||||
</h3>
|
||||
<p style="color: #64748b; margin-bottom: 20px">
|
||||
<p class="locked-desc">
|
||||
Subscribe to <strong id="locked-creator"></strong> to access
|
||||
this prompt.
|
||||
</p>
|
||||
<button
|
||||
id="locked-subscribe-btn"
|
||||
style="
|
||||
padding: 12px 28px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
"
|
||||
>
|
||||
<button id="locked-subscribe-btn">
|
||||
Subscribe <span id="locked-tier-name"></span>
|
||||
</button>
|
||||
</div>
|
||||
@ -238,19 +125,30 @@
|
||||
<h2>REVIEWS</h2>
|
||||
<div class="review-form" id="review-form">
|
||||
<h3>Your review</h3>
|
||||
<div class="review-star-input" id="review-star-input" aria-label="Select rating">
|
||||
<div
|
||||
class="review-star-input"
|
||||
id="review-star-input"
|
||||
aria-label="Select rating"
|
||||
>
|
||||
<button type="button" data-rating="1">☆</button>
|
||||
<button type="button" data-rating="2">☆</button>
|
||||
<button type="button" data-rating="3">☆</button>
|
||||
<button type="button" data-rating="4">☆</button>
|
||||
<button type="button" data-rating="5">☆</button>
|
||||
</div>
|
||||
<textarea id="review-comment" maxlength="200" rows="3" placeholder="Write a short comment..."></textarea>
|
||||
<button type="button" id="submit-review-btn">Submit Review</button>
|
||||
<textarea
|
||||
id="review-comment"
|
||||
maxlength="200"
|
||||
rows="3"
|
||||
placeholder="Write a short comment..."
|
||||
></textarea>
|
||||
<button type="button" id="submit-review-btn">
|
||||
Submit Review
|
||||
</button>
|
||||
<p id="review-message"></p>
|
||||
</div>
|
||||
<div class="reviews-list" id="reviews-list">
|
||||
<p style="color:#94a3b8;">Loading reviews...</p>
|
||||
<p class="detail-loading-text">Loading reviews...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -331,8 +229,8 @@
|
||||
p.description;
|
||||
document.getElementById("prompt-body").textContent = p.content;
|
||||
document.getElementById("prompt-rating-stat").innerHTML =
|
||||
`<i class="bi ${p.isLiked ? 'bi-heart-fill' : 'bi-heart'}" style="color:#ef4444;"></i> ${p.likeCount || 0} likes
|
||||
<span style="margin-left:12px;"><i class="bi ${p.isSaved ? 'bi-bookmark-fill' : 'bi-bookmark'}" style="color:#f59e0b;"></i> ${p.saveCount || 0} saves</span>`;
|
||||
`<i class="bi ${p.isLiked ? "bi-heart-fill" : "bi-heart"} detail-heart-icon"></i> ${p.likeCount || 0} likes
|
||||
<span class="detail-bookmark-span"><i class="bi ${p.isSaved ? "bi-bookmark-fill" : "bi-bookmark"} detail-bookmark-icon"></i> ${p.saveCount || 0} saves</span>`;
|
||||
renderExamples(p);
|
||||
renderOwnerActions(p);
|
||||
setupReviewSection(p);
|
||||
@ -341,27 +239,27 @@
|
||||
// Tier badge
|
||||
const badge = document.getElementById("tier-badge");
|
||||
if (p.price != null && Number(p.price) > 0) {
|
||||
badge.innerHTML = `<span style="background:#fef3c7;color:#92400e;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">$${Number(p.price).toFixed(2)}</span>`;
|
||||
badge.innerHTML = `<span class="tier-badge-paid">$${Number(p.price).toFixed(2)}</span>`;
|
||||
} else if (p.tierName) {
|
||||
badge.innerHTML = `<span style="background:#f1f5f9;color:#475569;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;"><i class="bi bi-lock-fill"></i> ${p.tierName}</span>`;
|
||||
badge.innerHTML = `<span class="tier-badge-tier"><i class="bi bi-lock-fill"></i> ${p.tierName}</span>`;
|
||||
} else {
|
||||
badge.innerHTML = `<span style="background:#dcfce7;color:#166534;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">Free</span>`;
|
||||
badge.innerHTML = `<span class="tier-badge-free">Free</span>`;
|
||||
}
|
||||
|
||||
// Rating
|
||||
if (p.averageRating != null) {
|
||||
const stars = Math.round(p.averageRating);
|
||||
document.getElementById("rating-display").innerHTML =
|
||||
`<span style="color:#f59e0b;font-size:1.1rem;">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span>
|
||||
<span style="margin-left:8px;font-weight:600;">${p.averageRating.toFixed(1)}</span>
|
||||
<span style="color:#94a3b8;font-size:0.85rem;margin-left:4px;">/ 5.0 (${p.reviewCount || 0} ${(p.reviewCount || 0) === 1 ? "review" : "reviews"})</span>`;
|
||||
`<span class="rating-stars-display">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span>
|
||||
<span class="rating-value">${p.averageRating.toFixed(1)}</span>
|
||||
<span class="rating-count">/ 5.0 (${p.reviewCount || 0} ${(p.reviewCount || 0) === 1 ? "review" : "reviews"})</span>`;
|
||||
document.getElementById("prompt-rating-stat").innerHTML =
|
||||
`<i class="bi ${p.isLiked ? 'bi-heart-fill' : 'bi-heart'}" style="color:#ef4444;"></i> ${p.likeCount || 0} likes
|
||||
<span style="margin-left:12px;"><i class="bi ${p.isSaved ? 'bi-bookmark-fill' : 'bi-bookmark'}" style="color:#f59e0b;"></i> ${p.saveCount || 0} saves</span>
|
||||
<span style="margin-left:12px;"><i class="bi bi-star-fill" style="color:#f59e0b;"></i> ${p.averageRating.toFixed(1)} (${p.reviewCount || 0})</span>`;
|
||||
`<i class="bi ${p.isLiked ? "bi-heart-fill" : "bi-heart"} detail-heart-icon"></i> ${p.likeCount || 0} likes
|
||||
<span class="detail-bookmark-span"><i class="bi ${p.isSaved ? "bi-bookmark-fill" : "bi-bookmark"} detail-bookmark-icon"></i> ${p.saveCount || 0} saves</span>
|
||||
<span class="detail-bookmark-span"><i class="bi bi-star-fill detail-bookmark-icon"></i> ${p.averageRating.toFixed(1)} (${p.reviewCount || 0})</span>`;
|
||||
} else {
|
||||
document.getElementById("rating-display").innerHTML =
|
||||
'<span style="color:#94a3b8;font-size:0.9rem;">No ratings yet</span>';
|
||||
'<span class="rating-none">No ratings yet</span>';
|
||||
}
|
||||
|
||||
// Content visibility
|
||||
@ -397,10 +295,12 @@
|
||||
|
||||
function setReviewRating(rating) {
|
||||
selectedReviewRating = rating;
|
||||
document.querySelectorAll("#review-star-input button").forEach((button) => {
|
||||
const value = Number(button.dataset.rating);
|
||||
button.textContent = value <= rating ? "★" : "☆";
|
||||
});
|
||||
document
|
||||
.querySelectorAll("#review-star-input button")
|
||||
.forEach((button) => {
|
||||
const value = Number(button.dataset.rating);
|
||||
button.textContent = value <= rating ? "★" : "☆";
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
@ -451,9 +351,12 @@
|
||||
// Keep the review form visible; the API will reject unauthenticated users.
|
||||
}
|
||||
|
||||
document.querySelectorAll("#review-star-input button").forEach((button) => {
|
||||
button.onclick = () => setReviewRating(Number(button.dataset.rating));
|
||||
});
|
||||
document
|
||||
.querySelectorAll("#review-star-input button")
|
||||
.forEach((button) => {
|
||||
button.onclick = () =>
|
||||
setReviewRating(Number(button.dataset.rating));
|
||||
});
|
||||
|
||||
submitBtn.onclick = async () => {
|
||||
if (!selectedReviewRating) {
|
||||
@ -470,7 +373,8 @@
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
rating: selectedReviewRating,
|
||||
comment: document.getElementById("review-comment").value.trim() || null,
|
||||
comment:
|
||||
document.getElementById("review-comment").value.trim() || null,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -505,13 +409,16 @@
|
||||
|
||||
const reviews = await response.json();
|
||||
if (reviews.length === 0) {
|
||||
list.innerHTML = '<p style="color:#94a3b8;">No reviews yet.</p>';
|
||||
list.innerHTML =
|
||||
'<p class="detail-loading-text">No reviews yet.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = reviews.map((review) => {
|
||||
const stars = "★".repeat(review.rating) + "☆".repeat(5 - review.rating);
|
||||
return `
|
||||
list.innerHTML = reviews
|
||||
.map((review) => {
|
||||
const stars =
|
||||
"★".repeat(review.rating) + "☆".repeat(5 - review.rating);
|
||||
return `
|
||||
<article class="review-card">
|
||||
<div class="review-card-header">
|
||||
<span class="review-card-user">@${escapeHtml(review.creatorName)}</span>
|
||||
@ -519,9 +426,10 @@
|
||||
</div>
|
||||
<p class="review-card-comment">${escapeHtml(review.comment || "No comment.")}</p>
|
||||
</article>`;
|
||||
}).join("");
|
||||
})
|
||||
.join("");
|
||||
} catch (error) {
|
||||
list.innerHTML = `<p style="color:#ef4444;">${error.message}</p>`;
|
||||
list.innerHTML = `<p class="detail-error-text">${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,398 +1,486 @@
|
||||
<!-- OnlyPrompt - Profile page:
|
||||
- User profile display with avatar, bio, stats, and prompt cards (personal prompts) -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OnlyPrompt - Profile</title>
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
<link rel="stylesheet" href="../css/base.css">
|
||||
<link rel="stylesheet" href="../css/sidebar.css">
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/profile.css">
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OnlyPrompt - Profile</title>
|
||||
<link rel="stylesheet" href="../css/variables.css" />
|
||||
<link rel="stylesheet" href="../css/base.css" />
|
||||
<link rel="stylesheet" href="../css/sidebar.css" />
|
||||
<link rel="stylesheet" href="../css/login.css" />
|
||||
<link rel="stylesheet" href="../css/topbar.css" />
|
||||
<link rel="stylesheet" href="../css/profile.css" />
|
||||
<script src="../js/profile-shared.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex:1; margin:40px auto; max-width:950px;">
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="login-card profile-main" style="background:#fff;border-radius:18px;box-shadow:0 2px 8px rgba(59,130,246,0.06);padding:24px;">
|
||||
|
||||
<section class="profile-header" style="display:flex;align-items:center;gap:32px;border-bottom:1px solid #e5e7eb;padding-bottom:24px;">
|
||||
|
||||
<img id="profileAvatar" src="../images/content/cat.png" class="profile-avatar" style="width:110px;height:110px;border-radius:50%;object-fit:cover;">
|
||||
|
||||
<div class="profile-info" style="flex:1;">
|
||||
<h1 id="profileDisplayName" style="font-size:2rem;font-weight:700;margin-bottom:4px;">Loading...</h1>
|
||||
<div id="profileSlug" style="color:#64748b;margin-bottom:8px;">
|
||||
@profile <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>
|
||||
<main class="login-card profile-main">
|
||||
<section class="profile-header">
|
||||
<img
|
||||
id="profileAvatar"
|
||||
src="../images/content/cat.png"
|
||||
class="profile-avatar"
|
||||
/>
|
||||
|
||||
<div class="profile-info">
|
||||
<h1 id="profileDisplayName">Loading...</h1>
|
||||
<div id="profileSlug">
|
||||
@profile
|
||||
<i class="bi bi-patch-check-fill profile-badge-icon"></i>
|
||||
</div>
|
||||
|
||||
<div id="profileBio">Loading profile...</div>
|
||||
|
||||
<div id="profileSpecialities"></div>
|
||||
<div id="profileStats">
|
||||
<span><strong id="profileRating">0.0</strong> rating</span>
|
||||
<span
|
||||
><strong id="profileSubscribers">0</strong> subscribers</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="profileBio" style="margin-bottom:8px;">
|
||||
Loading profile...
|
||||
<div id="profileActions">
|
||||
<button
|
||||
id="primaryProfileButton"
|
||||
class="login-button"
|
||||
onclick="location.href = 'settings.html'"
|
||||
>
|
||||
Edit Profile
|
||||
</button>
|
||||
<button id="shareProfileButton" class="login-button">
|
||||
Share Profile
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="profileSpecialities" style="color:#64748b;"></div>
|
||||
<div id="profileStats" style="display:flex;gap:18px;color:#64748b;margin-top:12px;font-size:0.95rem;">
|
||||
<span><strong id="profileRating" style="color:#111827;">0.0</strong> rating</span>
|
||||
<span><strong id="profileSubscribers" style="color:#111827;">0</strong> subscribers</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="profile-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="profile-tab active"
|
||||
data-tab="mine"
|
||||
id="myPromptsTab"
|
||||
>
|
||||
My Prompts
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="profile-tab"
|
||||
data-tab="favorites"
|
||||
id="favoritesTab"
|
||||
>
|
||||
Favorites
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="profile-tab"
|
||||
data-tab="saved"
|
||||
id="savedTab"
|
||||
>
|
||||
Saved
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div id="profileActions" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<button id="primaryProfileButton" class="login-button" onclick="location.href='settings.html'">Edit Profile</button>
|
||||
<button id="shareProfileButton" class="login-button" style="background:#f3f4f6;color:#111;box-shadow:none;">Share Profile</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<nav class="profile-tabs" style="display:flex;gap:24px;border-bottom:2px solid #e5e7eb;margin:32px 0 18px 0;flex-wrap:wrap;">
|
||||
<button type="button" class="profile-tab active" data-tab="mine" id="myPromptsTab">My Prompts</button>
|
||||
<button type="button" class="profile-tab" data-tab="favorites" id="favoritesTab">Favorites</button>
|
||||
<button type="button" class="profile-tab" data-tab="saved" id="savedTab">Saved</button>
|
||||
</nav>
|
||||
|
||||
<section id="profile-prompts-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:24px;">
|
||||
<div style="grid-column:1/-1;color:#64748b;text-align:center;padding:28px;">Loading prompts...</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
<section id="profile-prompts-grid">
|
||||
<div class="profile-grid-loading">Loading prompts...</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
fetch('/sidebar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
document.getElementById('sidebar-container').innerHTML = data;
|
||||
// Remove 'active' from all sidebar links
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
<script>
|
||||
fetch("/sidebar.html")
|
||||
.then((r) => r.text())
|
||||
.then((data) => {
|
||||
document.getElementById("sidebar-container").innerHTML = data;
|
||||
// Remove 'active' from all sidebar links
|
||||
document
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((link) => {
|
||||
link.classList.remove("active");
|
||||
});
|
||||
// Then set 'active' only on the My Profile link
|
||||
const profileLink = document.querySelector(
|
||||
'#sidebar-container a[href="profile.html"]',
|
||||
);
|
||||
if (profileLink) profileLink.classList.add("active");
|
||||
});
|
||||
// Then set 'active' only on the My Profile link
|
||||
const profileLink = document.querySelector('#sidebar-container a[href="profile.html"]');
|
||||
if (profileLink) profileLink.classList.add('active');
|
||||
});
|
||||
|
||||
fetch('/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
fetch("/topbar.html")
|
||||
.then((r) => r.text())
|
||||
.then(
|
||||
(data) =>
|
||||
(document.getElementById("topbar-container").innerHTML = data),
|
||||
);
|
||||
|
||||
const profileAvatar = document.getElementById('profileAvatar');
|
||||
const profileDisplayName = document.getElementById('profileDisplayName');
|
||||
const profileSlug = document.getElementById('profileSlug');
|
||||
const profileBio = document.getElementById('profileBio');
|
||||
const profileSpecialities = document.getElementById('profileSpecialities');
|
||||
const profileRating = document.getElementById('profileRating');
|
||||
const profileSubscribers = document.getElementById('profileSubscribers');
|
||||
const profilePromptsGrid = document.getElementById('profile-prompts-grid');
|
||||
const myPromptsTab = document.getElementById('myPromptsTab');
|
||||
const favoritesTab = document.getElementById('favoritesTab');
|
||||
const savedTab = document.getElementById('savedTab');
|
||||
const profileActions = document.getElementById('profileActions');
|
||||
const primaryProfileButton = document.getElementById('primaryProfileButton');
|
||||
const shareProfileButton = document.getElementById('shareProfileButton');
|
||||
const profileTabs = document.querySelector('.profile-tabs');
|
||||
const params = new URLSearchParams(location.search);
|
||||
const profileId = params.get('id');
|
||||
let ownPrompts = [];
|
||||
let allPrompts = [];
|
||||
let profilePrompts = [];
|
||||
let activeProfileTab = 'mine';
|
||||
let currentUserId = null;
|
||||
let isOwnProfile = !profileId;
|
||||
let profileLoaded = false;
|
||||
let currentIsFollowing = false;
|
||||
const profileAvatar = document.getElementById("profileAvatar");
|
||||
const profileDisplayName = document.getElementById("profileDisplayName");
|
||||
const profileSlug = document.getElementById("profileSlug");
|
||||
const profileBio = document.getElementById("profileBio");
|
||||
const profileSpecialities = document.getElementById(
|
||||
"profileSpecialities",
|
||||
);
|
||||
const profileRating = document.getElementById("profileRating");
|
||||
const profileSubscribers = document.getElementById("profileSubscribers");
|
||||
const profilePromptsGrid = document.getElementById(
|
||||
"profile-prompts-grid",
|
||||
);
|
||||
const myPromptsTab = document.getElementById("myPromptsTab");
|
||||
const favoritesTab = document.getElementById("favoritesTab");
|
||||
const savedTab = document.getElementById("savedTab");
|
||||
const profileActions = document.getElementById("profileActions");
|
||||
const primaryProfileButton = document.getElementById(
|
||||
"primaryProfileButton",
|
||||
);
|
||||
const shareProfileButton = document.getElementById("shareProfileButton");
|
||||
const profileTabs = document.querySelector(".profile-tabs");
|
||||
const params = new URLSearchParams(location.search);
|
||||
const profileId = params.get("id");
|
||||
let ownPrompts = [];
|
||||
let allPrompts = [];
|
||||
let profilePrompts = [];
|
||||
let activeProfileTab = "mine";
|
||||
let currentUserId = null;
|
||||
let isOwnProfile = !profileId;
|
||||
let profileLoaded = false;
|
||||
let currentIsFollowing = false;
|
||||
|
||||
async function fetchJson(url) {
|
||||
const response = await fetch(url, { credentials: 'same-origin' });
|
||||
if (response.status === 401) {
|
||||
location.href = '/login';
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) throw new Error(`${url} returned ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function renderProfile(profile, fallbackName = 'Profile') {
|
||||
profileDisplayName.textContent = profile.displayName || fallbackName;
|
||||
profileSlug.innerHTML = `@${profile.user?.userName || profile.slug || 'profile'} <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>`;
|
||||
profileBio.textContent = profile.bio || 'No bio yet.';
|
||||
profileSpecialities.textContent = profile.specialities || 'No specialities added yet.';
|
||||
profileRating.textContent = Number(profile.averageRating || 0).toFixed(1);
|
||||
profileSubscribers.textContent = profile.subscribers || 0;
|
||||
|
||||
if (profile.avatarUrl) {
|
||||
profileAvatar.src = profile.avatarUrl;
|
||||
}
|
||||
profileLoaded = true;
|
||||
}
|
||||
|
||||
function renderProfileFromPrompt(prompt) {
|
||||
if (!prompt || profileLoaded) return;
|
||||
|
||||
profileDisplayName.textContent = prompt.creatorName || 'Creator Profile';
|
||||
profileSlug.innerHTML = `@${prompt.creatorName || 'creator'} <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>`;
|
||||
profileBio.textContent = 'No bio yet.';
|
||||
profileSpecialities.textContent = '';
|
||||
profileRating.textContent = Number(prompt.averageRating || 0).toFixed(1);
|
||||
profileSubscribers.textContent = 0;
|
||||
|
||||
if (prompt.creatorAvatarUrl) {
|
||||
profileAvatar.src = prompt.creatorAvatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCreatorCardFallback() {
|
||||
if (isOwnProfile || profileLoaded || !profileId) return;
|
||||
|
||||
try {
|
||||
const creators = await fetchJson('/api/v1/profiles?limit=100');
|
||||
const creator = creators.find((item) => item.userId?.toLowerCase() === profileId.toLowerCase());
|
||||
if (!creator) return;
|
||||
|
||||
renderProfile({
|
||||
displayName: creator.displayName,
|
||||
slug: creator.slug,
|
||||
bio: creator.bio,
|
||||
avatarUrl: creator.avatarUrl,
|
||||
specialities: null,
|
||||
averageRating: creator.averageRating,
|
||||
subscribers: creator.subscribers
|
||||
}, 'Creator Profile');
|
||||
} catch {
|
||||
// Prompt data below still provides a minimal fallback if creator cards fail.
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const currentProfile = await window.loadCurrentProfile();
|
||||
currentUserId = currentProfile.user?.id;
|
||||
isOwnProfile = !profileId || profileId.toLowerCase() === currentUserId?.toLowerCase();
|
||||
|
||||
if (isOwnProfile) {
|
||||
renderProfile(currentProfile, 'My Profile');
|
||||
return;
|
||||
async function fetchJson(url) {
|
||||
const response = await fetch(url, { credentials: "same-origin" });
|
||||
if (response.status === 401) {
|
||||
location.href = "/login";
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) throw new Error(`${url} returned ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const profile = await fetchJson(`/api/v1/profiles/${encodeURIComponent(profileId)}`);
|
||||
renderProfile(profile, 'Creator Profile');
|
||||
} catch (error) {
|
||||
if (isOwnProfile) {
|
||||
profileDisplayName.textContent = 'Profile unavailable';
|
||||
profileBio.textContent = error.message;
|
||||
} else {
|
||||
profileDisplayName.textContent = 'Loading creator...';
|
||||
profileBio.textContent = '';
|
||||
function renderProfile(profile, fallbackName = "Profile") {
|
||||
profileDisplayName.textContent = profile.displayName || fallbackName;
|
||||
profileSlug.innerHTML = `@${profile.user?.userName || profile.slug || "profile"} <i class="bi bi-patch-check-fill profile-badge-icon"></i>`;
|
||||
profileBio.textContent = profile.bio || "No bio yet.";
|
||||
profileSpecialities.textContent =
|
||||
profile.specialities || "No specialities added yet.";
|
||||
profileRating.textContent = Number(profile.averageRating || 0).toFixed(
|
||||
1,
|
||||
);
|
||||
profileSubscribers.textContent = profile.subscribers || 0;
|
||||
|
||||
if (profile.avatarUrl) {
|
||||
profileAvatar.src = profile.avatarUrl;
|
||||
}
|
||||
profileLoaded = true;
|
||||
}
|
||||
|
||||
function renderProfileFromPrompt(prompt) {
|
||||
if (!prompt || profileLoaded) return;
|
||||
|
||||
profileDisplayName.textContent =
|
||||
prompt.creatorName || "Creator Profile";
|
||||
profileSlug.innerHTML = `@${prompt.creatorName || "creator"} <i class="bi bi-patch-check-fill profile-badge-icon"></i>`;
|
||||
profileBio.textContent = "No bio yet.";
|
||||
profileSpecialities.textContent = "";
|
||||
profileRating.textContent = Number(prompt.averageRating || 0).toFixed(
|
||||
1,
|
||||
);
|
||||
profileSubscribers.textContent = 0;
|
||||
|
||||
if (prompt.creatorAvatarUrl) {
|
||||
profileAvatar.src = prompt.creatorAvatarUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isPromptMarked(type, id) {
|
||||
if (type === 'liked') {
|
||||
const prompt = allPrompts.find((item) => item.id === id);
|
||||
return prompt?.isLiked === true;
|
||||
async function loadCreatorCardFallback() {
|
||||
if (isOwnProfile || profileLoaded || !profileId) return;
|
||||
|
||||
try {
|
||||
const creators = await fetchJson("/api/v1/profiles?limit=100");
|
||||
const creator = creators.find(
|
||||
(item) => item.userId?.toLowerCase() === profileId.toLowerCase(),
|
||||
);
|
||||
if (!creator) return;
|
||||
|
||||
renderProfile(
|
||||
{
|
||||
displayName: creator.displayName,
|
||||
slug: creator.slug,
|
||||
bio: creator.bio,
|
||||
avatarUrl: creator.avatarUrl,
|
||||
specialities: null,
|
||||
averageRating: creator.averageRating,
|
||||
subscribers: creator.subscribers,
|
||||
},
|
||||
"Creator Profile",
|
||||
);
|
||||
} catch {
|
||||
// Prompt data below still provides a minimal fallback if creator cards fail.
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'saved') {
|
||||
const prompt = allPrompts.find((item) => item.id === id);
|
||||
return prompt?.isSaved === true;
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const currentProfile = await window.loadCurrentProfile();
|
||||
currentUserId = currentProfile.user?.id;
|
||||
isOwnProfile =
|
||||
!profileId ||
|
||||
profileId.toLowerCase() === currentUserId?.toLowerCase();
|
||||
|
||||
if (isOwnProfile) {
|
||||
renderProfile(currentProfile, "My Profile");
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await fetchJson(
|
||||
`/api/v1/profiles/${encodeURIComponent(profileId)}`,
|
||||
);
|
||||
renderProfile(profile, "Creator Profile");
|
||||
} catch (error) {
|
||||
if (isOwnProfile) {
|
||||
profileDisplayName.textContent = "Profile unavailable";
|
||||
profileBio.textContent = error.message;
|
||||
} else {
|
||||
profileDisplayName.textContent = "Loading creator...";
|
||||
profileBio.textContent = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function isPromptMarked(type, id) {
|
||||
if (type === "liked") {
|
||||
const prompt = allPrompts.find((item) => item.id === id);
|
||||
return prompt?.isLiked === true;
|
||||
}
|
||||
|
||||
function renderProfilePrompt(prompt, options = {}) {
|
||||
const image = prompt.exampleImageUrl || '../images/content/post1.png';
|
||||
const showEdit = options.showEdit === true;
|
||||
const rating = prompt.averageRating == null ? 'No ratings' : prompt.averageRating.toFixed(1);
|
||||
return `
|
||||
<div onclick="location.href='/post-detail?id=${prompt.id}'" style="background:#fff;border-radius:18px;box-shadow:0 2px 8px rgba(59,130,246,0.06);padding:18px;display:flex;gap:16px;cursor:pointer;">
|
||||
<img src="${image}" alt="${prompt.title}" style="width:72px;height:72px;border-radius:12px;object-fit:cover;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:700;">${prompt.title}</div>
|
||||
<div style="color:#64748b;margin-bottom:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${prompt.description || 'No description yet.'}</div>
|
||||
<div style="display:flex;gap:16px;color:#64748b;align-items:center;flex-wrap:wrap;">
|
||||
if (type === "saved") {
|
||||
const prompt = allPrompts.find((item) => item.id === id);
|
||||
return prompt?.isSaved === true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderProfilePrompt(prompt, options = {}) {
|
||||
const image = prompt.exampleImageUrl || "../images/content/post1.png";
|
||||
const showEdit = options.showEdit === true;
|
||||
const rating =
|
||||
prompt.averageRating == null
|
||||
? "No ratings"
|
||||
: prompt.averageRating.toFixed(1);
|
||||
return `
|
||||
<div onclick="location.href='/post-detail?id=${prompt.id}'" class="profile-prompt-card">
|
||||
<img src="${image}" alt="${prompt.title}" class="profile-prompt-img">
|
||||
<div class="profile-prompt-body">
|
||||
<div class="profile-prompt-title">${prompt.title}</div>
|
||||
<div class="profile-prompt-desc">${prompt.description || "No description yet."}</div>
|
||||
<div class="profile-prompt-meta">
|
||||
<span><i class="bi bi-star"></i> ${rating}</span>
|
||||
${prompt.creatorName ? `<span>@${prompt.creatorName}</span>` : ''}
|
||||
${showEdit ? `<button onclick="event.stopPropagation(); location.href='/create?id=${prompt.id}'" style="border:none;background:#f1f5f9;color:#334155;border-radius:10px;padding:6px 10px;font-weight:700;cursor:pointer;">Edit</button>` : ''}
|
||||
${prompt.creatorName ? `<span>@${prompt.creatorName}</span>` : ""}
|
||||
${showEdit ? `<button onclick="event.stopPropagation(); location.href='/create?id=${prompt.id}'" class="profile-prompt-edit-btn">Edit</button>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderPromptList(prompts, emptyText, options = {}) {
|
||||
if (!prompts.length) {
|
||||
profilePromptsGrid.innerHTML = `<div style="grid-column:1/-1;color:#64748b;text-align:center;padding:28px;">${emptyText}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
profilePromptsGrid.innerHTML = prompts.map((prompt) => renderProfilePrompt(prompt, options)).join('');
|
||||
}
|
||||
function renderPromptList(prompts, emptyText, options = {}) {
|
||||
if (!prompts.length) {
|
||||
profilePromptsGrid.innerHTML = `<div class="profile-grid-empty">${emptyText}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
function updateTabs() {
|
||||
document.querySelectorAll('.profile-tab').forEach((tab) => {
|
||||
tab.classList.toggle('active', tab.dataset.tab === activeProfileTab);
|
||||
});
|
||||
|
||||
const liked = allPrompts.filter((prompt) => isPromptMarked('liked', prompt.id));
|
||||
const saved = allPrompts.filter((prompt) => isPromptMarked('saved', prompt.id));
|
||||
|
||||
myPromptsTab.textContent = `My Prompts (${ownPrompts.length})`;
|
||||
favoritesTab.textContent = `Favorites (${liked.length})`;
|
||||
savedTab.textContent = `Saved (${saved.length})`;
|
||||
|
||||
if (activeProfileTab === 'favorites') {
|
||||
renderPromptList(liked, 'No liked prompts yet.');
|
||||
} else if (activeProfileTab === 'saved') {
|
||||
renderPromptList(saved, 'No saved prompts yet.');
|
||||
} else {
|
||||
renderPromptList(ownPrompts, 'No prompts yet.', { showEdit: true });
|
||||
}
|
||||
}
|
||||
|
||||
function updateProfileMode() {
|
||||
if (isOwnProfile) {
|
||||
profileActions.style.display = 'flex';
|
||||
primaryProfileButton.textContent = 'Edit Profile';
|
||||
primaryProfileButton.disabled = false;
|
||||
primaryProfileButton.onclick = () => location.href = 'settings.html';
|
||||
profileTabs.style.display = 'flex';
|
||||
return;
|
||||
profilePromptsGrid.innerHTML = prompts
|
||||
.map((prompt) => renderProfilePrompt(prompt, options))
|
||||
.join("");
|
||||
}
|
||||
|
||||
profileActions.style.display = 'flex';
|
||||
primaryProfileButton.textContent = currentIsFollowing ? 'Following' : 'Follow';
|
||||
primaryProfileButton.disabled = false;
|
||||
primaryProfileButton.onclick = toggleProfileFollow;
|
||||
favoritesTab.style.display = 'none';
|
||||
savedTab.style.display = 'none';
|
||||
myPromptsTab.textContent = `Prompts (${profilePrompts.length})`;
|
||||
renderPromptList(profilePrompts, 'No prompts yet.');
|
||||
}
|
||||
|
||||
async function loadFollowState() {
|
||||
if (isOwnProfile || !profileId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/subscriptions/${encodeURIComponent(profileId)}`, {
|
||||
credentials: 'same-origin'
|
||||
function updateTabs() {
|
||||
document.querySelectorAll(".profile-tab").forEach((tab) => {
|
||||
tab.classList.toggle("active", tab.dataset.tab === activeProfileTab);
|
||||
});
|
||||
if (response.status === 401) {
|
||||
location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = response.ok ? await response.json() : null;
|
||||
currentIsFollowing = subscription !== null;
|
||||
updateProfileMode();
|
||||
} catch {
|
||||
currentIsFollowing = false;
|
||||
}
|
||||
}
|
||||
const liked = allPrompts.filter((prompt) =>
|
||||
isPromptMarked("liked", prompt.id),
|
||||
);
|
||||
const saved = allPrompts.filter((prompt) =>
|
||||
isPromptMarked("saved", prompt.id),
|
||||
);
|
||||
|
||||
async function toggleProfileFollow() {
|
||||
if (!profileId) return;
|
||||
myPromptsTab.textContent = `My Prompts (${ownPrompts.length})`;
|
||||
favoritesTab.textContent = `Favorites (${liked.length})`;
|
||||
savedTab.textContent = `Saved (${saved.length})`;
|
||||
|
||||
primaryProfileButton.disabled = true;
|
||||
const response = await fetch(`/api/v1/subscriptions/${encodeURIComponent(profileId)}`, {
|
||||
method: currentIsFollowing ? 'DELETE' : 'PUT',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const currentSubscribers = Number(profileSubscribers.textContent || 0);
|
||||
currentIsFollowing = !currentIsFollowing;
|
||||
profileSubscribers.textContent = Math.max(0, currentSubscribers + (currentIsFollowing ? 1 : -1));
|
||||
updateProfileMode();
|
||||
} else {
|
||||
primaryProfileButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
shareProfileButton.addEventListener('click', async () => {
|
||||
const url = isOwnProfile
|
||||
? `${location.origin}/profile.html`
|
||||
: `${location.origin}/profile.html?id=${encodeURIComponent(profileId)}`;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
shareProfileButton.textContent = 'Copied';
|
||||
setTimeout(() => shareProfileButton.textContent = 'Share Profile', 1200);
|
||||
} catch {
|
||||
location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadOwnPrompts() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/prompts/mine');
|
||||
if (response.status === 401) {
|
||||
location.href = '/login';
|
||||
return;
|
||||
}
|
||||
if (!response.ok) throw new Error('Prompts could not be loaded.');
|
||||
|
||||
ownPrompts = await response.json();
|
||||
updateTabs();
|
||||
} catch (error) {
|
||||
profilePromptsGrid.innerHTML = `<div style="grid-column:1/-1;color:#ef4444;text-align:center;padding:28px;">${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAllPromptReferences() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/prompts?limit=100');
|
||||
if (!response.ok) return;
|
||||
allPrompts = await response.json();
|
||||
if (isOwnProfile) {
|
||||
updateTabs();
|
||||
if (activeProfileTab === "favorites") {
|
||||
renderPromptList(liked, "No liked prompts yet.");
|
||||
} else if (activeProfileTab === "saved") {
|
||||
renderPromptList(saved, "No saved prompts yet.");
|
||||
} else {
|
||||
profilePrompts = allPrompts.filter((prompt) => prompt.creatorId?.toLowerCase() === profileId.toLowerCase());
|
||||
renderProfileFromPrompt(profilePrompts[0]);
|
||||
updateProfileMode();
|
||||
renderPromptList(ownPrompts, "No prompts yet.", { showEdit: true });
|
||||
}
|
||||
} catch {
|
||||
// Favorites and saved stay empty if prompts cannot be loaded.
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.profile-tab').forEach((tab) => {
|
||||
tab.addEventListener('click', () => {
|
||||
if (!isOwnProfile) {
|
||||
updateProfileMode();
|
||||
function updateProfileMode() {
|
||||
if (isOwnProfile) {
|
||||
profileActions.style.display = "flex";
|
||||
primaryProfileButton.textContent = "Edit Profile";
|
||||
primaryProfileButton.disabled = false;
|
||||
primaryProfileButton.onclick = () =>
|
||||
(location.href = "settings.html");
|
||||
profileTabs.style.display = "flex";
|
||||
return;
|
||||
}
|
||||
activeProfileTab = tab.dataset.tab;
|
||||
updateTabs();
|
||||
});
|
||||
});
|
||||
|
||||
(async function initProfilePage() {
|
||||
await loadProfile();
|
||||
await loadCreatorCardFallback();
|
||||
await loadFollowState();
|
||||
updateProfileMode();
|
||||
if (isOwnProfile) {
|
||||
loadOwnPrompts();
|
||||
profileActions.style.display = "flex";
|
||||
primaryProfileButton.textContent = currentIsFollowing
|
||||
? "Following"
|
||||
: "Follow";
|
||||
primaryProfileButton.disabled = false;
|
||||
primaryProfileButton.onclick = toggleProfileFollow;
|
||||
favoritesTab.style.display = "none";
|
||||
savedTab.style.display = "none";
|
||||
myPromptsTab.textContent = `Prompts (${profilePrompts.length})`;
|
||||
renderPromptList(profilePrompts, "No prompts yet.");
|
||||
}
|
||||
loadAllPromptReferences();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
async function loadFollowState() {
|
||||
if (isOwnProfile || !profileId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/subscriptions/${encodeURIComponent(profileId)}`,
|
||||
{
|
||||
credentials: "same-origin",
|
||||
},
|
||||
);
|
||||
if (response.status === 401) {
|
||||
location.href = "/login";
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = response.ok ? await response.json() : null;
|
||||
currentIsFollowing = subscription !== null;
|
||||
updateProfileMode();
|
||||
} catch {
|
||||
currentIsFollowing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleProfileFollow() {
|
||||
if (!profileId) return;
|
||||
|
||||
primaryProfileButton.disabled = true;
|
||||
const response = await fetch(
|
||||
`/api/v1/subscriptions/${encodeURIComponent(profileId)}`,
|
||||
{
|
||||
method: currentIsFollowing ? "DELETE" : "PUT",
|
||||
credentials: "same-origin",
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 401) {
|
||||
location.href = "/login";
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const currentSubscribers = Number(
|
||||
profileSubscribers.textContent || 0,
|
||||
);
|
||||
currentIsFollowing = !currentIsFollowing;
|
||||
profileSubscribers.textContent = Math.max(
|
||||
0,
|
||||
currentSubscribers + (currentIsFollowing ? 1 : -1),
|
||||
);
|
||||
updateProfileMode();
|
||||
} else {
|
||||
primaryProfileButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
shareProfileButton.addEventListener("click", async () => {
|
||||
const url = isOwnProfile
|
||||
? `${location.origin}/profile.html`
|
||||
: `${location.origin}/profile.html?id=${encodeURIComponent(profileId)}`;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
shareProfileButton.textContent = "Copied";
|
||||
setTimeout(
|
||||
() => (shareProfileButton.textContent = "Share Profile"),
|
||||
1200,
|
||||
);
|
||||
} catch {
|
||||
location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadOwnPrompts() {
|
||||
try {
|
||||
const response = await fetch("/api/v1/prompts/mine");
|
||||
if (response.status === 401) {
|
||||
location.href = "/login";
|
||||
return;
|
||||
}
|
||||
if (!response.ok) throw new Error("Prompts could not be loaded.");
|
||||
|
||||
ownPrompts = await response.json();
|
||||
updateTabs();
|
||||
} catch (error) {
|
||||
profilePromptsGrid.innerHTML = `<div class="profile-grid-error">${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAllPromptReferences() {
|
||||
try {
|
||||
const response = await fetch("/api/v1/prompts?limit=100");
|
||||
if (!response.ok) return;
|
||||
allPrompts = await response.json();
|
||||
if (isOwnProfile) {
|
||||
updateTabs();
|
||||
} else {
|
||||
profilePrompts = allPrompts.filter(
|
||||
(prompt) =>
|
||||
prompt.creatorId?.toLowerCase() === profileId.toLowerCase(),
|
||||
);
|
||||
renderProfileFromPrompt(profilePrompts[0]);
|
||||
updateProfileMode();
|
||||
}
|
||||
} catch {
|
||||
// Favorites and saved stay empty if prompts cannot be loaded.
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll(".profile-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
if (!isOwnProfile) {
|
||||
updateProfileMode();
|
||||
return;
|
||||
}
|
||||
activeProfileTab = tab.dataset.tab;
|
||||
updateTabs();
|
||||
});
|
||||
});
|
||||
|
||||
(async function initProfilePage() {
|
||||
await loadProfile();
|
||||
await loadCreatorCardFallback();
|
||||
await loadFollowState();
|
||||
updateProfileMode();
|
||||
if (isOwnProfile) {
|
||||
loadOwnPrompts();
|
||||
}
|
||||
loadAllPromptReferences();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -17,11 +17,11 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
||||
<div class="layout">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex:1; display: flex; flex-direction: column;">
|
||||
<div class="page-body">
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="save-btn">Save Changes</button>
|
||||
<p id="profileSaveStatus" style="margin-top:10px;color:#64748b;text-align:center;"></p>
|
||||
<p id="profileSaveStatus"></p></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user