Compare commits
2 Commits
de1d3d2d63
...
10592b76c7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10592b76c7 | ||
|
|
af7271e2f8 |
@ -20,126 +20,56 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="chats-main">
|
||||
<main class="chats-main" id="main-content" tabindex="-1">
|
||||
<!-- 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 type="button" class="new-chat-btn" id="newChatBtn" aria-label="New chat" aria-expanded="false" aria-controls="new-chat-panel">
|
||||
<i class="bi bi-pencil-square" aria-hidden="true"></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 class="new-chat-panel" id="new-chat-panel" hidden>
|
||||
<label for="creatorSearch" class="sr-only">Search creator</label>
|
||||
<input type="search" id="creatorSearch" placeholder="Search creator..." autocomplete="off" />
|
||||
<div class="creator-search-results" id="creatorSearchResults" role="listbox" aria-label="Creator results"></div>
|
||||
</div>
|
||||
<div class="chat-list-items" id="chatList" role="list" aria-label="Conversations">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Active Chat (with Alex Chen) -->
|
||||
<!-- Right Column: Active Chat -->
|
||||
<div class="chat-active">
|
||||
<div class="chat-header">
|
||||
<img
|
||||
src="../images/content/creator2.png"
|
||||
alt="Alex Chen"
|
||||
src="../images/content/cat.png"
|
||||
alt=""
|
||||
class="chat-avatar-large"
|
||||
id="activeChatAvatar"
|
||||
/>
|
||||
<div class="chat-header-info">
|
||||
<div class="chat-header-name">Alex Chen</div>
|
||||
<div class="chat-header-name" id="activeChatName">Select a chat</div>
|
||||
<div class="chat-header-status">
|
||||
<span class="online-dot"></span> Online
|
||||
<span class="online-dot" aria-hidden="true"></span>
|
||||
<span id="activeChatStatus">Ready</span>
|
||||
</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 class="chat-messages" id="chatMessages" aria-live="polite" aria-label="Conversation">
|
||||
</div>
|
||||
<form class="chat-input-area" id="chatForm">
|
||||
<input type="text" id="messageInput" placeholder="Type your message..." aria-label="Message" autocomplete="off" />
|
||||
<button type="submit" class="send-btn">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@ -156,12 +86,16 @@
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((link) => {
|
||||
link.classList.remove("active");
|
||||
link.removeAttribute("aria-current");
|
||||
});
|
||||
// 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");
|
||||
if (chatsLink) {
|
||||
chatsLink.classList.add("active");
|
||||
chatsLink.setAttribute("aria-current", "page");
|
||||
}
|
||||
});
|
||||
|
||||
fetch("/topbar.html")
|
||||
@ -170,6 +104,278 @@
|
||||
(data) =>
|
||||
(document.getElementById("topbar-container").innerHTML = data),
|
||||
);
|
||||
|
||||
const STORAGE_KEY = "onlyprompt-chat-conversations";
|
||||
const chatList = document.getElementById("chatList");
|
||||
const chatMessages = document.getElementById("chatMessages");
|
||||
const chatForm = document.getElementById("chatForm");
|
||||
const messageInput = document.getElementById("messageInput");
|
||||
const activeChatAvatar = document.getElementById("activeChatAvatar");
|
||||
const activeChatName = document.getElementById("activeChatName");
|
||||
const activeChatStatus = document.getElementById("activeChatStatus");
|
||||
const newChatBtn = document.getElementById("newChatBtn");
|
||||
const newChatPanel = document.getElementById("new-chat-panel");
|
||||
const creatorSearch = document.getElementById("creatorSearch");
|
||||
const creatorSearchResults = document.getElementById("creatorSearchResults");
|
||||
|
||||
let conversations = loadConversations();
|
||||
let creators = [];
|
||||
let activeConversationId = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function loadConversations() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
||||
return Array.isArray(saved) && saved.length ? saved : demoConversations();
|
||||
} catch {
|
||||
return demoConversations();
|
||||
}
|
||||
}
|
||||
|
||||
function saveConversations() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(conversations));
|
||||
}
|
||||
|
||||
function demoConversations() {
|
||||
const now = Date.now();
|
||||
return [
|
||||
{
|
||||
id: "demo-alex",
|
||||
userId: "demo-alex",
|
||||
name: "Alex Chen",
|
||||
avatar: "../images/content/creator2.png",
|
||||
updatedAt: now - 60000,
|
||||
messages: [
|
||||
{ from: "them", text: "Hey, I liked your last prompt idea.", createdAt: now - 180000 },
|
||||
{ from: "me", text: "Thanks. Which part was useful?", createdAt: now - 120000 },
|
||||
{ from: "them", text: "The structure was easy to adapt.", createdAt: now - 60000 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "demo-mia",
|
||||
userId: "demo-mia",
|
||||
name: "Mia Wong",
|
||||
avatar: "../images/content/creator3.png",
|
||||
updatedAt: now - 86400000,
|
||||
messages: [
|
||||
{ from: "them", text: "Thanks for the prompt tips.", createdAt: now - 86400000 },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const today = new Date();
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
return date.toLocaleDateString([], { month: "short", day: "numeric" });
|
||||
}
|
||||
|
||||
function getLastMessage(conversation) {
|
||||
return conversation.messages.at(-1)?.text || "No messages yet.";
|
||||
}
|
||||
|
||||
function renderChatList() {
|
||||
conversations.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
if (!conversations.length) {
|
||||
chatList.innerHTML = '<div class="chat-empty">Start a chat with a creator.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
chatList.innerHTML = conversations
|
||||
.map((conversation) => `
|
||||
<button type="button"
|
||||
class="chat-item ${conversation.id === activeConversationId ? "active" : ""}"
|
||||
data-chat-id="${escapeHtml(conversation.id)}"
|
||||
aria-label="Open chat with ${escapeHtml(conversation.name)}"
|
||||
${conversation.id === activeConversationId ? 'aria-current="true"' : ""}>
|
||||
<img src="${escapeHtml(conversation.avatar)}" alt="" class="chat-avatar" />
|
||||
<div class="chat-item-info">
|
||||
<div class="chat-name">${escapeHtml(conversation.name)}</div>
|
||||
<div class="chat-last-msg">${escapeHtml(getLastMessage(conversation))}</div>
|
||||
</div>
|
||||
<time class="chat-time" datetime="${new Date(conversation.updatedAt).toISOString()}">${formatTime(conversation.updatedAt)}</time>
|
||||
</button>
|
||||
`)
|
||||
.join("");
|
||||
|
||||
chatList.querySelectorAll("[data-chat-id]").forEach((button) => {
|
||||
button.addEventListener("click", () => selectConversation(button.dataset.chatId));
|
||||
});
|
||||
}
|
||||
|
||||
function renderMessages() {
|
||||
const conversation = conversations.find((item) => item.id === activeConversationId);
|
||||
if (!conversation) {
|
||||
activeChatAvatar.src = "../images/content/cat.png";
|
||||
activeChatName.textContent = "Select a chat";
|
||||
activeChatStatus.textContent = "Ready";
|
||||
chatMessages.innerHTML = '<div class="chat-empty">Choose a conversation or start a new chat.</div>';
|
||||
messageInput.disabled = true;
|
||||
chatForm.querySelector("button").disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
activeChatAvatar.src = conversation.avatar;
|
||||
activeChatName.textContent = conversation.name;
|
||||
activeChatStatus.textContent = "Online";
|
||||
chatMessages.setAttribute("aria-label", `Conversation with ${conversation.name}`);
|
||||
messageInput.disabled = false;
|
||||
chatForm.querySelector("button").disabled = false;
|
||||
|
||||
chatMessages.innerHTML = conversation.messages
|
||||
.map((message) => `
|
||||
<div class="message ${message.from === "me" ? "sent" : "received"}">
|
||||
<div class="message-bubble">${escapeHtml(message.text)}</div>
|
||||
<time class="message-time" datetime="${new Date(message.createdAt).toISOString()}">${formatTime(message.createdAt)}</time>
|
||||
</div>
|
||||
`)
|
||||
.join("");
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
function selectConversation(id) {
|
||||
activeConversationId = id;
|
||||
renderChatList();
|
||||
renderMessages();
|
||||
}
|
||||
|
||||
function startConversation(creator) {
|
||||
const id = `user-${creator.userId}`;
|
||||
let conversation = conversations.find((item) => item.id === id);
|
||||
if (!conversation) {
|
||||
conversation = {
|
||||
id,
|
||||
userId: creator.userId,
|
||||
name: creator.displayName,
|
||||
avatar: creator.avatarUrl || "../images/content/cat.png",
|
||||
updatedAt: Date.now(),
|
||||
messages: [],
|
||||
};
|
||||
conversations.push(conversation);
|
||||
saveConversations();
|
||||
}
|
||||
activeConversationId = id;
|
||||
newChatPanel.hidden = true;
|
||||
newChatBtn.setAttribute("aria-expanded", "false");
|
||||
creatorSearch.value = "";
|
||||
renderCreatorResults();
|
||||
renderChatList();
|
||||
renderMessages();
|
||||
messageInput.focus();
|
||||
}
|
||||
|
||||
function addMessage(text) {
|
||||
const conversation = conversations.find((item) => item.id === activeConversationId);
|
||||
if (!conversation || !text.trim()) return;
|
||||
|
||||
conversation.messages.push({
|
||||
from: "me",
|
||||
text: text.trim(),
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
conversation.updatedAt = Date.now();
|
||||
saveConversations();
|
||||
renderChatList();
|
||||
renderMessages();
|
||||
}
|
||||
|
||||
async function loadCreators() {
|
||||
try {
|
||||
const response = await fetch("/api/v1/profiles?limit=100", {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (!response.ok) throw new Error("Creators could not be loaded.");
|
||||
creators = await response.json();
|
||||
} catch {
|
||||
creators = [
|
||||
{ userId: "demo-alex", displayName: "Alex Chen", slug: "alex", avatarUrl: "../images/content/creator2.png" },
|
||||
{ userId: "demo-mia", displayName: "Mia Wong", slug: "mia", avatarUrl: "../images/content/creator3.png" },
|
||||
{ userId: "demo-tom", displayName: "Tom Rivera", slug: "tom", avatarUrl: "../images/content/creator4.png" },
|
||||
];
|
||||
}
|
||||
renderCreatorResults();
|
||||
}
|
||||
|
||||
function renderCreatorResults() {
|
||||
const search = creatorSearch.value.trim().toLowerCase();
|
||||
const results = creators
|
||||
.filter((creator) =>
|
||||
`${creator.displayName || ""} ${creator.slug || ""}`.toLowerCase().includes(search),
|
||||
)
|
||||
.slice(0, 8);
|
||||
|
||||
if (!results.length) {
|
||||
creatorSearchResults.innerHTML = '<div class="creator-result-empty">No creators found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
creatorSearchResults.innerHTML = results
|
||||
.map((creator) => `
|
||||
<button type="button" class="creator-result" data-user-id="${escapeHtml(creator.userId)}" role="option">
|
||||
<img src="${escapeHtml(creator.avatarUrl || "../images/content/cat.png")}" alt="" />
|
||||
<span>
|
||||
<strong>${escapeHtml(creator.displayName)}</strong>
|
||||
<small>@${escapeHtml(creator.slug || "creator")}</small>
|
||||
</span>
|
||||
</button>
|
||||
`)
|
||||
.join("");
|
||||
|
||||
creatorSearchResults.querySelectorAll("[data-user-id]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const creator = creators.find((item) => item.userId === button.dataset.userId);
|
||||
if (creator) startConversation(creator);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openConversationFromUrl() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const userId = params.get("userId");
|
||||
if (!userId) return false;
|
||||
|
||||
startConversation({
|
||||
userId,
|
||||
displayName: params.get("name") || "Creator",
|
||||
slug: "creator",
|
||||
avatarUrl: params.get("avatar") || "../images/content/cat.png",
|
||||
});
|
||||
history.replaceState(null, "", "/chats.html");
|
||||
return true;
|
||||
}
|
||||
|
||||
newChatBtn.addEventListener("click", () => {
|
||||
const willOpen = newChatPanel.hidden;
|
||||
newChatPanel.hidden = !willOpen;
|
||||
newChatBtn.setAttribute("aria-expanded", String(willOpen));
|
||||
if (willOpen) creatorSearch.focus();
|
||||
});
|
||||
|
||||
creatorSearch.addEventListener("input", renderCreatorResults);
|
||||
chatForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
addMessage(messageInput.value);
|
||||
messageInput.value = "";
|
||||
});
|
||||
|
||||
loadCreators().then(() => {
|
||||
if (!openConversationFromUrl()) {
|
||||
activeConversationId = conversations[0]?.id || null;
|
||||
renderChatList();
|
||||
renderMessages();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -20,31 +20,32 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="creators-main">
|
||||
<main class="creators-main" id="main-content" tabindex="-1">
|
||||
<div class="creators-header">
|
||||
<h1>Discover Creators</h1>
|
||||
<p>Follow your favorite prompt artists and get inspired.</p>
|
||||
</div>
|
||||
|
||||
<div class="filter-buttons">
|
||||
<button class="filter-btn active" data-sort="popular">
|
||||
<div class="filter-buttons" role="group" aria-label="Sort creators">
|
||||
<button type="button" class="filter-btn active" data-sort="popular" aria-pressed="true">
|
||||
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>
|
||||
<button type="button" class="filter-btn" data-sort="prompts" aria-pressed="false">Rising</button>
|
||||
<button type="button" class="filter-btn" data-sort="new" aria-pressed="false">New</button>
|
||||
<button type="button" class="filter-btn" data-sort="rating" aria-pressed="false">Top Rated</button>
|
||||
</div>
|
||||
|
||||
<div class="creators-grid" id="creators-grid"></div>
|
||||
<div class="creators-grid" id="creators-grid" aria-live="polite"></div>
|
||||
|
||||
<div id="creators-empty" class="state-empty">
|
||||
<i class="bi bi-people state-icon"></i>
|
||||
<div id="creators-empty" class="state-empty" role="status" aria-live="polite">
|
||||
<i class="bi bi-people state-icon" aria-hidden="true"></i>
|
||||
<h3 id="creators-empty-title" class="state-title">
|
||||
No creators found
|
||||
</h3>
|
||||
@ -53,8 +54,8 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="creators-error" class="state-error">
|
||||
<i class="bi bi-exclamation-circle state-icon"></i>
|
||||
<div id="creators-error" class="state-error" role="alert" aria-live="assertive">
|
||||
<i class="bi bi-exclamation-circle state-icon" aria-hidden="true"></i>
|
||||
<h3 class="state-title">Could not load creators</h3>
|
||||
<p id="creators-error-msg"></p>
|
||||
</div>
|
||||
@ -70,11 +71,17 @@
|
||||
document.getElementById("sidebar-container").innerHTML = data;
|
||||
document
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((l) => l.classList.remove("active"));
|
||||
.forEach((l) => {
|
||||
l.classList.remove("active");
|
||||
l.removeAttribute("aria-current");
|
||||
});
|
||||
const thirdLink = document.querySelectorAll(
|
||||
"#sidebar-container .sidebar li a",
|
||||
)[2];
|
||||
if (thirdLink) thirdLink.classList.add("active");
|
||||
if (thirdLink) {
|
||||
thirdLink.classList.add("active");
|
||||
thirdLink.setAttribute("aria-current", "page");
|
||||
}
|
||||
});
|
||||
fetch("/topbar.html")
|
||||
.then((r) => r.text())
|
||||
@ -91,27 +98,37 @@
|
||||
}
|
||||
|
||||
function renderCard(c) {
|
||||
const profileHref = `/profile?id=${encodeURIComponent(c.userId)}`;
|
||||
const chatHref = `/chats.html?userId=${encodeURIComponent(c.userId)}&name=${encodeURIComponent(c.displayName)}&avatar=${encodeURIComponent(c.avatarUrl || "../images/content/cat.png")}`;
|
||||
return `
|
||||
<div class="creator-card">
|
||||
<a class="creator-avatar-link" href="${profileHref}" aria-label="Open profile for ${c.displayName}">
|
||||
<img class="creator-avatar"
|
||||
src="${c.avatarUrl || "../images/content/cat.png"}"
|
||||
alt="${c.displayName}"
|
||||
onclick="location.href='/profile?id=${c.userId}'">
|
||||
alt="${c.displayName}">
|
||||
</a>
|
||||
<div class="creator-info">
|
||||
<h3 class="creator-name"
|
||||
onclick="location.href='/profile?id=${c.userId}'">${c.displayName}</h3>
|
||||
<h3 class="creator-name"><a href="${profileHref}">${c.displayName}</a></h3>
|
||||
<div class="creator-handle">@${c.slug}</div>
|
||||
<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>
|
||||
<span><i class="bi bi-puzzle" aria-hidden="true"></i> ${c.promptCount} prompts</span>
|
||||
<span><i class="bi bi-people" aria-hidden="true"></i> ${c.subscribers} subscribers</span>
|
||||
${c.averageRating > 0 ? `<span>${renderStars(c.averageRating)}</span>` : ""}
|
||||
</div>
|
||||
<button class="follow-btn ${c.isFollowing ? "following" : ""}"
|
||||
<div class="creator-actions">
|
||||
<button type="button" class="follow-btn ${c.isFollowing ? "following" : ""}"
|
||||
data-userid="${c.userId}"
|
||||
data-following="${c.isFollowing}">
|
||||
data-following="${c.isFollowing}"
|
||||
aria-pressed="${c.isFollowing}"
|
||||
aria-label="${c.isFollowing ? "Unfollow" : "Follow"} ${c.displayName}">
|
||||
${c.isFollowing ? "Following" : "Follow"}
|
||||
</button>
|
||||
<a class="creator-chat-btn" href="${chatHref}" aria-label="Chat with ${c.displayName}">
|
||||
<i class="bi bi-chat-dots" aria-hidden="true"></i>
|
||||
Chat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@ -136,6 +153,7 @@
|
||||
const nowFollowing = !isFollowing;
|
||||
btn.dataset.following = nowFollowing;
|
||||
btn.textContent = nowFollowing ? "Following" : "Follow";
|
||||
btn.setAttribute("aria-pressed", String(nowFollowing));
|
||||
btn.classList.toggle("following", nowFollowing);
|
||||
}
|
||||
|
||||
@ -208,8 +226,12 @@
|
||||
btn.addEventListener("click", () => {
|
||||
document
|
||||
.querySelectorAll(".filter-btn")
|
||||
.forEach((b) => b.classList.remove("active"));
|
||||
.forEach((b) => {
|
||||
b.classList.remove("active");
|
||||
b.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
btn.classList.add("active");
|
||||
btn.setAttribute("aria-pressed", "true");
|
||||
loadCreators(btn.dataset.sort);
|
||||
});
|
||||
});
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div class="layout">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
@ -25,7 +26,7 @@
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="create-main">
|
||||
<main class="create-main" id="main-content" tabindex="-1">
|
||||
<div class="create-container">
|
||||
<div class="create-header">
|
||||
<h1 id="create-title">Create AI Prompt</h1>
|
||||
@ -36,7 +37,7 @@
|
||||
<!-- Title -->
|
||||
<div class="form-group">
|
||||
<label for="title">Prompt Title *</label>
|
||||
<input type="text" id="title" name="title" placeholder="e.g., Write an inspiring startup story about innovation" required>
|
||||
<input type="text" id="title" name="title" placeholder="e.g., Write an inspiring startup story about innovation" autocomplete="off" required>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
@ -61,8 +62,8 @@
|
||||
<!-- Prompt Content -->
|
||||
<div class="form-group">
|
||||
<label for="promptContent">Prompt Content *</label>
|
||||
<textarea id="promptContent" name="promptContent" rows="6" placeholder="Write your prompt instructions here..." required></textarea>
|
||||
<small class="form-hint">Use clear, step-by-step instructions for the AI.</small>
|
||||
<textarea id="promptContent" name="promptContent" rows="6" placeholder="Write your prompt instructions here..." aria-describedby="promptContentHint" required></textarea>
|
||||
<small class="form-hint" id="promptContentHint">Use clear, step-by-step instructions for the AI.</small>
|
||||
</div>
|
||||
|
||||
<!-- Example Output (Text) -->
|
||||
@ -74,21 +75,22 @@
|
||||
<!-- Example Image (optional) -->
|
||||
<div class="form-group">
|
||||
<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">
|
||||
<img id="previewImg" src="#" alt="Preview">
|
||||
<input type="file" id="exampleImage" name="exampleImage" accept="image/png, image/jpeg, image/jpg" aria-describedby="exampleImageHint">
|
||||
<small class="form-hint" id="exampleImageHint">Upload a PNG or JPG. Preview will appear below.</small>
|
||||
<div id="imagePreview" aria-live="polite">
|
||||
<img id="previewImg" src="#" alt="Selected example image preview">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing (with toggle) -->
|
||||
<div class="form-group pricing-group">
|
||||
<label>Access</label>
|
||||
<div class="pricing-toggle">
|
||||
<button type="button" id="freeBtn" class="price-option active">Free</button>
|
||||
<button type="button" id="tierBtn" class="price-option">Tier</button>
|
||||
<span class="form-label" id="access-label">Access</span>
|
||||
<div class="pricing-toggle" role="group" aria-labelledby="access-label">
|
||||
<button type="button" id="freeBtn" class="price-option active" aria-pressed="true">Free</button>
|
||||
<button type="button" id="tierBtn" class="price-option" aria-pressed="false">Tier</button>
|
||||
</div>
|
||||
<div id="tierField">
|
||||
<label for="subscriptionTier" class="sr-only">Subscription tier</label>
|
||||
<select id="subscriptionTier" name="subscriptionTier">
|
||||
<option value="">No tiers created yet</option>
|
||||
</select>
|
||||
@ -102,7 +104,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"></p></p>
|
||||
<p id="create-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
@ -122,12 +124,16 @@
|
||||
freeBtn.addEventListener('click', () => {
|
||||
freeBtn.classList.add('active');
|
||||
tierBtn.classList.remove('active');
|
||||
freeBtn.setAttribute('aria-pressed', 'true');
|
||||
tierBtn.setAttribute('aria-pressed', 'false');
|
||||
tierField.style.display = 'none';
|
||||
tierSelect.removeAttribute('required');
|
||||
});
|
||||
tierBtn.addEventListener('click', () => {
|
||||
tierBtn.classList.add('active');
|
||||
freeBtn.classList.remove('active');
|
||||
tierBtn.setAttribute('aria-pressed', 'true');
|
||||
freeBtn.setAttribute('aria-pressed', 'false');
|
||||
tierField.style.display = 'grid';
|
||||
tierSelect.setAttribute('required', 'required');
|
||||
});
|
||||
@ -318,10 +324,14 @@
|
||||
// Remove active class from all sidebar links
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
link.removeAttribute('aria-current');
|
||||
});
|
||||
// Optionally set active on "Create New" if it exists, otherwise keep none
|
||||
const createLink = document.querySelector('#sidebar-container a[href="create.html"]');
|
||||
if (createLink) createLink.classList.add('active');
|
||||
if (createLink) {
|
||||
createLink.classList.add('active');
|
||||
createLink.setAttribute('aria-current', 'page');
|
||||
}
|
||||
});
|
||||
|
||||
fetch('/topbar.html')
|
||||
|
||||
@ -18,6 +18,58 @@ body {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 1000;
|
||||
transform: translateY(-160%);
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.skip-link + .skip-link {
|
||||
top: 58px;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
textarea:focus-visible,
|
||||
[tabindex]:focus-visible {
|
||||
outline: 3px solid #2563eb;
|
||||
outline-offset: 3px;
|
||||
outline-style: solid !important;
|
||||
outline-width: 3px !important;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
[aria-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Form errors */
|
||||
.form-error {
|
||||
color: red;
|
||||
|
||||
@ -42,9 +42,81 @@
|
||||
.new-chat-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
font-size: 1.2rem;
|
||||
color: #3b82f6;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.new-chat-btn:hover,
|
||||
.new-chat-btn[aria-expanded="true"] {
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.new-chat-panel {
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.new-chat-panel input {
|
||||
width: 100%;
|
||||
border: 1px solid #dbe2ea;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.creator-search-results {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.creator-result {
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.creator-result:hover,
|
||||
.creator-result:focus-visible {
|
||||
background: #eef2ff;
|
||||
border-color: #c7d2fe;
|
||||
}
|
||||
|
||||
.creator-result img {
|
||||
border-radius: 50%;
|
||||
height: 36px;
|
||||
object-fit: cover;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.creator-result span {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.creator-result small,
|
||||
.creator-result-empty,
|
||||
.chat-empty {
|
||||
color: #64748b;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.creator-result-empty,
|
||||
.chat-empty {
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chat-list-items {
|
||||
@ -53,15 +125,22 @@
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
padding: 16px 20px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
width: 100%;
|
||||
}
|
||||
.chat-item:hover {
|
||||
.chat-item:hover,
|
||||
.chat-item:focus-visible {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.chat-item.active {
|
||||
@ -188,6 +267,7 @@
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #eef2f7;
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
.chat-input-area input {
|
||||
flex: 1;
|
||||
@ -200,6 +280,12 @@
|
||||
.chat-input-area input:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.chat-input-area input:disabled {
|
||||
background: #f8fafc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
background: var(--gradient);
|
||||
border: none;
|
||||
@ -214,6 +300,11 @@
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.chats-main {
|
||||
|
||||
@ -71,11 +71,18 @@
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
.creator-card:hover {
|
||||
.creator-card:hover,
|
||||
.creator-card:focus-within {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
.creator-avatar-link {
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.creator-avatar {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
@ -92,6 +99,15 @@
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.creator-name a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.creator-name a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.creator-handle {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
@ -117,7 +133,14 @@
|
||||
.creator-stats i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.follow-btn {
|
||||
.creator-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.follow-btn,
|
||||
.creator-chat-btn {
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
@ -126,11 +149,25 @@
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
min-height: 34px;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.follow-btn:hover {
|
||||
|
||||
.follow-btn:hover,
|
||||
.creator-chat-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.creator-chat-btn {
|
||||
background: #eef2ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.follow-btn.following {
|
||||
background: transparent;
|
||||
border: 2px solid #94a3b8;
|
||||
@ -171,6 +208,10 @@
|
||||
.follow-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.creator-actions,
|
||||
.creator-chat-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Star rating in creator cards */
|
||||
@ -181,9 +222,3 @@
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Clickable elements */
|
||||
.creator-card .creator-avatar,
|
||||
.creator-card .creator-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -49,7 +49,8 @@
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.form-group label {
|
||||
.form-group label,
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
@ -72,11 +72,20 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.post-card:hover {
|
||||
.post-card:hover,
|
||||
.post-card:focus-within {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
.post-card-link {
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Post Header */
|
||||
.post-header {
|
||||
display: flex;
|
||||
|
||||
@ -146,6 +146,7 @@
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #f59e0b;
|
||||
text-decoration: none;
|
||||
}
|
||||
.prompt-rating span:first-child i {
|
||||
color: #f59e0b;
|
||||
@ -251,6 +252,7 @@
|
||||
.market-rating-none {
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
.market-rating-clickable {
|
||||
cursor: pointer;
|
||||
|
||||
@ -158,7 +158,19 @@
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.profile-prompt-card:focus-within,
|
||||
.profile-prompt-card:hover {
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
.profile-prompt-link {
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.profile-prompt-img {
|
||||
width: 72px;
|
||||
@ -196,6 +208,7 @@
|
||||
padding: 6px 10px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Inner spacing for the profile card ─────────────────────────────── */
|
||||
|
||||
@ -154,7 +154,18 @@
|
||||
}
|
||||
|
||||
.sidebar .nav-text,
|
||||
.sidebar-logout .nav-text,
|
||||
.sidebar-logout .nav-text {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.logout-arrow {
|
||||
display: none;
|
||||
}
|
||||
@ -163,6 +174,7 @@
|
||||
.sidebar-logout {
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar a.active {
|
||||
|
||||
@ -46,6 +46,11 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.topbar-search:focus-within {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
|
||||
}
|
||||
|
||||
.topbar-search input::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
|
||||
@ -20,13 +20,14 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="feed-main">
|
||||
<main class="feed-main" id="main-content" tabindex="-1">
|
||||
<!-- Optional: Feed Header -->
|
||||
<div class="feed-header">
|
||||
<h1>Feed</h1>
|
||||
@ -34,39 +35,43 @@
|
||||
</div>
|
||||
|
||||
<!-- Filter Buttons -->
|
||||
<div class="filter-buttons">
|
||||
<div class="filter-buttons" role="group" aria-label="Sort feed">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-btn active"
|
||||
data-sort="date"
|
||||
data-ascending="false"
|
||||
aria-pressed="true"
|
||||
>
|
||||
Recent
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="filter-btn"
|
||||
data-sort="rating"
|
||||
data-ascending="false"
|
||||
aria-pressed="false"
|
||||
>
|
||||
Top Rated
|
||||
</button>
|
||||
<button class="filter-btn" data-sort="date" data-ascending="true">
|
||||
<button type="button" class="filter-btn" data-sort="date" data-ascending="true" aria-pressed="false">
|
||||
Oldest
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Posts Grid -->
|
||||
<div class="posts-grid" id="posts-grid"></div>
|
||||
<div class="posts-grid" id="posts-grid" aria-live="polite"></div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="feed-empty" class="state-empty">
|
||||
<i class="bi bi-inbox state-icon"></i>
|
||||
<div id="feed-empty" class="state-empty" role="status" aria-live="polite">
|
||||
<i class="bi bi-inbox state-icon" aria-hidden="true"></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" class="state-error">
|
||||
<i class="bi bi-exclamation-circle state-icon"></i>
|
||||
<div id="feed-error" class="state-error" role="alert" aria-live="assertive">
|
||||
<i class="bi bi-exclamation-circle state-icon" aria-hidden="true"></i>
|
||||
<h3 class="state-title">Could not load feed</h3>
|
||||
<p id="feed-error-msg"></p>
|
||||
</div>
|
||||
@ -82,11 +87,17 @@
|
||||
document.getElementById("sidebar-container").innerHTML = data;
|
||||
document
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((link) => link.classList.remove("active"));
|
||||
.forEach((link) => {
|
||||
link.classList.remove("active");
|
||||
link.removeAttribute("aria-current");
|
||||
});
|
||||
const firstLink = document.querySelectorAll(
|
||||
"#sidebar-container .sidebar li a",
|
||||
)[0];
|
||||
if (firstLink) firstLink.classList.add("active");
|
||||
if (firstLink) {
|
||||
firstLink.classList.add("active");
|
||||
firstLink.setAttribute("aria-current", "page");
|
||||
}
|
||||
});
|
||||
|
||||
fetch("/topbar.html")
|
||||
@ -117,9 +128,9 @@
|
||||
function renderStars(rating) {
|
||||
if (rating == null) return "";
|
||||
const stars = Math.round(rating);
|
||||
return `<span class="post-rating" title="${rating.toFixed(1)} / 5">
|
||||
${'<i class="bi bi-star-fill"></i>'.repeat(stars)}${'<i class="bi bi-star"></i>'.repeat(5 - stars)}
|
||||
<span>${rating.toFixed(1)}</span>
|
||||
return `<span class="post-rating" title="${rating.toFixed(1)} / 5" aria-label="${rating.toFixed(1)} out of 5 stars">
|
||||
${'<i class="bi bi-star-fill" aria-hidden="true"></i>'.repeat(stars)}${'<i class="bi bi-star" aria-hidden="true"></i>'.repeat(5 - stars)}
|
||||
<span aria-hidden="true">${rating.toFixed(1)}</span>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
@ -136,28 +147,30 @@
|
||||
const liked = prompt.isLiked;
|
||||
const saved = prompt.isSaved;
|
||||
return `
|
||||
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='${profileUrl(prompt.creatorId)}'">
|
||||
<article class="post-card${locked ? " post-locked" : ""}">
|
||||
<a class="post-card-link" href="${profileUrl(prompt.creatorId)}" aria-label="Open profile for ${prompt.creatorName}">
|
||||
<div class="post-header">
|
||||
<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>
|
||||
<span class="post-date"><time datetime="${prompt.timeStamp}">${timeAgo(prompt.timeStamp)}</time></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">${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 ?? "Subscription"} tier required</p>` : ""}
|
||||
${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill" aria-hidden="true"></i> ${prompt.tierName ?? "Subscription"} tier required</p>` : ""}
|
||||
${renderStars(prompt.averageRating)}
|
||||
</div>
|
||||
</a>
|
||||
<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 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 type="button" class="action-btn like-btn ${liked ? "active" : ""}" aria-pressed="${liked}" aria-label="${liked ? "Unlike" : "Like"} ${prompt.title}. ${prompt.likeCount || 0} likes" onclick="toggleLike(event, '${prompt.id}', ${liked})"><i class="bi ${liked ? "bi-heart-fill" : "bi-heart"}" aria-hidden="true"></i> <span>Like (${prompt.likeCount || 0})</span></button>
|
||||
<button type="button" class="action-btn comment-btn" aria-label="Review ${prompt.title}" onclick="event.stopPropagation(); location.href='/post-detail?id=${prompt.id}#rating-section'"><i class="bi bi-chat" aria-hidden="true"></i> <span>Review</span></button>
|
||||
<button type="button" class="action-btn share-btn" aria-label="Share ${prompt.title}" onclick="sharePrompt(event, '${prompt.id}')"><i class="bi bi-share" aria-hidden="true"></i> <span>Share</span></button>
|
||||
<button type="button" class="action-btn save-btn ${saved ? "active" : ""}" aria-pressed="${saved}" aria-label="${saved ? "Remove saved" : "Save"} ${prompt.title}. ${prompt.saveCount || 0} saves" onclick="toggleSave(event, '${prompt.id}', ${saved})"><i class="bi ${saved ? "bi-bookmark-fill" : "bi-bookmark"}" aria-hidden="true"></i> <span>Save (${prompt.saveCount || 0})</span></button>
|
||||
</div>
|
||||
</div>`;
|
||||
</article>`;
|
||||
}
|
||||
|
||||
window.toggleLike = async function (event, id, isLiked) {
|
||||
@ -251,8 +264,12 @@
|
||||
btn.addEventListener("click", () => {
|
||||
document
|
||||
.querySelectorAll(".filter-btn")
|
||||
.forEach((b) => b.classList.remove("active"));
|
||||
.forEach((b) => {
|
||||
b.classList.remove("active");
|
||||
b.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
btn.classList.add("active");
|
||||
btn.setAttribute("aria-pressed", "true");
|
||||
loadFeed(btn.dataset.sort, btn.dataset.ascending === "true");
|
||||
});
|
||||
});
|
||||
|
||||
@ -13,8 +13,12 @@ async function redirectIfAlreadySignedIn() {
|
||||
|
||||
function togglePassword() {
|
||||
const passwordInput = document.getElementById('password');
|
||||
const togglePasswordButton = document.getElementById('togglePassword');
|
||||
const newInputType = passwordInput.type === 'password' ? 'text' : 'password';
|
||||
passwordInput.type = newInputType;
|
||||
const isVisible = newInputType === 'text';
|
||||
togglePasswordButton.textContent = isVisible ? 'Hide' : 'Show';
|
||||
togglePasswordButton.setAttribute('aria-pressed', String(isVisible));
|
||||
}
|
||||
|
||||
async function submitLoginForm(){
|
||||
|
||||
@ -90,7 +90,7 @@ export async function postFormAndRenderAsync(
|
||||
}
|
||||
|
||||
const genericFormErrorTemplate = new Template(`
|
||||
<div class="form-error">
|
||||
<div class="form-error" role="alert" aria-live="assertive">
|
||||
{{ $this }}
|
||||
</div>
|
||||
`);
|
||||
@ -109,7 +109,7 @@ function handleGenericFormError(response, responseText, form) {
|
||||
}
|
||||
|
||||
const validationErrorTemplate = new Template(`
|
||||
<div class="form-error">
|
||||
<div class="form-error" role="alert" aria-live="assertive">
|
||||
<ul>
|
||||
@for(error of $this) {
|
||||
<li class="error">{{error}}</li>
|
||||
@ -119,7 +119,7 @@ const validationErrorTemplate = new Template(`
|
||||
`);
|
||||
|
||||
const unknownInputErrorTemplate = new Template(`
|
||||
<div class="form-error">
|
||||
<div class="form-error" role="alert" aria-live="assertive">
|
||||
<p>An error occurred with the following fields:</p>
|
||||
@for(field, errors of Object.entries($this)) {
|
||||
<ul>
|
||||
|
||||
@ -5,6 +5,18 @@ async function signupAsync(params) {
|
||||
await sendFormAsync(form);
|
||||
}
|
||||
|
||||
function togglePassword() {
|
||||
const passwordInput = document.getElementById('password');
|
||||
const togglePasswordButton = document.getElementById('togglePassword');
|
||||
const newInputType = passwordInput.type === 'password' ? 'text' : 'password';
|
||||
passwordInput.type = newInputType;
|
||||
const isVisible = newInputType === 'text';
|
||||
togglePasswordButton.textContent = isVisible ? 'Hide' : 'Show';
|
||||
togglePasswordButton.setAttribute('aria-pressed', String(isVisible));
|
||||
}
|
||||
|
||||
document.getElementById('togglePassword')?.addEventListener('click', togglePassword);
|
||||
|
||||
const signupForm = document.getElementById('signupForm');
|
||||
signupForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault(); // Prevent the default form submission
|
||||
|
||||
@ -20,8 +20,9 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<!-- Main container for the login page (CSS layout) -->
|
||||
<main class="login-page">
|
||||
<main class="login-page" id="main-content" tabindex="-1">
|
||||
<!-- White login card -->
|
||||
<section class="login-card">
|
||||
<!-- Logo container -->
|
||||
@ -55,9 +56,10 @@
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
>
|
||||
<button type="button" id="togglePassword" class="toggle-password">
|
||||
<button type="button" id="togglePassword" class="toggle-password" aria-controls="password" aria-pressed="false">
|
||||
Show <!-- Click to show/hide password -->
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -19,13 +19,14 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="marketplace-main">
|
||||
<main class="marketplace-main" id="main-content" tabindex="-1">
|
||||
<!-- Header -->
|
||||
<div class="marketplace-header">
|
||||
<h1>Marketplace</h1>
|
||||
@ -34,8 +35,8 @@
|
||||
|
||||
<!-- Filter + Sort Row -->
|
||||
<div class="filter-sort-row">
|
||||
<div class="filter-buttons" id="category-filters">
|
||||
<button class="filter-btn active" data-category="">All</button>
|
||||
<div class="filter-buttons" id="category-filters" role="group" aria-label="Filter prompts by category">
|
||||
<button type="button" class="filter-btn active" data-category="" aria-pressed="true">All</button>
|
||||
</div>
|
||||
<select
|
||||
class="sort-dropdown"
|
||||
@ -53,18 +54,18 @@
|
||||
</div>
|
||||
|
||||
<!-- Prompts Grid -->
|
||||
<div class="prompts-grid" id="prompts-grid"></div>
|
||||
<div class="prompts-grid" id="prompts-grid" aria-live="polite"></div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="market-empty" class="state-empty">
|
||||
<i class="bi bi-bag-x state-icon"></i>
|
||||
<div id="market-empty" class="state-empty" role="status" aria-live="polite">
|
||||
<i class="bi bi-bag-x state-icon" aria-hidden="true"></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" class="state-error">
|
||||
<i class="bi bi-exclamation-circle state-icon"></i>
|
||||
<div id="market-error" class="state-error" role="alert" aria-live="assertive">
|
||||
<i class="bi bi-exclamation-circle state-icon" aria-hidden="true"></i>
|
||||
<h3 class="state-title">Could not load prompts</h3>
|
||||
<p id="market-error-msg"></p>
|
||||
</div>
|
||||
@ -80,11 +81,17 @@
|
||||
document.getElementById("sidebar-container").innerHTML = data;
|
||||
document
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((l) => l.classList.remove("active"));
|
||||
.forEach((l) => {
|
||||
l.classList.remove("active");
|
||||
l.removeAttribute("aria-current");
|
||||
});
|
||||
const link = document.querySelectorAll(
|
||||
"#sidebar-container .sidebar li a",
|
||||
)[1];
|
||||
if (link) link.classList.add("active");
|
||||
if (link) {
|
||||
link.classList.add("active");
|
||||
link.setAttribute("aria-current", "page");
|
||||
}
|
||||
});
|
||||
fetch("/topbar.html")
|
||||
.then((r) => r.text())
|
||||
@ -113,15 +120,20 @@
|
||||
promptId = null,
|
||||
locked = false,
|
||||
) {
|
||||
const target =
|
||||
const href =
|
||||
promptId && !locked
|
||||
? ` onclick="location.href='/post-detail?id=${promptId}#rating-section'" title="View reviews" class="market-rating-clickable"`
|
||||
? `/post-detail?id=${encodeURIComponent(promptId)}#rating-section`
|
||||
: "";
|
||||
if (rating == null)
|
||||
return `<span${target} class="market-rating-none">No reviews yet</span>`;
|
||||
return href
|
||||
? `<a href="${href}" title="View reviews" class="market-rating-none market-rating-clickable">No reviews yet</a>`
|
||||
: `<span 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 class="market-rating-stars">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> ${rating.toFixed(1)} (${reviewCount} ${label})</span>`;
|
||||
const content = `<span class="market-rating-stars" aria-hidden="true">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> <span aria-label="${rating.toFixed(1)} out of 5 stars">${rating.toFixed(1)}</span> (${reviewCount} ${label})`;
|
||||
return href
|
||||
? `<a class="prompt-rating market-rating-clickable" href="${href}" title="View reviews">${content}</a>`
|
||||
: `<span class="prompt-rating">${content}</span>`;
|
||||
}
|
||||
|
||||
function promptPrice(prompt) {
|
||||
@ -179,7 +191,7 @@
|
||||
<div class="market-card-header">
|
||||
<div class="market-card-avatar">${p.creatorName.charAt(0).toUpperCase()}</div>
|
||||
<span class="prompt-author">@${p.creatorName}</span>
|
||||
<span class="market-card-time">${timeAgo(p.timeStamp)}</span>
|
||||
<span class="market-card-time"><time datetime="${p.timeStamp}">${timeAgo(p.timeStamp)}</time></span>
|
||||
</div>
|
||||
<h3 class="prompt-title">${p.title}</h3>
|
||||
<p class="prompt-description">${p.description || "No description yet."}</p>
|
||||
@ -188,13 +200,13 @@
|
||||
<div class="prompt-actions">
|
||||
${
|
||||
locked
|
||||
? `<button class="buy-btn buy-btn-locked" onclick='subscribeToPromptTier(${JSON.stringify(p)})'><i class="bi bi-lock-fill"></i> Subscribe</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>`
|
||||
? `<button type="button" class="buy-btn buy-btn-locked" aria-label="Subscribe to unlock ${p.title}" onclick='subscribeToPromptTier(${JSON.stringify(p)})'><i class="bi bi-lock-fill" aria-hidden="true"></i> Subscribe</button>`
|
||||
: `<button type="button" class="buy-btn buy-btn-unlocked" aria-label="Access ${p.title}" onclick="location.href='/post-detail?id=${p.id}'">Access <i class="bi bi-unlock-fill" aria-hidden="true"></i></button>`
|
||||
}
|
||||
${
|
||||
locked
|
||||
? `<button class="details-btn" disabled><i class="bi bi-lock-fill"></i> Details</button>`
|
||||
: `<button class="details-btn" onclick="location.href='/post-detail?id=${p.id}'">View Details</button>`
|
||||
? `<button type="button" class="details-btn" disabled aria-label="Details for ${p.title} are locked"><i class="bi bi-lock-fill" aria-hidden="true"></i> Details</button>`
|
||||
: `<button type="button" class="details-btn" aria-label="View details for ${p.title}" onclick="location.href='/post-detail?id=${p.id}'">View Details</button>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -258,14 +270,20 @@
|
||||
const container = document.getElementById("category-filters");
|
||||
cats.forEach((c) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "filter-btn";
|
||||
btn.dataset.category = c.slug;
|
||||
btn.setAttribute("aria-pressed", "false");
|
||||
btn.textContent = c.name;
|
||||
btn.addEventListener("click", () => {
|
||||
document
|
||||
.querySelectorAll("#category-filters .filter-btn")
|
||||
.forEach((b) => b.classList.remove("active"));
|
||||
.forEach((b) => {
|
||||
b.classList.remove("active");
|
||||
b.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
btn.classList.add("active");
|
||||
btn.setAttribute("aria-pressed", "true");
|
||||
activeCategory = c.slug;
|
||||
loadPrompts();
|
||||
});
|
||||
@ -280,8 +298,12 @@
|
||||
.addEventListener("click", function () {
|
||||
document
|
||||
.querySelectorAll("#category-filters .filter-btn")
|
||||
.forEach((b) => b.classList.remove("active"));
|
||||
.forEach((b) => {
|
||||
b.classList.remove("active");
|
||||
b.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
this.classList.add("active");
|
||||
this.setAttribute("aria-pressed", "true");
|
||||
activeCategory = "";
|
||||
loadPrompts();
|
||||
});
|
||||
|
||||
@ -19,26 +19,27 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="post-detail-main">
|
||||
<main class="post-detail-main" id="main-content" tabindex="-1">
|
||||
<div class="post-detail-container" id="detail-content">
|
||||
<!-- Loading -->
|
||||
<div id="detail-loading">
|
||||
<i class="bi bi-hourglass-split state-icon"></i>
|
||||
<i class="bi bi-hourglass-split state-icon" aria-hidden="true"></i>
|
||||
<p>Loading prompt...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div id="detail-error" class="state-error">
|
||||
<i class="bi bi-exclamation-circle state-icon"></i>
|
||||
<div id="detail-error" class="state-error" role="alert" aria-live="assertive">
|
||||
<i class="bi bi-exclamation-circle state-icon" aria-hidden="true"></i>
|
||||
<h3 id="detail-error-title">Prompt not found</h3>
|
||||
<p id="detail-error-msg"></p>
|
||||
<button onclick="history.back()" class="detail-back-btn">
|
||||
<button type="button" onclick="history.back()" class="detail-back-btn">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
@ -55,7 +56,7 @@
|
||||
</div>
|
||||
<div class="detail-actions-right">
|
||||
<span id="tier-badge"></span>
|
||||
<button id="edit-prompt-btn">Edit</button>
|
||||
<button type="button" id="edit-prompt-btn">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="post-title" id="prompt-title"></h1>
|
||||
@ -102,7 +103,7 @@
|
||||
|
||||
<!-- Locked section (shown instead of prompt if no access) -->
|
||||
<div id="locked-section">
|
||||
<i class="bi bi-lock-fill locked-icon"></i>
|
||||
<i class="bi bi-lock-fill locked-icon" aria-hidden="true"></i>
|
||||
<h3 class="locked-title">
|
||||
This prompt requires a subscription
|
||||
</h3>
|
||||
@ -110,7 +111,7 @@
|
||||
Subscribe to <strong id="locked-creator"></strong> to access
|
||||
this prompt.
|
||||
</p>
|
||||
<button id="locked-subscribe-btn">
|
||||
<button type="button" id="locked-subscribe-btn">
|
||||
Subscribe <span id="locked-tier-name"></span>
|
||||
</button>
|
||||
</div>
|
||||
@ -128,26 +129,30 @@
|
||||
<div
|
||||
class="review-star-input"
|
||||
id="review-star-input"
|
||||
role="group"
|
||||
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>
|
||||
<button type="button" aria-pressed="false" aria-label="1 star" data-rating="1">☆</button>
|
||||
<button type="button" aria-pressed="false" aria-label="2 stars" data-rating="2">☆</button>
|
||||
<button type="button" aria-pressed="false" aria-label="3 stars" data-rating="3">☆</button>
|
||||
<button type="button" aria-pressed="false" aria-label="4 stars" data-rating="4">☆</button>
|
||||
<button type="button" aria-pressed="false" aria-label="5 stars" data-rating="5">☆</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="review-comment"
|
||||
maxlength="200"
|
||||
rows="3"
|
||||
placeholder="Write a short comment..."
|
||||
aria-label="Review comment"
|
||||
aria-describedby="review-comment-hint"
|
||||
></textarea>
|
||||
<p id="review-comment-hint" class="sr-only">Optional comment, maximum 200 characters.</p>
|
||||
<button type="button" id="submit-review-btn">
|
||||
Submit Review
|
||||
</button>
|
||||
<p id="review-message"></p>
|
||||
<p id="review-message" role="status" aria-live="polite"></p>
|
||||
</div>
|
||||
<div class="reviews-list" id="reviews-list">
|
||||
<div class="reviews-list" id="reviews-list" aria-live="polite">
|
||||
<p class="detail-loading-text">Loading reviews...</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -164,7 +169,10 @@
|
||||
document.getElementById("sidebar-container").innerHTML = data;
|
||||
document
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((l) => l.classList.remove("active"));
|
||||
.forEach((l) => {
|
||||
l.classList.remove("active");
|
||||
l.removeAttribute("aria-current");
|
||||
});
|
||||
});
|
||||
fetch("/topbar.html")
|
||||
.then((r) => r.text())
|
||||
@ -229,8 +237,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"} 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>`;
|
||||
`<i class="bi ${p.isLiked ? "bi-heart-fill" : "bi-heart"} detail-heart-icon" aria-hidden="true"></i> ${p.likeCount || 0} likes
|
||||
<span class="detail-bookmark-span"><i class="bi ${p.isSaved ? "bi-bookmark-fill" : "bi-bookmark"} detail-bookmark-icon" aria-hidden="true"></i> ${p.saveCount || 0} saves</span>`;
|
||||
renderExamples(p);
|
||||
renderOwnerActions(p);
|
||||
setupReviewSection(p);
|
||||
@ -242,7 +250,7 @@
|
||||
const price = p.tierMonthlyPrice == null
|
||||
? ""
|
||||
: ` - $${Number(p.tierMonthlyPrice).toFixed(2)}/mo`;
|
||||
badge.innerHTML = `<span class="tier-badge-tier"><i class="bi bi-lock-fill"></i> ${p.tierName}${price}</span>`;
|
||||
badge.innerHTML = `<span class="tier-badge-tier"><i class="bi bi-lock-fill" aria-hidden="true"></i> ${p.tierName}${price}</span>`;
|
||||
} else {
|
||||
badge.innerHTML = `<span class="tier-badge-free">Free</span>`;
|
||||
}
|
||||
@ -255,9 +263,9 @@
|
||||
<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"} 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>`;
|
||||
`<i class="bi ${p.isLiked ? "bi-heart-fill" : "bi-heart"} detail-heart-icon" aria-hidden="true"></i> ${p.likeCount || 0} likes
|
||||
<span class="detail-bookmark-span"><i class="bi ${p.isSaved ? "bi-bookmark-fill" : "bi-bookmark"} detail-bookmark-icon" aria-hidden="true"></i> ${p.saveCount || 0} saves</span>
|
||||
<span class="detail-bookmark-span"><i class="bi bi-star-fill detail-bookmark-icon" aria-hidden="true"></i> ${p.averageRating.toFixed(1)} (${p.reviewCount || 0})</span>`;
|
||||
} else {
|
||||
document.getElementById("rating-display").innerHTML =
|
||||
'<span class="rating-none">No ratings yet</span>';
|
||||
@ -334,6 +342,8 @@
|
||||
.forEach((button) => {
|
||||
const value = Number(button.dataset.rating);
|
||||
button.textContent = value <= rating ? "★" : "☆";
|
||||
button.setAttribute("aria-pressed", String(value === rating));
|
||||
button.setAttribute("aria-label", `${value} ${value === 1 ? "star" : "stars"}${value === rating ? ", selected" : ""}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -20,17 +20,19 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="login-card profile-main">
|
||||
<main class="login-card profile-main" id="main-content" tabindex="-1">
|
||||
<section class="profile-header">
|
||||
<img
|
||||
id="profileAvatar"
|
||||
src="../images/content/cat.png"
|
||||
alt="Profile avatar"
|
||||
class="profile-avatar"
|
||||
/>
|
||||
|
||||
@ -38,7 +40,7 @@
|
||||
<h1 id="profileDisplayName">Loading...</h1>
|
||||
<div id="profileSlug">
|
||||
@profile
|
||||
<i class="bi bi-patch-check-fill profile-badge-icon"></i>
|
||||
<i class="bi bi-patch-check-fill profile-badge-icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div id="profileBio">Loading profile...</div>
|
||||
@ -54,15 +56,16 @@
|
||||
|
||||
<div id="profileActions">
|
||||
<button
|
||||
type="button"
|
||||
id="primaryProfileButton"
|
||||
class="login-button"
|
||||
onclick="location.href = 'settings.html'"
|
||||
>
|
||||
<i class="bi bi-gear"></i>
|
||||
<i class="bi bi-gear" aria-hidden="true"></i>
|
||||
Edit Profile
|
||||
</button>
|
||||
<button id="shareProfileButton" class="login-button">
|
||||
<i class="bi bi-share"></i>
|
||||
<button type="button" id="shareProfileButton" class="login-button">
|
||||
<i class="bi bi-share" aria-hidden="true"></i>
|
||||
Share Profile
|
||||
</button>
|
||||
<a
|
||||
@ -70,19 +73,22 @@
|
||||
class="login-button"
|
||||
href="subscription-tiers.html"
|
||||
>
|
||||
<i class="bi bi-gem"></i>
|
||||
<i class="bi bi-gem" aria-hidden="true"></i>
|
||||
Manage Tiers
|
||||
</a>
|
||||
<div id="creatorTierList"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="profile-tabs">
|
||||
<nav class="profile-tabs" role="tablist" aria-label="Profile prompt lists">
|
||||
<button
|
||||
type="button"
|
||||
class="profile-tab active"
|
||||
data-tab="mine"
|
||||
id="myPromptsTab"
|
||||
role="tab"
|
||||
aria-selected="true"
|
||||
aria-controls="profile-prompts-grid"
|
||||
>
|
||||
My Prompts
|
||||
</button>
|
||||
@ -91,6 +97,9 @@
|
||||
class="profile-tab"
|
||||
data-tab="favorites"
|
||||
id="favoritesTab"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
aria-controls="profile-prompts-grid"
|
||||
>
|
||||
Favorites
|
||||
</button>
|
||||
@ -99,12 +108,15 @@
|
||||
class="profile-tab"
|
||||
data-tab="saved"
|
||||
id="savedTab"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
aria-controls="profile-prompts-grid"
|
||||
>
|
||||
Saved
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<section id="profile-prompts-grid">
|
||||
<section id="profile-prompts-grid" role="tabpanel" aria-live="polite" aria-labelledby="myPromptsTab">
|
||||
<div class="profile-grid-loading">Loading prompts...</div>
|
||||
</section>
|
||||
</main>
|
||||
@ -121,12 +133,16 @@
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((link) => {
|
||||
link.classList.remove("active");
|
||||
link.removeAttribute("aria-current");
|
||||
});
|
||||
// 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");
|
||||
if (profileLink) {
|
||||
profileLink.classList.add("active");
|
||||
profileLink.setAttribute("aria-current", "page");
|
||||
}
|
||||
});
|
||||
|
||||
fetch("/topbar.html")
|
||||
@ -184,7 +200,7 @@
|
||||
|
||||
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>`;
|
||||
profileSlug.innerHTML = `@${profile.user?.userName || profile.slug || "profile"} <i class="bi bi-patch-check-fill profile-badge-icon" aria-hidden="true"></i>`;
|
||||
profileBio.textContent = profile.bio || "No bio yet.";
|
||||
profileSpecialities.textContent =
|
||||
profile.specialities || "No specialities added yet.";
|
||||
@ -204,7 +220,7 @@
|
||||
|
||||
profileDisplayName.textContent =
|
||||
prompt.creatorName || "Creator Profile";
|
||||
profileSlug.innerHTML = `@${prompt.creatorName || "creator"} <i class="bi bi-patch-check-fill profile-badge-icon"></i>`;
|
||||
profileSlug.innerHTML = `@${prompt.creatorName || "creator"} <i class="bi bi-patch-check-fill profile-badge-icon" aria-hidden="true"></i>`;
|
||||
profileBio.textContent = "No bio yet.";
|
||||
profileSpecialities.textContent = "";
|
||||
profileRating.textContent = Number(prompt.averageRating || 0).toFixed(
|
||||
@ -294,18 +310,20 @@
|
||||
? "No ratings"
|
||||
: prompt.averageRating.toFixed(1);
|
||||
return `
|
||||
<div onclick="location.href='/post-detail?id=${prompt.id}'" class="profile-prompt-card">
|
||||
<article class="profile-prompt-card">
|
||||
<a class="profile-prompt-link" href="/post-detail?id=${encodeURIComponent(prompt.id)}" aria-label="Open prompt ${prompt.title}">
|
||||
<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>
|
||||
<span><i class="bi bi-star" aria-hidden="true"></i> ${rating}</span>
|
||||
${prompt.creatorName ? `<span>@${prompt.creatorName}</span>` : ""}
|
||||
${showEdit ? `<button onclick="event.stopPropagation(); location.href='/create?id=${prompt.id}'" class="profile-prompt-edit-btn">Edit</button>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
</a>
|
||||
${showEdit ? `<button type="button" onclick="location.href='/create?id=${encodeURIComponent(prompt.id)}'" class="profile-prompt-edit-btn" aria-label="Edit ${prompt.title}">Edit</button>` : ""}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function renderPromptList(prompts, emptyText, options = {}) {
|
||||
@ -322,7 +340,9 @@
|
||||
function updateTabs() {
|
||||
document.querySelectorAll(".profile-tab").forEach((tab) => {
|
||||
tab.classList.toggle("active", tab.dataset.tab === activeProfileTab);
|
||||
tab.setAttribute("aria-selected", String(tab.dataset.tab === activeProfileTab));
|
||||
});
|
||||
profilePromptsGrid.setAttribute("aria-labelledby", activeProfileTab === "favorites" ? "favoritesTab" : activeProfileTab === "saved" ? "savedTab" : "myPromptsTab");
|
||||
|
||||
const liked = allPrompts.filter((prompt) =>
|
||||
isPromptMarked("liked", prompt.id),
|
||||
@ -347,7 +367,7 @@
|
||||
function updateProfileMode() {
|
||||
if (isOwnProfile) {
|
||||
profileActions.style.display = "flex";
|
||||
primaryProfileButton.innerHTML = '<i class="bi bi-gear"></i> Edit Profile';
|
||||
primaryProfileButton.innerHTML = '<i class="bi bi-gear" aria-hidden="true"></i> Edit Profile';
|
||||
primaryProfileButton.disabled = false;
|
||||
primaryProfileButton.onclick = () =>
|
||||
(location.href = "settings.html");
|
||||
@ -389,7 +409,7 @@
|
||||
${creatorSubscriptionTiers
|
||||
.map(
|
||||
(tier) => `
|
||||
<button type="button" class="profile-tier-option ${currentSubscriptionTier?.level === tier.level ? "active" : ""}" data-tier-level="${tier.level}">
|
||||
<button type="button" class="profile-tier-option ${currentSubscriptionTier?.level === tier.level ? "active" : ""}" data-tier-level="${tier.level}" aria-pressed="${currentSubscriptionTier?.level === tier.level}">
|
||||
<span>
|
||||
<strong>${tier.name}</strong>
|
||||
<small>Level ${tier.level}</small>
|
||||
@ -534,9 +554,9 @@
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
shareProfileButton.innerHTML = '<i class="bi bi-check2"></i> Copied';
|
||||
shareProfileButton.innerHTML = '<i class="bi bi-check2" aria-hidden="true"></i> Copied';
|
||||
setTimeout(
|
||||
() => (shareProfileButton.innerHTML = '<i class="bi bi-share"></i> Share Profile'),
|
||||
() => (shareProfileButton.innerHTML = '<i class="bi bi-share" aria-hidden="true"></i> Share Profile'),
|
||||
1200,
|
||||
);
|
||||
} catch {
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div class="layout">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
@ -25,7 +26,7 @@
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="settings-main">
|
||||
<main class="settings-main" id="main-content" tabindex="-1">
|
||||
<div class="settings-container">
|
||||
<div class="settings-header">
|
||||
<h1>Settings</h1>
|
||||
@ -33,21 +34,21 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="settings-tabs">
|
||||
<button class="tab-btn active" data-tab="profile">Profile</button>
|
||||
<button class="tab-btn" data-tab="security">Security</button>
|
||||
<button class="tab-btn" data-tab="notifications">Notifications</button>
|
||||
<div class="settings-tabs" role="tablist" aria-label="Settings sections">
|
||||
<button type="button" class="tab-btn active" data-tab="profile" id="profileTabButton" role="tab" aria-selected="true" aria-controls="profileTab">Profile</button>
|
||||
<button type="button" class="tab-btn" data-tab="security" id="securityTabButton" role="tab" aria-selected="false" aria-controls="securityTab">Security</button>
|
||||
<button type="button" class="tab-btn" data-tab="notifications" id="notificationsTabButton" role="tab" aria-selected="false" aria-controls="notificationsTab">Notifications</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Profile -->
|
||||
<div id="profileTab" class="tab-content active">
|
||||
<div id="profileTab" class="tab-content active" role="tabpanel" aria-labelledby="profileTabButton">
|
||||
<form class="settings-form" id="profileSettingsForm">
|
||||
<div class="form-group">
|
||||
<label for="avatar">Profile Picture</label>
|
||||
<div class="avatar-upload">
|
||||
<img src="../images/content/cat.png" alt="Avatar" class="settings-avatar" id="avatarPreview">
|
||||
<img src="../images/content/cat.png" alt="Profile picture preview" class="settings-avatar" id="avatarPreview">
|
||||
<input type="file" id="avatarUpload" accept="image/png, image/jpeg">
|
||||
<button type="button" class="upload-btn">Upload new</button>
|
||||
<button type="button" class="upload-btn" aria-controls="avatarUpload">Upload new</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -68,13 +69,13 @@
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="save-btn">Save Changes</button>
|
||||
<p id="profileSaveStatus"></p></p>
|
||||
<p id="profileSaveStatus" role="status" aria-live="polite"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Security -->
|
||||
<div id="securityTab" class="tab-content">
|
||||
<div id="securityTab" class="tab-content" role="tabpanel" aria-labelledby="securityTabButton" hidden>
|
||||
<form class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="currentPw">Current Password</label>
|
||||
@ -100,7 +101,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Notifications (erweitert) -->
|
||||
<div id="notificationsTab" class="tab-content">
|
||||
<div id="notificationsTab" class="tab-content" role="tabpanel" aria-labelledby="notificationsTabButton" hidden>
|
||||
<form class="settings-form">
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
@ -149,10 +150,19 @@
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tabId = btn.getAttribute('data-tab');
|
||||
tabBtns.forEach(b => b.classList.remove('active'));
|
||||
tabBtns.forEach(b => {
|
||||
b.classList.remove('active');
|
||||
b.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
btn.classList.add('active');
|
||||
tabContents.forEach(content => content.classList.remove('active'));
|
||||
document.getElementById(`${tabId}Tab`).classList.add('active');
|
||||
btn.setAttribute('aria-selected', 'true');
|
||||
tabContents.forEach(content => {
|
||||
content.classList.remove('active');
|
||||
content.hidden = true;
|
||||
});
|
||||
const selectedTab = document.getElementById(`${tabId}Tab`);
|
||||
selectedTab.classList.add('active');
|
||||
selectedTab.hidden = false;
|
||||
});
|
||||
});
|
||||
|
||||
@ -247,9 +257,13 @@
|
||||
document.getElementById('sidebar-container').innerHTML = data;
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
link.removeAttribute('aria-current');
|
||||
});
|
||||
const settingsLink = document.querySelector('#sidebar-container a[href="settings.html"]');
|
||||
if (settingsLink) settingsLink.classList.add('active');
|
||||
if (settingsLink) {
|
||||
settingsLink.classList.add('active');
|
||||
settingsLink.setAttribute('aria-current', 'page');
|
||||
}
|
||||
});
|
||||
fetch('/topbar.html')
|
||||
.then(r => r.text())
|
||||
|
||||
@ -6,66 +6,66 @@
|
||||
|
||||
<div class="sidebar-shell">
|
||||
<!-- Logo -->
|
||||
<div class="sidebar-logo">
|
||||
<img src="../images/logo_full.png" alt="OnlyPrompt Logo" class="sidebar-logo-full">
|
||||
<img src="../images/logo_icon.png" alt="OnlyPrompt Icon" class="sidebar-logo-icon">
|
||||
<div class="sidebar-logo" aria-label="OnlyPrompt">
|
||||
<img src="../images/logo_full.png" alt="" class="sidebar-logo-full">
|
||||
<img src="../images/logo_icon.png" alt="" class="sidebar-logo-icon">
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="sidebar">
|
||||
<nav class="sidebar" id="main-navigation" aria-label="Main navigation" tabindex="-1">
|
||||
<ul>
|
||||
<li class="mobile-nav-item">
|
||||
<a href="dashboard.html" class="active">
|
||||
<i class="bi bi-house icon-blue"></i>
|
||||
<a href="dashboard.html" class="active" aria-current="page">
|
||||
<i class="bi bi-house icon-blue" aria-hidden="true"></i>
|
||||
<span class="nav-text">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mobile-nav-item">
|
||||
<a href="marketplace.html">
|
||||
<i class="bi bi-shop icon-purple"></i>
|
||||
<i class="bi bi-shop icon-purple" aria-hidden="true"></i>
|
||||
<span class="nav-text">Marketplace</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mobile-nav-item">
|
||||
<a href="community.html">
|
||||
<i class="bi bi-people icon-pink"></i>
|
||||
<i class="bi bi-people icon-pink" aria-hidden="true"></i>
|
||||
<span class="nav-text">Community</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mobile-nav-item">
|
||||
<a href="chats.html">
|
||||
<i class="bi bi-chat-dots icon-blue"></i>
|
||||
<i class="bi bi-chat-dots icon-blue" aria-hidden="true"></i>
|
||||
<span class="nav-text">Chats</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mobile-nav-item">
|
||||
<a href="settings.html">
|
||||
<i class="bi bi-gear icon-purple"></i>
|
||||
<i class="bi bi-gear icon-purple" aria-hidden="true"></i>
|
||||
<span class="nav-text">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mobile-nav-item">
|
||||
<a href="profile.html">
|
||||
<i class="bi bi-person icon-pink"></i>
|
||||
<i class="bi bi-person icon-pink" aria-hidden="true"></i>
|
||||
<span class="nav-text">My Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mobile-nav-item">
|
||||
<a href="create.html">
|
||||
<i class="bi bi-plus-circle-fill icon-blue"></i>
|
||||
<i class="bi bi-plus-circle-fill icon-blue" aria-hidden="true"></i>
|
||||
<span class="nav-text">Create New</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mobile-nav-item">
|
||||
<a href="subscription-tiers.html">
|
||||
<i class="bi bi-gem icon-purple"></i>
|
||||
<i class="bi bi-gem icon-purple" aria-hidden="true"></i>
|
||||
<span class="nav-text">Subscriptions</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -75,12 +75,12 @@
|
||||
<!-- Logout bottom -->
|
||||
<div class="sidebar-bottom">
|
||||
<form action="/api/v1/auth/logout" method="post">
|
||||
<button type="submit" class="sidebar-logout">
|
||||
<button type="submit" class="sidebar-logout" aria-label="Logout">
|
||||
<div class="logout-left">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
<i class="bi bi-box-arrow-right" aria-hidden="true"></i>
|
||||
<span class="nav-text">Logout</span>
|
||||
</div>
|
||||
<i class="bi bi-chevron-right logout-arrow"></i>
|
||||
<i class="bi bi-chevron-right logout-arrow" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<!-- For responsive design: adapts width for different devices -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Title shown in browser tab -->
|
||||
<title>OnlyPrompt - Login</title>
|
||||
<title>OnlyPrompt - Sign Up</title>
|
||||
|
||||
<!-- CSS files for variables, base styles, and login page -->
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
@ -20,8 +20,9 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<!-- Main container for the login page (CSS layout) -->
|
||||
<main class="login-page">
|
||||
<main class="login-page" id="main-content" tabindex="-1">
|
||||
<!-- White login card -->
|
||||
<section class="login-card">
|
||||
<!-- Logo container -->
|
||||
@ -37,25 +38,25 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email" placeholder="yourname@email.com" required>
|
||||
<input type="email" id="email" name="email" placeholder="yourname@email.com" autocomplete="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="displayName">Display Name (how it will appear to others)</label>
|
||||
<input type="text" id="displayName" name="displayName" placeholder="Enter your display name" required>
|
||||
<input type="text" id="displayName" name="displayName" placeholder="Enter your display name" autocomplete="name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="userName">Username</label>
|
||||
<input type="text" id="userName" name="userName" placeholder="Choose a username" required>
|
||||
<input type="text" id="userName" name="userName" placeholder="Choose a username" autocomplete="username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<!-- Password field with button to show/hide password -->
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="password" name="password" placeholder="Enter your password" required>
|
||||
<button type="button" id="togglePassword" class="toggle-password">
|
||||
<input type="password" id="password" name="password" placeholder="Enter your password" autocomplete="new-password" required>
|
||||
<button type="button" id="togglePassword" class="toggle-password" aria-controls="password" aria-pressed="false">
|
||||
Show <!-- Click to show/hide password -->
|
||||
</button>
|
||||
</div>
|
||||
@ -73,7 +74,7 @@
|
||||
|
||||
<p class="signup-text">
|
||||
Have an account?
|
||||
<a href="#">Log In</a>
|
||||
<a href="/login">Log In</a>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@ -18,28 +18,29 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div class="layout">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="page-body">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="tiers-main">
|
||||
<main class="tiers-main" id="main-content" tabindex="-1">
|
||||
<header class="tiers-header">
|
||||
<h1>Subscription Tiers</h1>
|
||||
<p>Create monthly access levels for your paid prompts.</p>
|
||||
</header>
|
||||
|
||||
<nav class="tiers-tabs">
|
||||
<button type="button" class="tiers-tab active" data-tab="manage">
|
||||
<nav class="tiers-tabs" role="tablist" aria-label="Subscription tier sections">
|
||||
<button type="button" class="tiers-tab active" data-tab="manage" id="manageTiersTab" role="tab" aria-selected="true" aria-controls="manage-tab-panel">
|
||||
My Tiers
|
||||
</button>
|
||||
<button type="button" class="tiers-tab" data-tab="subscriptions">
|
||||
<button type="button" class="tiers-tab" data-tab="subscriptions" id="subscriptionsTiersTab" role="tab" aria-selected="false" aria-controls="subscriptions-tab-panel">
|
||||
My Subscriptions
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<section class="tiers-layout" id="manage-tab-panel">
|
||||
<section class="tiers-layout" id="manage-tab-panel" role="tabpanel" aria-labelledby="manageTiersTab">
|
||||
<article class="tier-panel">
|
||||
<h2 id="tier-form-title">Create Tier</h2>
|
||||
<form id="tier-form" class="tier-form">
|
||||
@ -86,7 +87,7 @@
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<p id="tier-status"></p>
|
||||
<p id="tier-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
@ -95,18 +96,18 @@
|
||||
<h2>Your Tiers</h2>
|
||||
<p>Higher levels include access to prompts from lower levels.</p>
|
||||
</div>
|
||||
<div class="tiers-grid" id="tiers-grid">
|
||||
<div class="tiers-grid" id="tiers-grid" aria-live="polite">
|
||||
<div class="tiers-empty">Loading tiers...</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="subscriptions-panel" id="subscriptions-tab-panel">
|
||||
<section class="subscriptions-panel" id="subscriptions-tab-panel" role="tabpanel" aria-labelledby="subscriptionsTiersTab" hidden>
|
||||
<div class="tier-list-header">
|
||||
<h2>Your Subscriptions</h2>
|
||||
<p>Creators you follow or support with a monthly tier.</p>
|
||||
</div>
|
||||
<div class="subscriptions-grid" id="subscriptions-grid">
|
||||
<div class="subscriptions-grid" id="subscriptions-grid" aria-live="polite">
|
||||
<div class="tiers-empty">Loading subscriptions...</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -121,11 +122,17 @@
|
||||
document.getElementById("sidebar-container").innerHTML = data;
|
||||
document
|
||||
.querySelectorAll("#sidebar-container .sidebar a")
|
||||
.forEach((link) => link.classList.remove("active"));
|
||||
.forEach((link) => {
|
||||
link.classList.remove("active");
|
||||
link.removeAttribute("aria-current");
|
||||
});
|
||||
const tiersLink = document.querySelector(
|
||||
'#sidebar-container a[href="subscription-tiers.html"]',
|
||||
);
|
||||
if (tiersLink) tiersLink.classList.add("active");
|
||||
if (tiersLink) {
|
||||
tiersLink.classList.add("active");
|
||||
tiersLink.setAttribute("aria-current", "page");
|
||||
}
|
||||
});
|
||||
|
||||
fetch("/topbar.html")
|
||||
@ -156,10 +163,13 @@
|
||||
function setActiveTab(tabName) {
|
||||
document.querySelectorAll(".tiers-tab").forEach((tab) => {
|
||||
tab.classList.toggle("active", tab.dataset.tab === tabName);
|
||||
tab.setAttribute("aria-selected", String(tab.dataset.tab === tabName));
|
||||
});
|
||||
manageTabPanel.style.display = tabName === "manage" ? "grid" : "none";
|
||||
manageTabPanel.hidden = tabName !== "manage";
|
||||
subscriptionsTabPanel.style.display =
|
||||
tabName === "subscriptions" ? "block" : "none";
|
||||
subscriptionsTabPanel.hidden = tabName !== "subscriptions";
|
||||
|
||||
if (tabName === "subscriptions") loadSubscriptions();
|
||||
}
|
||||
@ -283,8 +293,8 @@
|
||||
</div>
|
||||
<p class="tier-desc">${escapeHtml(tier.description || "No description yet.")}</p>
|
||||
<div class="tier-card-actions">
|
||||
<button type="button" data-edit="${tier.id}">Edit</button>
|
||||
<button type="button" data-delete="${tier.id}" class="tier-delete-btn">Delete</button>
|
||||
<button type="button" data-edit="${tier.id}" aria-label="Edit ${escapeHtml(tier.name)} tier">Edit</button>
|
||||
<button type="button" data-delete="${tier.id}" class="tier-delete-btn" aria-label="Delete ${escapeHtml(tier.name)} tier">Delete</button>
|
||||
</div>
|
||||
</article>`,
|
||||
)
|
||||
|
||||
@ -7,23 +7,23 @@
|
||||
|
||||
<header class="topbar-shell">
|
||||
<div class="topbar-search">
|
||||
<i class="bi bi-search"></i>
|
||||
<input id="topbarSearchInput" type="search" placeholder="Search">
|
||||
<i class="bi bi-search" aria-hidden="true"></i>
|
||||
<input id="topbarSearchInput" type="search" placeholder="Search" aria-label="Search prompts and creators">
|
||||
</div>
|
||||
|
||||
<div class="topbar-actions">
|
||||
<form action="/api/v1/auth/logout" method="post" class="topbar-logout-form">
|
||||
<button class="topbar-icon-btn" type="submit" aria-label="Logout" title="Logout">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
<i class="bi bi-box-arrow-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button class="topbar-icon-btn" type="button" aria-label="Messages" title="Messages" onclick="location.href='/chats.html'">
|
||||
<i class="bi bi-chat-dots"></i>
|
||||
<i class="bi bi-chat-dots" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<!-- Profile avatar on the right (must be changed with backend) -->
|
||||
<button class="topbar-avatar-btn" type="button" aria-label="Profile" title="Profile" onclick="location.href='/profile.html'">
|
||||
<img id="topbarAvatar" src="../images/content/cat.png" alt="Profile Picture" class="topbar-avatar">
|
||||
<img id="topbarAvatar" src="../images/content/cat.png" alt="" class="topbar-avatar">
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user