Compare commits

...

2 Commits

Author SHA1 Message Date
Thuvaraka Yogarajah
10592b76c7 Add basic creator chat flow 2026-06-14 12:07:01 +02:00
Thuvaraka Yogarajah
af7271e2f8 Improve frontend accessibility 2026-06-14 11:58:04 +02:00
25 changed files with 878 additions and 308 deletions

View File

@ -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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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>

View File

@ -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);
});
});

View File

@ -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')

View File

@ -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;

View File

@ -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 {

View File

@ -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;
}

View File

@ -49,7 +49,8 @@
flex-direction: column;
gap: 8px;
}
.form-group label {
.form-group label,
.form-label {
font-weight: 600;
font-size: 0.95rem;
}

View File

@ -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;

View File

@ -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;

View File

@ -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 ─────────────────────────────── */

View File

@ -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 {

View File

@ -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;

View File

@ -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");
});
});

View File

@ -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(){

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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();
});

View File

@ -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" : ""}`);
});
}

View File

@ -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 {

View File

@ -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())

View File

@ -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>

View File

@ -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>

View File

@ -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>`,
)

View File

@ -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>