382 lines
15 KiB
HTML
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("&", "&")
|
|
.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>
|