// NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards. const isEncryptedTestMode = true const encryptedCardIdentifierPrefix = "test-MDC" let isExistingEncryptedCard = false let existingDecryptedCardData = {} let existingEncryptedCardIdentifier = {} let cardMinterName = {} let existingCardMinterNames = [] console.log("Attempting to load AdminBoard.js"); const loadAdminBoardPage = async () => { // Clear existing content on the page 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(); } } // Add the "Minter Board" content const mainContent = document.createElement("div"); mainContent.innerHTML = ` <div class="minter-board-main" style="padding: 20px; text-align: center;"> <h1 style="color: lightblue;">AdminBoard</h1> <p style="font-size: 1.25em;"> The Admin Board is an encrypted card publishing board to keep track of minter data for the Minter Admins. Any Admin may publish a card, and related data, make comments on existing cards, and vote on existing card data in support or not of the name on the card. It is essentially a 'project management' tool to assist the Minter Admins in keeping track of the data related to minters they are adding/removing from the minter group. </p> <p> More functionality will be added over time. One of the first features will be the ability to output the existing card data 'decisions', to a json formatted list in order to allow crowetic to run his script easily until the final Mintership proposal changes are completed, and the MINTER group is transferred to 'null'.</p> <button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Encrypted Card</button> <button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button> <div id="encrypted-cards-container" class="cards-container" style="margin-top: 20px;"></div> <div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;"> <form id="publish-card-form"> <h3>Create or Update Your Minter Card</h3> <label for="minter-name-input">Minter Name:</label> <input type="text" id="minter-name-input" maxlength="100" placeholder="Enter Minter's Name" required> <label for="card-header">Header:</label> <input type="text" id="card-header" maxlength="100" placeholder="Explain main point/issue" required> <label for="card-content">Content:</label> <textarea id="card-content" placeholder="Enter any information you like...You may also attach links to more in-depth information, etc." required></textarea> <label for="card-links">Links (qortal://...):</label> <div id="links-container"> <input type="text" class="card-link" placeholder="Enter QDN link"> </div> <button type="button" id="add-link-button">Add Another Link</button> <button type="submit" id="submit-publish-button">Publish Card</button> <button type="button" id="cancel-publish-button">Cancel</button> </form> </div> </div> `; document.body.appendChild(mainContent); const publishCardButton = document.getElementById("publish-card-button") if (publishCardButton) { publishCardButton.addEventListener("click", async () => { const publishCardView = document.getElementById("publish-card-view") publishCardView.style.display = "flex" document.getElementById("encrypted-cards-container").style.display = "none" }) } const refreshCardsButton = document.getElementById("refresh-cards-button") if (refreshCardsButton) { refreshCardsButton.addEventListener("click", async () => { const encryptedCardsContainer = document.getElementById("encrypted-cards-container") encryptedCardsContainer.innerHTML = "<p>Refreshing cards...</p>" await fetchAllEncryptedCards() }) } const cancelPublishButton = document.getElementById("cancel-publish-button") if (cancelPublishButton) { cancelPublishButton.addEventListener("click", async () => { const encryptedCardsContainer = document.getElementById("encrypted-cards-container") encryptedCardsContainer.style.display = "flex"; // Restore visibility const publishCardView = document.getElementById("publish-card-view") publishCardView.style.display = "none"; // Hide the publish form }) } const addLinkButton = document.getElementById("add-link-button") if (addLinkButton) { addLinkButton.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) }) } document.getElementById("publish-card-form").addEventListener("submit", async (event) => { event.preventDefault(); await publishEncryptedCard(); }); // await fetchAndValidateAllAdminCards(); await fetchAllEncryptedCards(); } const extractCardsMinterName = (cardIdentifier) => { // Ensure the identifier starts with the prefix if (!cardIdentifier.startsWith(`${encryptedCardIdentifierPrefix}-`)) { throw new Error('Invalid identifier format or prefix mismatch'); } // Split the identifier into parts const parts = cardIdentifier.split('-'); // Ensure the format has at least 3 parts if (parts.length < 3) { throw new Error('Invalid identifier format'); } // Extract minterName (everything from the second part to the second-to-last part) const minterName = parts.slice(2, -1).join('-'); // Return the extracted minterName return minterName; } const processCards = async (validEncryptedCards) => { const latestCardsMap = new Map() // Step 1: Filter and keep the most recent card per identifier validEncryptedCards.forEach(card => { const timestamp = card.updated || card.created || 0 const existingCard = latestCardsMap.get(card.identifier) if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) { latestCardsMap.set(card.identifier, card) } }) // Step 2: Extract unique cards const uniqueValidCards = Array.from(latestCardsMap.values()) // Step 3: Group by minterName and select the most recent card per minterName const minterNameMap = new Map() for (const card of validEncryptedCards) { const minterName = await extractCardsMinterName(card.identifier) const existingCard = minterNameMap.get(minterName) const cardTimestamp = card.updated || card.created || 0 const existingTimestamp = existingCard?.updated || existingCard?.created || 0 if (!existingCardMinterNames.includes(minterName)) { existingCardMinterNames.push(minterName) console.log(`cardsMinterName: ${minterName} - added to list`) } // Keep only the most recent card for each minterName if (!existingCard || cardTimestamp > existingTimestamp) { minterNameMap.set(minterName, card) } } // Step 4: Filter cards to ensure each minterName is included only once const finalCards = [] const seenMinterNames = new Set() for (const [minterName, card] of minterNameMap.entries()) { if (!seenMinterNames.has(minterName)) { finalCards.push(card) seenMinterNames.add(minterName) // Mark the minterName as seen } } // Step 5: Sort by the most recent timestamp finalCards.sort((a, b) => { const timestampA = a.updated || a.created || 0 const timestampB = b.updated || b.created || 0 return timestampB - timestampA }) return finalCards } //Main function to load the Minter Cards ---------------------------------------- const fetchAllEncryptedCards = async () => { const encryptedCardsContainer = document.getElementById("encrypted-cards-container"); encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>"; try { const response = await qortalRequest({ action: "SEARCH_QDN_RESOURCES", service: "MAIL_PRIVATE", query: encryptedCardIdentifierPrefix, mode: "ALL" }); if (!response || !Array.isArray(response) || response.length === 0) { encryptedCardsContainer.innerHTML = "<p>No cards found.</p>"; return; } // Validate cards and filter const validatedEncryptedCards = await Promise.all( response.map(async card => { const isValid = await validateEncryptedCardIdentifier(card); return isValid ? card : null; }) ); const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null); if (validEncryptedCards.length === 0) { encryptedCardsContainer.innerHTML = "<p>No valid cards found.</p>"; return; } const finalCards = await processCards(validEncryptedCards) // Display skeleton cards immediately encryptedCardsContainer.innerHTML = ""; finalCards.forEach(card => { const skeletonHTML = createSkeletonCardHTML(card.identifier); encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML); }); // Fetch and update each card finalCards.forEach(async card => { try { const cardDataResponse = await qortalRequest({ action: "FETCH_QDN_RESOURCE", name: card.name, service: "MAIL_PRIVATE", identifier: card.identifier, encoding: "base64" }); if (!cardDataResponse) { console.warn(`Skipping invalid card: ${JSON.stringify(card)}`); removeSkeleton(card.identifier); return; } const decryptedCardData = await decryptAndParseObject(cardDataResponse); // Skip cards without polls if (!decryptedCardData.poll) { console.warn(`Skipping card with no poll: ${card.identifier}`); removeSkeleton(card.identifier); return; } // Fetch poll results const pollResults = await fetchPollResults(decryptedCardData.poll); const minterNameFromIdentifier = await extractCardsMinterName(card.identifier); const encryptedCommentCount = await getEncryptedCommentCount(card.identifier); // Generate final card HTML const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount); replaceEncryptedSkeleton(card.identifier, finalCardHTML); } catch (error) { console.error(`Error processing card ${card.identifier}:`, error); removeEncryptedSkeleton(card.identifier); // Silently remove skeleton on error } }); } catch (error) { console.error("Error loading cards:", error); encryptedCardsContainer.innerHTML = "<p>Failed to load cards.</p>"; } }; const removeEncryptedSkeleton = (cardIdentifier) => { const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`); if (encryptedSkeletonCard) { encryptedSkeletonCard.remove(); // Remove the skeleton silently } }; const replaceEncryptedSkeleton = (cardIdentifier, htmlContent) => { const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`); if (encryptedSkeletonCard) { encryptedSkeletonCard.outerHTML = htmlContent; } }; // Function to create a skeleton card const createEncryptedSkeletonCardHTML = (cardIdentifier) => { return ` <div id="skeleton-${cardIdentifier}" class="skeleton-card" style="padding: 10px; border: 1px solid gray; margin: 10px 0;"> <div style="display: flex; align-items: center;"> <div style="width: 50px; height: 50px; background-color: #ccc; border-radius: 50%;"></div> <div style="margin-left: 10px;"> <div style="width: 120px; height: 20px; background-color: #ccc; margin-bottom: 5px;"></div> <div style="width: 80px; height: 15px; background-color: #ddd;"></div> </div> </div> <div style="margin-top: 10px;"> <div style="width: 100%; height: 40px; background-color: #eee;"></div> </div> </div> `; }; // Function to check and fech an existing Minter Card if attempting to publish twice ---------------------------------------- const fetchExistingEncryptedCard = async (minterName) => { try { // Step 1: Perform the search const response = await qortalRequest({ action: "SEARCH_QDN_RESOURCES", service: "MAIL_PRIVATE", identifier: encryptedCardIdentifierPrefix, query: minterName, mode: "ALL", }); console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`); // Step 2: Check if the response is an array and not empty if (!response || !Array.isArray(response) || response.length === 0) { console.log("No cards found for the current user."); return null; } // Step 3: Validate cards asynchronously const validatedCards = await Promise.all( response.map(async card => { const isValid = await validateEncryptedCardIdentifier(card) return isValid ? card : null; }) ); // Step 4: Filter out invalid cards const validCards = validatedCards.filter(card => card !== null); if (validCards.length > 0) { // Step 5: Sort by most recent timestamp const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0]; // Step 6: Fetch full card data const cardDataResponse = await qortalRequest({ action: "FETCH_QDN_RESOURCE", name: mostRecentCard.name, service: mostRecentCard.service, identifier: mostRecentCard.identifier, encoding: "base64" }); existingEncryptedCardIdentifier = mostRecentCard.identifier; existingDecryptedCardData = await decryptAndParseObject(cardDataResponse) console.log("Full card data fetched successfully:", existingDecryptedCardData); return existingDecryptedCardData; } console.log("No valid cards found."); return null; } catch (error) { console.error("Error fetching existing card:", error); return null; } }; // Validate that a card is indeed a card and not a comment. ------------------------------------- const validateEncryptedCardIdentifier = async (card) => { return ( typeof card === "object" && card.name && card.service === "MAIL_PRIVATE" && card.identifier && !card.identifier.includes("comment") && card.created ); } // Load existing card data passed, into the form for editing ------------------------------------- const loadEncryptedCardIntoForm = async () => { if (existingDecryptedCardData) { console.log("Loading existing card data:", existingDecryptedCardData); document.getElementById("minter-name-input").value = existingDecryptedCardData.minterName document.getElementById("card-header").value = existingDecryptedCardData.header document.getElementById("card-content").value = existingDecryptedCardData.content const linksContainer = document.getElementById("links-container"); linksContainer.innerHTML = ""; // Clear previous links existingDecryptedCardData.links.forEach(link => { const linkInput = document.createElement("input"); linkInput.type = "text"; linkInput.className = "card-link"; linkInput.value = link; linksContainer.appendChild(linkInput); }); } } const validateMinterName = async(minterName) => { try { const nameInfo = await getNameInfo(minterName) if (!nameInfo) { return error (`No NameInfo able to be obtained? Did you pass name?`) } const name = nameInfo.name return name } catch (error){ console.error(`extracting name from name info: ${minterName} failed.`, error) } } // Main function to publish a new Minter Card ----------------------------------------------- const publishEncryptedCard = async () => { const minterNameInput = document.getElementById("minter-name-input").value.trim(); const header = document.getElementById("card-header").value.trim(); const content = document.getElementById("card-content").value.trim(); const links = Array.from(document.querySelectorAll(".card-link")) .map(input => input.value.trim()) .filter(link => link.startsWith("qortal://")); const publishedMinterName = await validateMinterName(minterNameInput) if (!header || !content) { alert("Header and Content are required!"); return; } if (!publishedMinterName) { alert(`Minter name invalid! Name input: ${minterNameInput} - please check the name and try again!`) return; } if (!isExistingEncryptedCard) { if (existingCardMinterNames.includes(publishedMinterName)) { const updateCard = confirm(`Minter Name: ${publishedMinterName} - CARD ALREADY EXISTS, you can update it (overwriting existing publish) or cancel... `) if (updateCard) { await fetchExistingEncryptedCard(publishedMinterName) await loadEncryptedCardIntoForm() isExistingEncryptedCard = true return }else { return; } } } const cardIdentifier = isExistingEncryptedCard ? existingEncryptedCardIdentifier : `${encryptedCardIdentifierPrefix}-${publishedMinterName}-${await uid()}`; const pollName = `${cardIdentifier}-poll`; const pollDescription = `Admin Board Poll Published By ${userState.accountName}`; const cardData = { minterName: `${publishedMinterName}`, header, content, links, creator: userState.accountName, timestamp: Date.now(), poll: pollName, }; try { let base64CardData = await objectToBase64(cardData); if (!base64CardData) { console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`); base64CardData = btoa(JSON.stringify(cardData)); } const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys() adminPublicKeys = verifiedAdminPublicKeys await qortalRequest({ action: "PUBLISH_QDN_RESOURCE", name: userState.accountName, service: "MAIL_PRIVATE", identifier: cardIdentifier, data64: base64CardData, encrypt: true, publicKeys: verifiedAdminPublicKeys }); if (!isExistingEncryptedCard){ await qortalRequest({ action: "CREATE_POLL", pollName, pollDescription, pollOptions: ['Yes, No'], pollOwnerAddress: userState.accountAddress, }); alert("Card and poll published successfully!"); existingCardMinterNames.push(`${publishedMinterName}`) } if (isExistingEncryptedCard){ alert("Card Updated Successfully! (No poll updates are possible at this time...)") } document.getElementById("publish-card-form").reset(); document.getElementById("publish-card-view").style.display = "none"; document.getElementById("encrypted-cards-container").style.display = "flex"; } catch (error) { console.error("Error publishing card or poll:", error); alert("Failed to publish card and poll."); } } const getEncryptedCommentCount = async (cardIdentifier) => { try { const response = await qortalRequest({ action: 'SEARCH_QDN_RESOURCES', service: 'MAIL_PRIVATE', query: `comment-${cardIdentifier}`, mode: "ALL" }); // Just return the count; no need to decrypt each comment here return Array.isArray(response) ? response.length : 0; } catch (error) { console.error(`Error fetching comment count for ${cardIdentifier}:`, error); return 0; } }; // Post a comment on a card. --------------------------------- const postEncryptedComment = async (cardIdentifier) => { const commentInput = document.getElementById(`new-comment-${cardIdentifier}`); const commentText = commentInput.value.trim(); if (!commentText) { alert('Comment cannot be empty!'); return; } const postTimestamp = Date.now() console.log(`timestmp to be posted: ${postTimestamp}`) const commentData = { content: commentText, creator: userState.accountName, timestamp: postTimestamp, }; const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`; if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0)) { const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys() adminPublicKeys = verifiedAdminPublicKeys } try { const base64CommentData = await objectToBase64(commentData); if (!base64CommentData) { console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`); base64CommentData = btoa(JSON.stringify(commentData)); } await qortalRequest({ action: "PUBLISH_QDN_RESOURCE", name: userState.accountName, service: "MAIL_PRIVATE", identifier: commentIdentifier, data64: base64CommentData, encrypt: true, publicKeys: adminPublicKeys }); alert('Comment posted successfully!'); commentInput.value = ''; // Clear input } catch (error) { console.error('Error posting comment:', error); alert('Failed to post comment.'); } }; //Fetch the comments for a card with passed card identifier ---------------------------- const fetchEncryptedComments = async (cardIdentifier) => { try { const response = await qortalRequest({ action: 'SEARCH_QDN_RESOURCES', service: 'MAIL_PRIVATE', query: `comment-${cardIdentifier}`, mode: "ALL" }); return response; } catch (error) { console.error(`Error fetching comments for ${cardIdentifier}:`, error); return []; } }; // 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}`); // Fetch and display each comment 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 = ` <div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;"> <p><strong><u>${decryptedCommentData.creator}</strong>:</p></u> <p>${decryptedCommentData.content}</p> <p><i>${timestamp}</p></i> </div> `; commentsContainer.insertAdjacentHTML('beforeend', commentHTML); } } catch (error) { console.error(`Error displaying comments for ${cardIdentifier}:`, error); alert("Failed to load comments. Please try again."); } }; const calculateAdminBoardPollResults = async (pollData, minterGroupMembers, minterAdmins) => { const memberAddresses = minterGroupMembers.map(member => member.member) const minterAdminAddresses = minterAdmins.map(member => member.member) const adminGroupsMembers = await fetchAllAdminGroupsMembers() const groupAdminAddresses = adminGroupsMembers.map(member => member.member) const adminAddresses = []; adminAddresses.push(...minterAdminAddresses,...groupAdminAddresses); let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, yesWeight = 0 , noWeight = 0 pollData.voteWeights.forEach(weightData => { if (weightData.optionName === 'Yes') { yesWeight = weightData.voteWeight } else if (weightData.optionName === 'No') { noWeight = weightData.voteWeight } }) for (const vote of pollData.votes) { const voterAddress = await getAddressFromPublicKey(vote.voterPublicKey) console.log(`voter address: ${voterAddress}`) if (vote.optionIndex === 0) { adminAddresses.includes(voterAddress) ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`) } else if (vote.optionIndex === 1) { adminAddresses.includes(voterAddress) ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`) } } // TODO - create a new function to calculate the weights of each voting MINTER only. // This will give ALL weight whether voter is in minter group or not... // until that is changed on the core we must calculate manually. const totalYesWeight = yesWeight const totalNoWeight = noWeight const totalYes = adminYes + minterYes const totalNo = adminNo + minterNo return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo, totalYesWeight, totalNoWeight } } const toggleEncryptedComments = async (cardIdentifier) => { const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`); if (commentsSection.style.display === 'none' || !commentsSection.style.display) { await displayEncryptedComments(cardIdentifier); commentsSection.style.display = 'block'; } else { commentsSection.style.display = 'none'; } }; const createLinkDisplayModal = async () => { const modalHTML = ` <div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;"> <div style="position: relative; margin: 10% auto; width: 95%; height: 80%; background: white; border-radius: 10px; overflow: hidden;"> <iframe id="modalContent" src="" style="width: 100%; height: 100%; border: none;"></iframe> <button onclick="closeLinkDisplayModal()" style="position: absolute; top: 10px; right: 10px; background: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;">Close</button> </div> </div> `; document.body.insertAdjacentHTML('beforeend', modalHTML); } // 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('modal'); const modalContent = document.getElementById('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('modal'); const modalContent = document.getElementById('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(); // Convert to uppercase // const remainingPath = match[2] || ""; // Rest of the URL // // Perform any asynchronous operation if necessary // await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation // return `/render/${firstParam}${remainingPath}`; // } // } // return link; // Return unchanged if not a Qortal link // } const processQortalLinkForRendering = async (link) => { if (link.startsWith('qortal://')) { const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/); if (match) { const firstParam = match[1].toUpperCase(); const remainingPath = match[2] || ""; const themeColor = window._qdnTheme || 'default'; // Fallback to 'default' if undefined // Simulating async operation if needed await new Promise(resolve => setTimeout(resolve, 10)); // Append theme as a query parameter return `/render/${firstParam}${remainingPath}?theme=${themeColor}`; } } return link; }; async function getMinterAvatar(minterName) { const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`; try { const response = await fetch(avatarUrl, { method: 'HEAD' }); if (response.ok) { // Avatar exists, return the image HTML return `<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">`; } else { // Avatar not found or no permission return ''; } } catch (error) { console.error('Error checking avatar availability:', error); return ''; } } // Create the overall Minter Card HTML ----------------------------------------------- const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => { const { minterName, header, content, links, creator, timestamp, poll } = cardData; const formattedDate = new Date(timestamp).toLocaleString(); const minterAvatar = await getMinterAvatar(minterName) // const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`; const creatorAvatar = await getMinterAvatar(creator) const linksHTML = links.map((link, index) => ` <button onclick="openLinkDisplayModal('${link}')"> ${`Link ${index + 1} - ${link}`} </button> `).join(""); 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 } = await calculateAdminBoardPollResults(pollResults, minterGroupMembers, minterAdmins) await createModal() return ` <div class="admin-card"> <div class="minter-card-header"> <h2 class="support-header"> Created By: </h2> ${creatorAvatar} <h2>${creator}</h2> <div class="support-header"><h5> REGARDING: </h5></div> ${minterAvatar} <h3>${minterName}</h3> <p>${header}</p> </div> <div class="info"> ${content} </div> <div class="support-header"><h5>LINKS</h5></div> <div class="info-links"> ${linksHTML} </div> <div class="results-header support-header"><h5>CURRENT RESULTS</h5></div> <div class="minter-card-results"> <div class="admin-results"> <span class="admin-yes">Admin Support: ${adminYes}</span> <span class="admin-no">Admin Against: ${adminNo}</span> </div> <div class="minter-results"> <span class="minter-yes">Supporting Weight ${totalYesWeight}</span> <span class="minter-no">Denial Weight ${totalNoWeight}</span> </div> </div> <div class="support-header"><h5>SUPPORT or DENY</h5><h5 style="color: #ffae42;">${minterName}</h5> <p style="color: #c7c7c7; font-size: .65rem; margin-top: 1vh">(click COMMENTS button to open/close card comments)</p> </div> <div class="actions"> <div class="actions-buttons"> <button class="yes" onclick="voteYesOnPoll('${poll}')">SUPPORT</button> <button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS (${commentCount})</button> <button class="no" onclick="voteNoOnPoll('${poll}')">OPPOSE</button> </div> </div> <div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;"> <div id="comments-container-${cardIdentifier}" class="comments-container"></div> <textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea> <button onclick="postEncryptedComment('${cardIdentifier}')">Post Comment</button> </div> <p style="font-size: 0.75rem; margin-top: 1vh; color: #4496a1">By: ${creator} - ${formattedDate}</p> </div> `; }