// // 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 MIN_ADMIN_YES_VOTES = 9; const GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT = 9999950 //TODO update this to correct featureTrigger height when known, either that, or pull from core. let featureTriggerPassed = false let isApproved = false 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 featureTriggerCheck() 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() 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) } }) const minterGroupMembers = await fetchMinterGroupMembers() const minterGroupAddresses = minterGroupMembers.map(m => m.member) const minterNameMap = new Map() for (const card of validMinterCards) { const minterName = await extractMinterCardsMinterName(card.identifier) console.log(`minterName`, minterName) const minterNameInfo = await getNameInfo(minterName) if (!minterNameInfo) { console.warn(`minterNameInfo is null for minter: ${minterName}`) continue } const minterAddress = await minterNameInfo.owner if (!minterAddress) { console.warn(`minterAddress is FAKE or INVALID in some way! minter: ${minterName}`) continue } else if (minterGroupAddresses.includes(minterAddress)){ console.log(`existing minter FOUND and/or FAKE NAME FOUND (if following is null then fake name: ${minterAddress}), not including minter card: ${card.identifier}`) continue } const existingCard = minterNameMap.get(minterName) const cardTimestamp = card.updated || card.created || 0 const existingTimestamp = existingCard?.updated || existingCard?.created || 0 if (!existingCard || cardTimestamp > existingTimestamp) { minterNameMap.set(minterName, card) } } const finalCards = [] const seenMinterNames = new Set() for (const [minterName, card] of minterNameMap.entries()) { if (!seenMinterNames.has(minterName)) { finalCards.push(card) seenMinterNames.add(minterName) } } 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 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) // 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 } if (!cardDataResponse.poll) { console.warn(`Skipping card with no poll: ${card.identifier}`) removeSkeleton(card.identifier) return } const pollPublisherPublicKey = await getPollPublisherPublicKey(cardDataResponse.poll) const cardPublisherPublicKey = await getPublicKeyByName(card.name) if (pollPublisherPublicKey != cardPublisherPublicKey) { console.warn(`not displaying card, QuickMythril pollHijack attack found! Discarding card with identifier: ${card.identifier}`) removeSkeleton(card.identifier) return } const pollResults = await fetchPollResults(cardDataResponse.poll) const bgColor = generateDarkPastelBackgroundBy(card.name) 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) } }) } 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() } } const replaceSkeleton = (cardIdentifier, htmlContent) => { const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`) if (skeletonCard) { skeletonCard.outerHTML = htmlContent } } 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 featureTriggerPassed = await featureTriggerCheck() const groupAdminAddresses = adminGroupsMembers.map(m => m.member) let adminAddresses = [...minterAdminAddresses] if (!featureTriggerPassed) { console.log(`featureTrigger is NOT passed, only showing admin results from Minter Admins and Group Admins`) 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 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 = `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}
${invitedText}(click COMMENTS button to open/close card comments)
By: ${creator} - ${formattedDate}
${commentDataResponse.creator} ${adminBadge}
${commentDataResponse.content}
${timestamp}