// // 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("time-range-select").addEventListener("change", async () => {
// Re-load the cards whenever user chooses a new sort option.
await loadCards(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, 0, '', false, true)
const minterName = await searchSimpleResults.name
return minterName
} else if (cardIdentifier.startsWith(addRemoveIdentifierPrefix)) {
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1, 0, '', false, true)
const publisherName = searchSimpleResults.name
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: publisherName,
service: "BLOG_POST",
identifier: cardIdentifier,
})
let nameInvalid = false
const minterName = cardDataResponse.minterName
if (minterName){
return minterName
} else {
nameInvalid = true
console.warn(`fuckery detected on identifier: ${cardIdentifier}, hello dipshit Mythril!, name invalid? Name doesn't match publisher? Returning invalid flag + publisherName...`)
return publisherName
}
}
} catch (error) {
throw error
}
}
const groupAndLabelByIdentifier = (allCards) => {
// Group by identifier
const mapById = new Map()
allCards.forEach(card => {
if (!mapById.has(card.identifier)) {
mapById.set(card.identifier, [])
}
mapById.get(card.identifier).push(card)
})
// For each identifier's group, sort oldest->newest so the first is "master"
const output = []
for (const [identifier, group] of mapById.entries()) {
group.sort((a, b) => {
const aTime = a.created || 0
const bTime = b.created || 0
return aTime - bTime // oldest first
})
// Mark the first as master
group[0].isMaster = true
// The rest are updates
for (let i = 1; i < group.length; i++) {
group[i].isMaster = false
}
// push them all to output
output.push(...group)
}
return output
}
const groupByIdentifierOldestFirst = (allCards) => {
// map of identifier => array of cards
const mapById = new Map()
allCards.forEach(card => {
if (!mapById.has(card.identifier)) {
mapById.set(card.identifier, [])
}
mapById.get(card.identifier).push(card)
})
// sort each group oldest->newest
for (const [identifier, group] of mapById.entries()) {
group.sort((a, b) => {
const aTime = a.created || 0
const bTime = b.created || 0
return aTime - bTime // oldest first
})
}
return mapById
}
const buildMinterNameGroups = async (mapById) => {
// We'll build an array of objects: { minterName, cards }
// Then we can combine any that share the same minterName.
const nameGroups = []
for (let [identifier, group] of mapById.entries()) {
// group[0] is the oldest => "master" card
let masterCard = group[0]
// Filter out any cards that are not published by the 'masterPublisher'
const masterPublisherName = masterCard.name
// Remove any cards in this identifier group that have a different publisherName
const filteredGroup = group.filter(c => c.name === masterPublisherName)
// If filtering left zero cards, skip entire group
if (!filteredGroup.length) {
console.warn(`All cards removed for identifier=${identifier} (different publishers). Skipping.`)
continue
}
// Reassign group to the filtered version, then re-define masterCard
group = filteredGroup
masterCard = group[0] // oldest after filtering
// attempt to obtain minterName from the master card
let masterMinterName
try {
masterMinterName = await extractMinterCardsMinterName(masterCard.identifier)
} catch (err) {
console.warn(`Skipping entire group ${identifier}, no valid minterName from master`, err)
continue
}
// Store an object with the minterName we extracted, plus all cards in that group
nameGroups.push({
minterName: masterMinterName,
cards: group // includes the master & updates
})
}
// Combine them: minterName => array of *all* cards from all matching groups
const combinedMap = new Map()
for (const entry of nameGroups) {
const mName = entry.minterName
if (!combinedMap.has(mName)) {
combinedMap.set(mName, [])
}
combinedMap.get(mName).push(...entry.cards)
}
return combinedMap
}
const getNewestCardPerMinterName = (combinedMap) => {
// We'll produce an array of the newest card for each minterName, this will be utilized as the 'final filter' to display cards published/updated by unique minters.
const finalOutput = []
for (const [mName, cardArray] of combinedMap.entries()) {
// sort by updated or created, descending => newest first
cardArray.sort((a, b) => {
const aTime = a.updated || a.created || 0
const bTime = b.updated || b.created || 0
return bTime - aTime
})
// newest is [0]
finalOutput.push(cardArray[0])
}
// Then maybe globally sort them newest first
finalOutput.sort((a, b) => {
const aTime = a.updated || a.created || 0
const bTime = b.updated || b.created || 0
return bTime - aTime
})
return finalOutput
}
const processMinterBoardCards = async (allValidCards) => {
// group by identifier, sorted oldest->newest
const mapById = groupByIdentifierOldestFirst(allValidCards)
// build a map of minterName => all cards from those identifiers
const minterNameMap = await buildMinterNameGroups(mapById)
// from that map, keep only the single newest card per minterName
const newestCards = getNewestCardPerMinterName(minterNameMap)
// return final array of all newest cards
return newestCards
}
const processARBoardCards = async (allValidCards) => {
const mapById = groupByIdentifierOldestFirst(allValidCards)
// build a map of minterName => all cards from those identifiers
const mapByName = await buildMinterNameGroups(mapById)
// For each minterName group, we might want to sort them newest->oldest
const finalOutput = []
for (const [minterName, group] of mapByName.entries()) {
group.sort((a, b) => {
const aTime = a.updated || a.created || 0
const bTime = b.updated || b.created || 0
return bTime - aTime
})
// both resolution for the duplicate QuickMythril card, and handling of all future duplicates that may be published...
if (group[0].identifier === 'QM-AR-card-Xw3dxL') {
console.warn(`This is a bug that allowed a duplicate prior to the logic displaying them based on original publisher only... displaying in reverse order...`)
group[0].isDuplicate = true
for (let i = 1; i < group.length; i++) {
group[i].isDuplicate = false
}
}else {
group[0].isDuplicate = false
for (let i = 1; i < group.length; i++) {
group[i].isDuplicate = true
}
}
// push them all
finalOutput.push(...group)
}
// Sort final by newest overall
finalOutput.sort((a, b) => {
const aTime = a.updated || a.created || 0
const bTime = b.updated || b.created || 0
return bTime - aTime
})
return finalOutput
}
//Main function to load the Minter Cards ----------------------------------------
const loadCards = async (cardIdentifierPrefix) => {
const cardsContainer = document.getElementById("cards-container")
let isARBoard = false
cardsContainer.innerHTML = "
Loading cards...
"
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
isARBoard = true
console.warn(`ARBoard determined:`, isARBoard)
}
let afterTime = 0
const timeRangeSelect = document.getElementById("time-range-select")
if (timeRangeSelect) {
const days = parseInt(timeRangeSelect.value, 10)
if (days > 0) {
const now = Date.now()
const dayMs = 24 * 60 * 60 * 1000
afterTime = now - days * dayMs // e.g. last X days
console.log(`afterTime for last ${days} days = ${new Date(afterTime).toLocaleString()}`)
}
}
try {
// 1) Fetch raw "BLOG_POST" entries
const response = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime)
if (!response || !Array.isArray(response) || response.length === 0) {
cardsContainer.innerHTML = "
`
return approvalButtonHtml
}
}
const buildApprovalTableHtml = async (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)"
const dateStr = new Date(tx.timestamp).toLocaleString()
return `
${displayName}
${dateStr}
`
})
)
// The total unique approvals = number of entries in approvalMap
const uniqueApprovalCount = approvalMap.size;
// 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, creatorAddress, 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(1, 65, 39, 0.41)"; // or any green you want
} else if (userVote === 1) {
finalBgColor = "rgba(107, 3, 3, 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)
${commenterName} ${adminBadge}
${commentDataResponse.content}
${timestamp}