// // 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 loadMinterBoardPage = 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"); const publishButtonColor = '#527c9d' const minterBoardNameColor = '#527c9d' mainContent.innerHTML = `
Publish a Minter Card with Information, and obtain and view the support of the community. Welcome to the Minter Board!
Refreshing cards...
"; await loadCards(); }); document.getElementById("cancel-publish-button").addEventListener("click", async () => { const cardsContainer = document.getElementById("cards-container"); cardsContainer.style.display = "flex"; // Restore visibility 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); }); document.getElementById("publish-card-form").addEventListener("submit", async (event) => { event.preventDefault(); await publishCard(); }); await loadCards(); } const extractMinterCardsMinterName = async (cardIdentifier) => { // Ensure the identifier starts with the prefix if (!cardIdentifier.startsWith(`${cardIdentifierPrefix}-`)) { 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') } try { const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1) const minterName = await searchSimpleResults.name return minterName } catch (error) { throw error } } const processMinterCards = async (validMinterCards) => { const latestCardsMap = new Map() // Step 1: Filter and keep the most recent card per identifier validMinterCards.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 validMinterCards) { const minterName = await extractMinterCardsMinterName(card.identifier) const existingCard = minterNameMap.get(minterName) const cardTimestamp = card.updated || card.created || 0 const existingTimestamp = existingCard?.updated || existingCard?.created || 0 // 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 loadCards = async () => { const cardsContainer = document.getElementById("cards-container"); cardsContainer.innerHTML = "Loading cards...
"; try { // const response = await qortalRequest({ // action: "SEARCH_QDN_RESOURCES", // service: "BLOG_POST", // query: cardIdentifierPrefix, // mode: "ALL" // }) const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, '' , 0) if (!response || !Array.isArray(response) || response.length === 0) { cardsContainer.innerHTML = "No cards found.
"; return; } // Validate cards and filter const validatedCards = await Promise.all( response.map(async card => { const isValid = await validateCardStructure(card); return isValid ? card : null; }) ); const validCards = validatedCards.filter(card => card !== null); if (validCards.length === 0) { cardsContainer.innerHTML = "No valid cards found.
"; return; } const finalCards = await processMinterCards(validCards) // Sort cards by timestamp descending (newest first) // validCards.sort((a, b) => { // const timestampA = a.updated || a.created || 0; // const timestampB = b.updated || b.created || 0; // return timestampB - timestampA; // }); // Display skeleton cards immediately cardsContainer.innerHTML = ""; finalCards.forEach(card => { const skeletonHTML = createSkeletonCardHTML(card.identifier); cardsContainer.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: "BLOG_POST", identifier: card.identifier, }); if (!cardDataResponse) { console.warn(`Skipping invalid card: ${JSON.stringify(card)}`); removeSkeleton(card.identifier); return; } // Skip cards without polls if (!cardDataResponse.poll) { console.warn(`Skipping card with no poll: ${card.identifier}`); removeSkeleton(card.identifier); return; } // Fetch poll results const pollResults = await fetchPollResults(cardDataResponse.poll); const BgColor = generateDarkPastelBackgroundBy(card.name) // Generate final card HTML const commentCount = await countComments(card.identifier) const cardUpdatedTime = card.updated || null const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, BgColor); replaceSkeleton(card.identifier, finalCardHTML); } catch (error) { console.error(`Error processing card ${card.identifier}:`, error); removeSkeleton(card.identifier); // Silently remove skeleton on error } }); } catch (error) { console.error("Error loading cards:", error); cardsContainer.innerHTML = "Failed to load cards.
"; } }; const removeSkeleton = (cardIdentifier) => { const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`); if (skeletonCard) { skeletonCard.remove(); // Remove the skeleton silently } }; const replaceSkeleton = (cardIdentifier, htmlContent) => { const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`); if (skeletonCard) { skeletonCard.outerHTML = htmlContent; } }; // Function to create a skeleton card const createSkeletonCardHTML = (cardIdentifier) => { return `LOADING CARD...
PLEASE BE PATIENT
While data loads from QDN...
Poll data is invalid or missing.
` } } const memberAddresses = minterGroupMembers.map(m => m.member) const minterAdminAddresses = minterAdmins.map(m => m.member) const adminGroupsMembers = await fetchAllAdminGroupsMembers() const groupAdminAddresses = adminGroupsMembers.map(m => m.member) const adminAddresses = [...minterAdminAddresses, ...groupAdminAddresses] let adminYes = 0, adminNo = 0 let minterYes = 0, minterNo = 0 let yesWeight = 0, noWeight = 0 for (const w of pollData.voteWeights) { if (w.optionName.toLowerCase() === 'yes') { yesWeight = w.voteWeight } else if (w.optionName.toLowerCase() === 'no') { noWeight = w.voteWeight } } const voterPromises = pollData.votes.map(async (vote) => { const optionIndex = vote.optionIndex; // 0 => yes, 1 => no const voterPublicKey = vote.voterPublicKey const voterAddress = await getAddressFromPublicKey(voterPublicKey) if (optionIndex === 0) { if (adminAddresses.includes(voterAddress)) { adminYes++ } else if (memberAddresses.includes(voterAddress)) { minterYes++ } else { console.log(`voter ${voterAddress} is not a minter nor an admin... Not included in aggregates.`) } } else if (optionIndex === 1) { if (adminAddresses.includes(voterAddress)) { adminNo++ } else if (memberAddresses.includes(voterAddress)) { minterNo++ } else { console.log(`voter ${voterAddress} is not a minter nor an admin... Not included in aggregates.`) } } let voterName = '' try { const nameInfo = await getNameFromAddress(voterAddress) if (nameInfo) { voterName = nameInfo if (nameInfo === voterAddress) voterName = '' } } catch (err) { console.warn(`No name for address ${voterAddress}`, err) } let blocksMinted = 0 try { const addressInfo = await getAddressInfo(voterAddress) blocksMinted = addressInfo?.blocksMinted || 0 } catch (e) { console.warn(`Failed to get addressInfo for ${voterAddress}`, e) } const isAdmin = adminAddresses.includes(voterAddress) const isMinter = memberAddresses.includes(voterAddress) return { optionIndex, voterPublicKey, voterAddress, voterName, isAdmin, isMinter, blocksMinted } }) const allVoters = await Promise.all(voterPromises) const yesVoters = [] const noVoters = [] let totalMinterAndAdminYesWeight = 0 let totalMinterAndAdminNoWeight = 0 for (const v of allVoters) { if (v.optionIndex === 0) { yesVoters.push(v) totalMinterAndAdminYesWeight+=v.blocksMinted } else if (v.optionIndex === 1) { noVoters.push(v) totalMinterAndAdminNoWeight+=v.blocksMinted } } yesVoters.sort((a,b) => b.blocksMinted - a.blocksMinted); noVoters.sort((a,b) => b.blocksMinted - a.blocksMinted); const yesTableHtml = buildVotersTableHtml(yesVoters, /* tableColor= */ "green") const noTableHtml = buildVotersTableHtml(noVoters, /* tableColor= */ "red") const detailsHtml = `No voters here.
`; } // Decide extremely dark background for the let bodyBackground; if (tableColor === "green") { bodyBackground = "rgba(0, 18, 0, 0.8)" // near-black green } else if (tableColor === "red") { bodyBackground = "rgba(30, 0, 0, 0.8)" // near-black red } else { // fallback color if needed bodyBackground = "rgba(40, 20, 10, 0.8)" } // tableColor is used for the , bodyBackground for the const minterColor = 'rgb(98, 122, 167)' const adminColor = 'rgb(44, 209, 151)' const userColor = 'rgb(102, 102, 102)' return `Voter Name/Address | Voter Type | Voter Weight(=BlocksMinted) |
---|---|---|
${displayName} | ${userType} | ${v.blocksMinted} |
${header}
(click COMMENTS button to open/close card comments)
By: ${creator} - ${formattedDate}
${commentDataResponse.creator}:
${commentDataResponse.content}
${timestamp}