// // 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 minterCardIdentifierPrefix = "Minter-board-card"
let isExistingCard = false
let existingCardData = {}
let existingCardIdentifier = {}
const MIN_ADMIN_YES_VOTES = 9;
const GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT = 2012800 //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 = `
Minter Board
Publish a Minter Card with Information, and obtain and view the support of the community. Welcome to the Minter Board!
`
document.body.appendChild(mainContent)
createScrollToTopButton()
document.getElementById("publish-card-button").addEventListener("click", async () => {
try {
const fetchedCard = await fetchExistingCard(minterCardIdentifierPrefix)
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?")
if (updateCard) {
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 = {}
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)
}
} else {
// No existing card found
console.log("No existing card found. Creating a new card.")
isExistingCard = false
}
// Show the form
const publishCardView = document.getElementById("publish-card-view")
publishCardView.style.display = "flex"
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.")
}
})
document.getElementById("refresh-cards-button").addEventListener("click", async () => {
const cardsContainer = document.getElementById("cards-container")
cardsContainer.innerHTML = "
Refreshing cards...
"
await loadCards(minterCardIdentifierPrefix)
})
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(minterCardIdentifierPrefix)
})
document.getElementById("sort-select").addEventListener("change", async () => {
// Re-load the cards whenever user chooses a new sort option.
await loadCards(minterCardIdentifierPrefix)
})
await featureTriggerCheck()
await loadCards(minterCardIdentifierPrefix)
}
const extractMinterCardsMinterName = async (cardIdentifier) => {
// Ensure the identifier starts with the prefix
if ((!cardIdentifier.startsWith(minterCardIdentifierPrefix)) && (!cardIdentifier.startsWith(addRemoveIdentifierPrefix))) {
throw new Error('minterCard does not match identifier check')
}
// 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 {
if (cardIdentifier.startsWith(minterCardIdentifierPrefix)){
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1)
const minterName = await searchSimpleResults.name
return minterName
} else if (cardIdentifier.startsWith(addRemoveIdentifierPrefix)) {
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1)
const publisherName = searchSimpleResults.name
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: publisherName,
service: "BLOG_POST",
identifier: cardIdentifier,
})
const minterName = cardDataResponse.minterName
return minterName
}
} catch (error) {
throw error
}
}
const processMinterCards = async (validMinterCards) => {
const latestCardsMap = new Map()
// Deduplicate by identifier, keeping the most recent
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)
}
})
// Convert Map back to array
const uniqueValidCards = Array.from(latestCardsMap.values())
const minterGroupMembers = await fetchMinterGroupMembers()
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
const minterNameMap = new Map()
// For each card, extract minterName safely
for (const card of uniqueValidCards) {
let minterName
try {
// If this throws, we catch below and skip
minterName = await extractMinterCardsMinterName(card.identifier)
} catch (error) {
console.warn(
`Skipping card ${card.identifier} because extractMinterCardsMinterName failed:`,
error
)
continue // Skip this card and move on
}
console.log(`minterName`, minterName)
// Next, get minterNameInfo
const minterNameInfo = await getNameInfo(minterName)
if (!minterNameInfo) {
console.warn(`minterNameInfo is null for minter: ${minterName}, skipping card.`)
continue
}
const minterAddress = minterNameInfo.owner
// Validate the address
const addressValid = await getAddressInfo(minterAddress)
if (!minterAddress || !addressValid) {
console.warn(`minterAddress invalid or missing for: ${minterName}, skipping card.`, minterAddress)
continue
}
// If this is a 'regular' minter card, skip if user is already a minter
if (!card.identifier.includes('QM-AR-card')) {
if (minterGroupAddresses.includes(minterAddress)) {
console.log(
`existing minter found or fake name detected. Not including minter card: ${card.identifier}`
)
continue
}
}
// Keep only the most recent card for each minterName
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)
}
}
// Convert minterNameMap to final array
const finalCards = []
const seenMinterNames = new Set()
for (const [minterName, card] of minterNameMap.entries()) {
if (!seenMinterNames.has(minterName)) {
finalCards.push(card)
seenMinterNames.add(minterName)
}
}
// Sort by timestamp descending
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 (cardIdentifierPrefix) => {
const cardsContainer = document.getElementById("cards-container")
let isARBoard = false
cardsContainer.innerHTML = "
`
return approvalButtonHtml
}
}
async function buildApprovalTableHtml(approvalTxs, getNameFunc) {
// Build a Map of adminAddress => one transaction (to handle multiple approvals from same admin)
const approvalMap = new Map()
for (const tx of approvalTxs) {
const adminAddr = tx.creatorAddress
if (!approvalMap.has(adminAddr)) {
approvalMap.set(adminAddr, tx)
}
}
// Turn the map into an array for iteration
const approvalArray = Array.from(approvalMap, ([adminAddr, tx]) => ({ adminAddr, tx }))
// Build table rows asynchronously, since we need getNameFromAddress
const tableRows = await Promise.all(
approvalArray.map(async ({ adminAddr, tx }) => {
let adminName
try {
adminName = await getNameFunc(adminAddr)
} catch (err) {
console.warn(`Error fetching name for ${adminAddr}:`, err)
adminName = null
}
const displayName =
adminName && adminName !== adminAddr
? adminName
: "(No registered name)"
// Format the transaction timestamp
const dateStr = new Date(tx.timestamp).toLocaleString()
return `
${displayName}
${dateStr}
`
})
)
// The total unique approvals = number of entries in approvalMap
const uniqueApprovalCount = approvalMap.size;
// 4) Wrap the table in a container with horizontal scroll:
// 1) max-width: 100% makes it fit the parent (card) width
// 2) overflow-x: auto allows scrolling if the table is too wide
const containerHtml = `
Admin Name
Approval Time
${tableRows.join("")}
`
// Return both the container-wrapped table and the count of unique approvals
return {
tableHtml: containerHtml,
uniqueApprovalCount
}
}
const handleGroupApproval = async (pendingSignature) => {
try{
if (!userState.isMinterAdmin) {
console.warn(`non-admin attempting to sign approval!`)
return
}
const fee = 0.01
const adminPublicKey = await getPublicKeyByName(userState.accountName)
const txGroupId = 0
const rawGroupApprovalTransaction = await createGroupApprovalTransaction(adminPublicKey, pendingSignature, txGroupId, fee)
const signedGroupApprovalTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
unsignedBytes: rawGroupApprovalTransaction
})
let txToProcess = signedGroupApprovalTransaction
const processGroupApprovalTx = await processTransaction(txToProcess)
if (processGroupApprovalTx) {
alert(`transaction processed, please wait for CONFIRMATION: ${JSON.stringify(processGroupApprovalTx)}`)
} else {
alert(`creating tx failed for some reason`)
}
}catch(error){
console.error(error)
throw error
}
}
const handleJoinGroup = async (minterAddress) => {
try{
if (userState.accountAddress === minterAddress) {
console.log(`minter user found `)
const qRequestAttempt = await qortalRequest({
action: "JOIN_GROUP",
groupId: 694
})
if (qRequestAttempt) {
return true
}
const joinerPublicKey = getPublicKeyFromAddress(minterAddress)
const fee = 0.01
const joinGroupTransactionData = await createGroupJoinTransaction(minterAddress, joinerPublicKey, 694, 0, fee)
const signedJoinGroupTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
unsignedBytes: joinGroupTransactionData
})
let txToProcess = signedJoinGroupTransaction
const processJoinGroupTransaction = await processTransaction(txToProcess)
if (processJoinGroupTransaction){
console.warn(`processed JOIN_GROUP tx`,processJoinGroupTransaction)
alert(`JOIN GROUP Transaction Processed Successfully, please WAIT FOR CONFIRMATION txData: ${JSON.stringify(processJoinGroupTransaction)}`)
}
} else {
console.warn(`user is not the minter`)
return ''
}
} catch(error){
throw error
}
}
const getMinterAvatar = async (minterName) => {
const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`
try {
const response = await fetch(avatarUrl, { method: 'HEAD' })
if (response.ok) {
return ``
} else {
return ''
}
} catch (error) {
console.error('Error checking avatar availability:', error)
return ''
}
}
const getNewestCommentTimestamp = async (cardIdentifier) => {
try {
// fetchCommentsForCard returns resources each with at least 'created' or 'updated'
const comments = await fetchCommentsForCard(cardIdentifier)
if (!comments || comments.length === 0) {
// No comments => fallback to 0 (or card's own date, if you like)
return 0
}
// The newest can be determined by comparing 'updated' or 'created'
const newestTimestamp = comments.reduce((acc, c) => {
const cTime = c.updated || c.created || 0
return (cTime > acc) ? cTime : acc
}, 0)
return newestTimestamp
} catch (err) {
console.error('Failed to get newest comment timestamp:', err)
return 0
}
}
// Create the overall Minter Card HTML -----------------------------------------------
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, address) => {
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) => `
`).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, detailsHtml, userVote } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier)
createModal('links')
createModal('poll-details')
const inviteButtonHtml = await checkAndDisplayInviteButton(adminYes, creator, cardIdentifier)
let inviteHtmlAdd = (inviteButtonHtml) ? inviteButtonHtml : ''
let finalBgColor = bgColor
let invitedText = "" // for "INVITED" label if found
const addressInfo = await getAddressInfo(address)
const penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '
'
try {
const invites = await fetchGroupInvitesByAddress(address)
const hasMinterInvite = invites.some((invite) => invite.groupId === 694)
if (userVote === 0) {
finalBgColor = "rgba(0, 192, 0, 0.3)"; // or any green you want
} else if (userVote === 1) {
finalBgColor = "rgba(192, 0, 0, 0.3)"; // or any red you want
} else if (hasMinterInvite) {
// If so, override background color & add an "INVITED" label
finalBgColor = "black";
invitedText = `
INVITED
`
if (userState.accountName === creator){ //Check also if the creator is the user, and display the join group button if so.
inviteHtmlAdd = `
`
}else{
console.log(`user is not the minter... NOT displaying any join button`)
inviteHtmlAdd = ''
}
}
//do not display invite button as they're already invited. Create a join button instead.
} catch (error) {
console.error("Error checking invites for user:", error)
}
return `
${avatarHtml}
${creator} - Level ${addressInfo.level}
${header}
${penaltyText}${adjustmentText}${invitedText}
USER'S POST
${content}
USER'S LINKS
${linksHTML}
CURRENT SUPPORT RESULTS
${detailsHtml}
${inviteHtmlAdd}
Admin Yes: ${adminYes}Admin No: ${adminNo}
Minter Yes: ${minterYes}Minter No: ${minterNo}
Total Yes: ${totalYes}Weight: ${totalYesWeight}Total No: ${totalNo}Weight: ${totalNoWeight}
SUPPORT ACTION FOR
${creator}
(click COMMENTS button to open/close card comments)
${commentDataResponse.creator} ${adminBadge}
${commentDataResponse.content}
${timestamp}