From 999b94f5e5c81f0b6b8721ab6070a656b8504f0b Mon Sep 17 00:00:00 2001 From: crowetic Date: Wed, 1 Jan 2025 13:47:45 -0800 Subject: [PATCH] added colorized comments based on poll results, and resolved image display issues for large images on forum posts. --- assets/css/forum-styles.css | 45 ++++--- assets/js/AdminBoard.js | 141 +++++++++++++++----- assets/js/MinterBoard.js | 256 +++++++++++++++++++++++++----------- 3 files changed, 312 insertions(+), 130 deletions(-) diff --git a/assets/css/forum-styles.css b/assets/css/forum-styles.css index e90ff43..9c25c15 100644 --- a/assets/css/forum-styles.css +++ b/assets/css/forum-styles.css @@ -369,36 +369,43 @@ } .attachment button { - align-self: flex-end; - margin-top: 0.8vh; - background-color: #0f364c; - color: #ffffff; - border-style: ridge; - border-radius: 1vh; - padding: 0.15vh 0.3vh; - cursor: pointer; + align-self: flex-end; + margin-top: 0.8vh; + background-color: #0f364c; + color: #ffffff; + border-style: ridge; + border-radius: 1vh; + padding: 0.15vh 0.3vh; + cursor: pointer; } .image-modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 1000; /* Sit on top */ - left: 10%; - top: 15%; - width: 80%; - height: 70%; + /* left: 15%; + top: 15%; */ + width: 85%; + height: 75%; overflow: none; /* Enable scroll if needed */ background-color: rgba(0, 0, 0, 0.8); /* Black w/ opacity */ } + +.image-modal img { + max-width: 95%; + max-height: 95%; + object-fit: contain; /* Ensures the image maintains its aspect ratio */ +} + .remove-image-button { - background-color: #0f4c41; - color: #ffffff; - border: dotted; - border-radius: 1vh; - padding: 0.3vh 0.6vh; - margin-top: 1px; - cursor: pointer; + background-color: #0f4c41; + color: #ffffff; + border: dotted; + border-radius: 1vh; + padding: 0.3vh 0.6vh; + margin-top: 1px; + cursor: pointer; } .modal-content { diff --git a/assets/js/AdminBoard.js b/assets/js/AdminBoard.js index 149b63f..940c243 100644 --- a/assets/js/AdminBoard.js +++ b/assets/js/AdminBoard.js @@ -182,7 +182,6 @@ const extractEncryptedCardsMinterName = (cardIdentifier) => { const processCards = async (validEncryptedCards) => { const latestCardsMap = new Map() - // Step 1: Process all cards in parallel await Promise.all(validEncryptedCards.map(async card => { const timestamp = card.updated || card.created || 0 const existingCard = latestCardsMap.get(card.identifier) @@ -194,7 +193,6 @@ const processCards = async (validEncryptedCards) => { console.log(`latestCardsMap, by timestamp`, latestCardsMap) - // Step 2: Extract unique cards const uniqueValidCards = Array.from(latestCardsMap.values()) return uniqueValidCards @@ -380,7 +378,7 @@ const fetchExistingEncryptedCard = async (minterName, existingIdentifier) => { return decryptedCardData } catch (error) { - console.error("Error fetching existing card:", error); + console.error("Error fetching existing card:", error) return null } } @@ -610,7 +608,7 @@ const fetchEncryptedComments = async (cardIdentifier) => { try { const response = await searchSimple('MAIL_PRIVATE', `comment-${cardIdentifier}`, '', 0, 0, '', false) if (response) { - return response; + return response } } catch (error) { console.error(`Error fetching comments for ${cardIdentifier}:`, error) @@ -619,38 +617,109 @@ const fetchEncryptedComments = async (cardIdentifier) => { } // display the comments on the card, with passed cardIdentifier to identify the card -------------- +// const displayEncryptedComments = async (cardIdentifier) => { +// try { +// const comments = await fetchEncryptedComments(cardIdentifier) +// const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`) + +// for (const comment of comments) { +// const commentDataResponse = await qortalRequest({ +// action: "FETCH_QDN_RESOURCE", +// name: comment.name, +// service: "MAIL_PRIVATE", +// identifier: comment.identifier, +// encoding: "base64" +// }) + +// const decryptedCommentData = await decryptAndParseObject(commentDataResponse) +// const timestampCheck = comment.updated || comment.created || 0 +// const timestamp = await timestampToHumanReadableDate(timestampCheck) +// //TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section. +// const commentHTML = ` +//
+//

${decryptedCommentData.creator}:

+//

${decryptedCommentData.content}

+//

${timestamp}

+//
+// ` +// commentsContainer.insertAdjacentHTML('beforeend', commentHTML) +// } +// } catch (error) { +// console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error) +// } +// } +//TODO testing this update to the comments fetching to improve performance by leveraging promise.all const displayEncryptedComments = async (cardIdentifier) => { try { const comments = await fetchEncryptedComments(cardIdentifier) const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`) - - for (const comment of comments) { - const commentDataResponse = await qortalRequest({ - action: "FETCH_QDN_RESOURCE", - name: comment.name, - service: "MAIL_PRIVATE", - identifier: comment.identifier, - encoding: "base64" - }) - const decryptedCommentData = await decryptAndParseObject(commentDataResponse) - const timestampCheck = comment.updated || comment.created || 0 - const timestamp = await timestampToHumanReadableDate(timestampCheck) - //TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section. - const commentHTML = ` -
-

${decryptedCommentData.creator}:

-

${decryptedCommentData.content}

-

${timestamp}

-
- ` - commentsContainer.insertAdjacentHTML('beforeend', commentHTML) - } + commentsContainer.innerHTML = '' + + const voterMap = globalVoterMap.get(cardIdentifier) || new Map() + + const commentHTMLArray = await Promise.all( + comments.map(async (comment) => { + try { + const commentDataResponse = await qortalRequest({ + action: "FETCH_QDN_RESOURCE", + name: comment.name, + service: "MAIL_PRIVATE", + identifier: comment.identifier, + encoding: "base64", + }) + + const decryptedCommentData = await decryptAndParseObject(commentDataResponse) + const timestampCheck = comment.updated || comment.created || 0 + const timestamp = await timestampToHumanReadableDate(timestampCheck) + + const commenter = decryptedCommentData.creator + const voterInfo = voterMap.get(commenter) + + let commentColor = "transparent" + let adminBadge = "" + + if (voterInfo) { + if (voterInfo.voterType === "Admin") { + // Admin-specific colors + commentColor = voterInfo.vote === "yes" ? "rgba(25, 175, 25, 0.6)" : "rgba(194, 39, 62, 0.6)" // Light green for yes, light red for no + const badgeColor = voterInfo.vote === "yes" ? "green" : "red" + adminBadge = `(Admin)` + } else { + // Non-admin colors + commentColor = voterInfo.vote === "yes" ? "rgba(0, 100, 0, 0.3)" : "rgba(100, 0, 0, 0.3)" // Darker green for yes, darker red for no + } + } + + return ` +
+

+ ${decryptedCommentData.creator} + ${adminBadge} +

+

${decryptedCommentData.content}

+

${timestamp}

+
+ ` + } catch (err) { + console.error(`Error processing comment ${comment.identifier}:`, err) + return null // Skip this comment if it fails + } + }) + ) + + // Add all comments to the container + commentHTMLArray + .filter((html) => html !== null) // Filter out failed comments + .forEach((commentHTML) => { + commentsContainer.insertAdjacentHTML('beforeend', commentHTML) + }) } catch (error) { console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error) } } + const toggleEncryptedComments = async (cardIdentifier) => { const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`) const commentButton = document.getElementById(`comment-button-${cardIdentifier}`) @@ -689,25 +758,25 @@ const createLinkDisplayModal = async () => { // Function to open the modal const openLinkDisplayModal = async (link) => { const processedLink = await processQortalLinkForRendering(link) // Process the link to replace `qortal://` for rendering in modal - const modal = document.getElementById('links-modal'); - const modalContent = document.getElementById('links-modalContent'); - modalContent.src = processedLink; // Set the iframe source to the link - modal.style.display = 'block'; // Show the modal + const modal = document.getElementById('links-modal') + const modalContent = document.getElementById('links-modalContent') + modalContent.src = processedLink // Set the iframe source to the link + modal.style.display = 'block' // Show the modal } // Function to close the modal const closeLinkDisplayModal = async () => { - const modal = document.getElementById('links-modal'); - const modalContent = document.getElementById('links-modalContent'); - modal.style.display = 'none'; // Hide the modal - modalContent.src = ''; // Clear the iframe source + const modal = document.getElementById('links-modal') + const modalContent = document.getElementById('links-modalContent') + modal.style.display = 'none' // Hide the modal + modalContent.src = '' // Clear the iframe source } const processQortalLinkForRendering = async (link) => { if (link.startsWith('qortal://')) { const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/) if (match) { - const firstParam = match[1].toUpperCase(); + const firstParam = match[1].toUpperCase() const remainingPath = match[2] || "" const themeColor = window._qdnTheme || 'default' // Fallback to 'default' if undefined // Simulating async operation if needed @@ -776,7 +845,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co const minterGroupMembers = await fetchMinterGroupMembers() const minterAdmins = await fetchMinterGroupAdmins() - const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator) + const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier) createModal('links') createModal('poll-details') return ` diff --git a/assets/js/MinterBoard.js b/assets/js/MinterBoard.js index 5e34cc0..91224c7 100644 --- a/assets/js/MinterBoard.js +++ b/assets/js/MinterBoard.js @@ -1,22 +1,22 @@ // // NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards. -const testMode = false; -const cardIdentifierPrefix = "Minter-board-card"; -let isExistingCard = false; -let existingCardData = {}; -let existingCardIdentifier = {}; +const testMode = false +const cardIdentifierPrefix = "Minter-board-card" +let isExistingCard = false +let existingCardData = {} +let existingCardIdentifier = {} const loadMinterBoardPage = async () => { // Clear existing content on the page - const bodyChildren = document.body.children; + const bodyChildren = document.body.children for (let i = bodyChildren.length - 1; i >= 0; i--) { const child = bodyChildren[i]; if (!child.classList.contains("menu")) { - child.remove(); + child.remove() } } // Add the "Minter Board" content - const mainContent = document.createElement("div"); + const mainContent = document.createElement("div") const publishButtonColor = '#527c9d' const minterBoardNameColor = '#527c9d' mainContent.innerHTML = ` @@ -43,78 +43,78 @@ const loadMinterBoardPage = async () => { - `; - document.body.appendChild(mainContent); + ` + document.body.appendChild(mainContent) document.getElementById("publish-card-button").addEventListener("click", async () => { try { - const fetchedCard = await fetchExistingCard(); + const fetchedCard = await fetchExistingCard() if (fetchedCard) { // An existing card is found if (testMode) { // In test mode, ask user what to do - const updateCard = confirm("A card already exists. Do you want to update it?"); + const updateCard = confirm("A card already exists. Do you want to update it?") if (updateCard) { - isExistingCard = true; - await loadCardIntoForm(existingCardData); - alert("Edit your existing card and publish."); + isExistingCard = true + await loadCardIntoForm(existingCardData) + alert("Edit your existing card and publish.") } else { - alert("Test mode: You can now create a new card."); - isExistingCard = false; - existingCardData = {}; // Reset - document.getElementById("publish-card-form").reset(); + alert("Test mode: You can now create a new card.") + isExistingCard = false + existingCardData = {} + document.getElementById("publish-card-form").reset() } } else { // Not in test mode, force editing alert("A card already exists. Publishing of multiple cards is not allowed. Please update your card."); isExistingCard = true; - await loadCardIntoForm(existingCardData); + await loadCardIntoForm(existingCardData) } } else { // No existing card found - console.log("No existing card found. Creating a new card."); - isExistingCard = false; + console.log("No existing card found. Creating a new card.") + isExistingCard = false } // Show the form - const publishCardView = document.getElementById("publish-card-view"); + const publishCardView = document.getElementById("publish-card-view") publishCardView.style.display = "flex"; - document.getElementById("cards-container").style.display = "none"; + document.getElementById("cards-container").style.display = "none" } catch (error) { - console.error("Error checking for existing card:", error); - alert("Failed to check for existing card. Please try again."); + console.error("Error checking for existing card:", error) + alert("Failed to check for existing card. Please try again.") } - }); + }) document.getElementById("refresh-cards-button").addEventListener("click", async () => { - const cardsContainer = document.getElementById("cards-container"); - cardsContainer.innerHTML = "

Refreshing cards...

"; + const cardsContainer = document.getElementById("cards-container") + cardsContainer.innerHTML = "

Refreshing cards...

" await loadCards(); - }); + }) document.getElementById("cancel-publish-button").addEventListener("click", async () => { - const cardsContainer = document.getElementById("cards-container"); + const cardsContainer = document.getElementById("cards-container") cardsContainer.style.display = "flex"; // Restore visibility - const publishCardView = document.getElementById("publish-card-view"); + const publishCardView = document.getElementById("publish-card-view") publishCardView.style.display = "none"; // Hide the publish form - }); + }) document.getElementById("add-link-button").addEventListener("click", async () => { - const linksContainer = document.getElementById("links-container"); - const newLinkInput = document.createElement("input"); - newLinkInput.type = "text"; - newLinkInput.className = "card-link"; - newLinkInput.placeholder = "Enter QDN link"; - linksContainer.appendChild(newLinkInput); - }); + const linksContainer = document.getElementById("links-container") + const newLinkInput = document.createElement("input") + newLinkInput.type = "text" + newLinkInput.className = "card-link" + newLinkInput.placeholder = "Enter QDN link" + linksContainer.appendChild(newLinkInput) + }) document.getElementById("publish-card-form").addEventListener("submit", async (event) => { - event.preventDefault(); - await publishCard(); - }); + event.preventDefault() + await publishCard() + }) - await loadCards(); + await loadCards() } const extractMinterCardsMinterName = async (cardIdentifier) => { @@ -343,7 +343,6 @@ const fetchExistingCard = async () => { existingCardData = cardDataResponse return cardDataResponse - } const validatedCards = await Promise.all( @@ -406,7 +405,7 @@ const loadCardIntoForm = async (cardData) => { linkInput.type = "text" linkInput.className = "card-link" linkInput.value = link; - linksContainer.appendChild(linkInput); + linksContainer.appendChild(linkInput) }) } @@ -419,7 +418,7 @@ const publishCard = async () => { const userAddress = userState.accountAddress; if (minterGroupAddresses.includes(userAddress)) { alert("You are already a Minter and cannot publish a new card!") - return; + return } const header = document.getElementById("card-header").value.trim() const content = document.getElementById("card-content").value.trim() @@ -487,7 +486,9 @@ const publishCard = async () => { } } -const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator) => { +let globalVoterMap = new Map() + +const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator, cardIdentifier) => { if (!pollData || !Array.isArray(pollData.voteWeights) || !Array.isArray(pollData.votes)) { console.warn("Poll data is missing or invalid. pollData:", pollData) return { @@ -574,6 +575,29 @@ const processPollData= async (pollData, minterGroupMembers, minterAdmins, creato blocksMinted } }) + //TODO verify this new voterPromises async function works better. + // const voterPromises = pollData.votes.map(async (vote) => { + // const voterPublicKey = vote.voterPublicKey; + // const voterAddress = await getAddressFromPublicKey(voterPublicKey); + + // const [nameInfo, addressInfo] = await Promise.all([ + // getNameFromAddress(voterAddress).catch(() => ""), + // getAddressInfo(voterAddress).catch(() => ({})), + // ]); + + // const voterName = nameInfo || (nameInfo === voterAddress ? "" : voterAddress); + // const blocksMinted = addressInfo?.blocksMinted || 0; + + // return { + // optionIndex: vote.optionIndex, + // voterPublicKey, + // voterAddress, + // voterName, + // isAdmin: adminAddresses.includes(voterAddress), + // isMinter: memberAddresses.includes(voterAddress), + // blocksMinted, + // }; + // }); const allVoters = await Promise.all(voterPromises) const yesVoters = [] @@ -591,8 +615,11 @@ const processPollData= async (pollData, minterGroupMembers, minterAdmins, creato } } - yesVoters.sort((a,b) => b.blocksMinted - a.blocksMinted); - noVoters.sort((a,b) => b.blocksMinted - a.blocksMinted); + yesVoters.sort((a,b) => b.blocksMinted - a.blocksMinted) + noVoters.sort((a,b) => b.blocksMinted - a.blocksMinted) + const sortedAllVoters = allVoters.sort((a,b) => b.blocksMinted - a.blocksMinted) + await createVoterMap(sortedAllVoters, cardIdentifier) + const yesTableHtml = buildVotersTableHtml(yesVoters, /* tableColor= */ "green") const noTableHtml = buildVotersTableHtml(noVoters, /* tableColor= */ "red") const detailsHtml = ` @@ -620,13 +647,26 @@ const processPollData= async (pollData, minterGroupMembers, minterAdmins, creato } } +const createVoterMap = async (voters, cardIdentifier) => { + const voterMap = new Map() + voters.forEach((voter) => { + const voterNameOrAddress = voter.voterName || voter.voterAddress + voterMap.set(voterNameOrAddress, { + vote: voter.optionIndex === 0 ? "yes" : "no", // Use optionIndex directly + voterType: voter.isAdmin ? "Admin" : voter.isMinter ? "Minter" : "User", + blocksMinted: voter.blocksMinted, + }) + }) + globalVoterMap.set(cardIdentifier, voterMap) +} + const buildVotersTableHtml = (voters, tableColor) => { if (!voters.length) { - return `

No voters here.

`; + return `

No voters here.

` } // Decide extremely dark background for the - let bodyBackground; + let bodyBackground if (tableColor === "green") { bodyBackground = "rgba(0, 18, 0, 0.8)" // near-black green } else if (tableColor === "red") { @@ -661,7 +701,7 @@ const buildVotersTableHtml = (voters, tableColor) => { ${voters .map(v => { - const userType = v.isAdmin ? "Admin" : v.isMinter ? "Minter" : "User"; + const userType = v.isAdmin ? "Admin" : v.isMinter ? "Minter" : "User" const pollName = v.pollName const displayName = v.voterName @@ -735,29 +775,95 @@ const fetchCommentsForCard = async (cardIdentifier) => { } // display the comments on the card, with passed cardIdentifier to identify the card -------------- +// const displayComments = async (cardIdentifier) => { +// try { +// const comments = await fetchCommentsForCard(cardIdentifier); +// const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`) + +// for (const comment of comments) { +// const commentDataResponse = await qortalRequest({ +// action: "FETCH_QDN_RESOURCE", +// name: comment.name, +// service: "BLOG_POST", +// identifier: comment.identifier, +// }) +// const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp) +// const commentHTML = ` +//
+//

${commentDataResponse.creator}:

+//

${commentDataResponse.content}

+//

${timestamp}

+//
+// ` +// commentsContainer.insertAdjacentHTML('beforeend', commentHTML) +// } + +// } catch (error) { +// console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error) +// } +// } + const displayComments = async (cardIdentifier) => { try { - const comments = await fetchCommentsForCard(cardIdentifier); + const comments = await fetchCommentsForCard(cardIdentifier) const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`) - - for (const comment of comments) { - const commentDataResponse = await qortalRequest({ - action: "FETCH_QDN_RESOURCE", - name: comment.name, - service: "BLOG_POST", - identifier: comment.identifier, - }) - const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp) - const commentHTML = ` -
-

${commentDataResponse.creator}:

-

${commentDataResponse.content}

-

${timestamp}

-
- ` - commentsContainer.insertAdjacentHTML('beforeend', commentHTML) - } + commentsContainer.innerHTML = '' + + const voterMap = globalVoterMap.get(cardIdentifier) || new Map() + + const commentHTMLArray = await Promise.all( + comments.map(async (comment) => { + try { + const commentDataResponse = await qortalRequest({ + action: "FETCH_QDN_RESOURCE", + name: comment.name, + service: "BLOG_POST", + identifier: comment.identifier, + }) + + const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp); + + const commenter = commentDataResponse.creator + const voterInfo = voterMap.get(commenter) + + let commentColor = "transparent" + let adminBadge = "" + + if (voterInfo) { + if (voterInfo.voterType === "Admin") { + + commentColor = voterInfo.vote === "yes" ? "rgba(21, 150, 21, 0.6)" : "rgba(212, 37, 64, 0.6)" // Light green for yes, light red for no + const badgeColor = voterInfo.vote === "yes" ? "green" : "red" + adminBadge = `(Admin)` + } else { + + commentColor = voterInfo.vote === "yes" ? "rgba(0, 100, 0, 0.3)" : "rgba(100, 0, 0, 0.3)" // Darker green for yes, darker red for no + } + } + + return ` +
+

+ ${commentDataResponse.creator} + ${adminBadge} +

+

${commentDataResponse.content}

+

${timestamp}

+
+ ` + } catch (err) { + console.error(`Error processing comment ${comment.identifier}:`, err) + return null + } + }) + ) + + commentHTMLArray + .filter((html) => html !== null) // Filter out failed comments + .forEach((commentHTML) => { + commentsContainer.insertAdjacentHTML('beforeend', commentHTML) + }) } catch (error) { console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error) } @@ -935,7 +1041,7 @@ const generateDarkPastelBackgroundBy = (name) => { // Create the overall Minter Card HTML ----------------------------------------------- const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, BgColor) => { - const { header, content, links, creator, timestamp, poll } = cardData; + const { header, content, links, creator, timestamp, poll } = cardData const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString() const avatarHtml = await getMinterAvatar(creator) const linksHTML = links.map((link, index) => ` @@ -946,7 +1052,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun const minterGroupMembers = await fetchMinterGroupMembers() const minterAdmins = await fetchMinterGroupAdmins() - const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator) + const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier) createModal('links') createModal('poll-details') @@ -1003,6 +1109,6 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun

By: ${creator} - ${formattedDate}

- `; + ` }