2026-06-14 12:07:01 +02:00

382 lines
15 KiB
HTML

<!-- OnlyPrompt - Chats page:
- Direct messaging interface with conversation list and active chat window -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OnlyPrompt - Chats</title>
<link rel="stylesheet" href="../css/variables.css" />
<link rel="stylesheet" href="../css/base.css" />
<link rel="stylesheet" href="../css/sidebar.css" />
<link rel="stylesheet" href="../css/login.css" />
<link rel="stylesheet" href="../css/topbar.css" />
<link rel="stylesheet" href="../css/chats.css" />
<script src="../js/profile-shared.js"></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
</head>
<body>
<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" 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 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="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 -->
<div class="chat-active">
<div class="chat-header">
<img
src="../images/content/cat.png"
alt=""
class="chat-avatar-large"
id="activeChatAvatar"
/>
<div class="chat-header-info">
<div class="chat-header-name" id="activeChatName">Select a chat</div>
<div class="chat-header-status">
<span class="online-dot" aria-hidden="true"></span>
<span id="activeChatStatus">Ready</span>
</div>
</div>
</div>
<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>
</div>
</div>
<script>
fetch("/sidebar.html")
.then((r) => r.text())
.then((data) => {
document.getElementById("sidebar-container").innerHTML = data;
// Remove 'active' from all sidebar links
document
.querySelectorAll("#sidebar-container .sidebar a")
.forEach((link) => {
link.classList.remove("active");
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");
chatsLink.setAttribute("aria-current", "page");
}
});
fetch("/topbar.html")
.then((r) => r.text())
.then(
(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>