QM-Mintership v1.05.2[2]b #8

Open
Quick_Mythril wants to merge 15 commits from Quick_Mythril/Q-Mintership-Alpha:testing-20250130 into main
8 changed files with 686 additions and 491 deletions

View File

@ -163,6 +163,21 @@
background-color: #19403d;
}
.delete-button {
align-self: flex-end;
margin-top: 1vh;
background-color: #891616;
color: #ffffff;
border: none;
border-radius: 1vh;
padding: 0.3vh 0.6vh;
cursor: pointer;
}
.delete-button:hover {
background-color: #3d1919;
}
/* forum-styles.css additions */
.message-input-section {

View File

@ -59,6 +59,13 @@ const loadAddRemoveAdminPage = async () => {
<div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button>
<select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:white; background-color: black;">
<option value="newest" selected>Sort by Date</option>
<option value="name">Sort by Name</option>
<option value="recent-comments">Newest Comments</option>
<option value="least-votes">Least Votes</option>
<option value="most-votes">Most Votes</option>
</select>
<select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;">
<option value="0">Show All</option>
<option value="1">Last 1 day</option>
@ -121,6 +128,12 @@ const loadAddRemoveAdminPage = async () => {
event.preventDefault()
await publishARCard(addRemoveIdentifierPrefix)
})
document.getElementById("sort-select").addEventListener("change", async () => {
// Re-load the cards whenever user chooses a new sort option.
await loadCards(addRemoveIdentifierPrefix)
})
await featureTriggerCheck()
await loadCards(addRemoveIdentifierPrefix)
await displayExistingMinterAdmins()
@ -133,6 +146,19 @@ const toggleProposeButton = () => {
proposeButton.style.display === 'flex' ? 'none' : 'flex'
}
const toggleAdminTable = () => {
const tableContainer = document.getElementById("adminTableContainer")
const toggleBtn = document.getElementById("toggleAdminTableButton")
if (tableContainer.style.display === "none") {
tableContainer.style.display = "block"
toggleBtn.textContent = "Hide Minter Admins"
} else {
tableContainer.style.display = "none"
toggleBtn.textContent = "Show Minter Admins"
}
}
const fetchAllARTxData = async () => {
const addAdmTx = "ADD_GROUP_ADMIN"
const remAdmTx = "REMOVE_GROUP_ADMIN"
@ -216,6 +242,9 @@ const displayExistingMinterAdmins = async () => {
// 1) Fetch addresses
const admins = await fetchMinterGroupAdmins()
minterAdminAddresses = admins.map(m => m.member)
// Compute total admin count and signatures needed (40%, rounded up)
const totalAdmins = admins.length;
const signaturesNeeded = Math.ceil(totalAdmins * 0.40);
let rowsHtml = "";
for (const adminAddr of admins) {
if (adminAddr.member === nullAddress) {
@ -262,6 +291,22 @@ const displayExistingMinterAdmins = async () => {
}
// 3) Build the table
const tableHtml = `
<div style="text-align: center; margin-bottom: 1em;">
<button
id="toggleAdminTableButton"
onclick="toggleAdminTable()"
style="
padding: 10px;
background: #444;
color: #fff;
border-radius: 5px;
cursor: pointer;
"
>
Show Minter Admins
</button>
</div>
<div id="adminTableContainer" style="display: none;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background:rgb(21, 36, 18); color:rgb(183, 208, 173); font-size: 1.5rem;">
@ -274,8 +319,13 @@ const displayExistingMinterAdmins = async () => {
${rowsHtml}
</tbody>
</table>
</div>
`
adminListContainer.innerHTML = tableHtml
adminListContainer.innerHTML = `
<h3 style="color:rgb(212, 212, 212);">Existing Minter Admins: ${totalAdmins}</h3>
<h4 style="color:rgb(212, 212, 212);">Signatures for Group Approval (40%): ${signaturesNeeded}</h4>
${tableHtml}
`;
} catch (err) {
console.error("Error fetching minter admins:", err)
adminListContainer.innerHTML =
@ -714,6 +764,36 @@ const handleRemoveMinterGroupAdmin = async (name, address) => {
}
}
const deleteARCard = async (cardIdentifier) => {
try {
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
if (!confirmed) return
const blankData = {
header: "",
content: "",
links: [],
creator: userState.accountName,
timestamp: Date.now(),
poll: ""
}
let base64Data = await objectToBase64(blankData)
if (!base64Data) {
base64Data = btoa(JSON.stringify(blankData))
}
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: cardIdentifier,
data64: base64Data,
})
alert("Your card has been effectively deleted.")
} catch (error) {
console.error("Error deleting AR card:", error)
alert("Failed to delete the card. Check console for details.")
}
}
const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins) => {
// Ensure we have addresses
if (!minterGroupMembers) {
@ -920,6 +1000,16 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div>
</div>
${creator === userState.accountName ? `
<div style="margin-top: 0.8em;">
<button
style="padding: 10px; background: darkred; color: white; border-radius: 4px; cursor: pointer;"
onclick="deleteARCard('${cardIdentifier}')"
>
DELETE CARD
</button>
</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>

View File

@ -439,123 +439,11 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
selectedSort = sortSelect.value
}
if (selectedSort === 'name') {
// Sort alphabetically by the minter's name
finalCards.sort((a, b) => {
const nameA = a.decryptedCardData.minterName?.toLowerCase() || ''
const nameB = b.decryptedCardData.minterName?.toLowerCase() || ''
return nameA.localeCompare(nameB)
})
} else if (selectedSort === 'recent-comments') {
// We need each card's newest comment timestamp for sorting
for (let card of finalCards) {
card.newestCommentTimestamp = await getNewestAdminCommentTimestamp(card.card.identifier)
}
// Then sort descending by newest comment
finalCards.sort((a, b) =>
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
)
} else if (selectedSort === 'least-votes') {
// TODO: Add the logic to sort by LEAST total ADMIN votes, then totalYesWeight
const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins()
for (const finalCard of finalCards) {
try {
const pollName = finalCard.decryptedCardData.poll
// If card or poll is missing, default to zero
if (!pollName) {
finalCard._adminTotalVotes = 0
finalCard._yesWeight = 0
continue
}
const pollResults = await fetchPollResults(pollName)
if (!pollResults || pollResults.error) {
finalCard._adminTotalVotes = 0
finalCard._yesWeight = 0
continue
}
// Pull only the adminYes/adminNo/totalYesWeight from processPollData
const {
adminYes,
adminNo,
totalYesWeight
} = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
finalCard.decryptedCardData.creator,
finalCard.card.identifier
)
finalCard._adminTotalVotes = adminYes + adminNo
finalCard._yesWeight = totalYesWeight
} catch (error) {
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
finalCard._adminTotalVotes = 0
finalCard._yesWeight = 0
}
}
// Sort ascending by (adminYes + adminNo), then descending by totalYesWeight
finalCards.sort((a, b) => {
const diffAdminTotal = a._adminTotalVotes - b._adminTotalVotes
if (diffAdminTotal !== 0) return diffAdminTotal
// If there's a tie, show the card with higher yesWeight first
return b._yesWeight - a._yesWeight
})
} else if (selectedSort === 'most-votes') {
// TODO: Add the logic to sort by MOST total ADMIN votes, then totalYesWeight
const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins()
for (const finalCard of finalCards) {
try {
const pollName = finalCard.decryptedCardData.poll
if (!pollName) {
finalCard._adminTotalVotes = 0
finalCard._yesWeight = 0
continue
}
const pollResults = await fetchPollResults(pollName)
if (!pollResults || pollResults.error) {
finalCard._adminTotalVotes = 0
finalCard._yesWeight = 0
continue
}
const {
adminYes,
adminNo,
totalYesWeight
} = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
finalCard.decryptedCardData.creator,
finalCard.card.identifier
)
finalCard._adminTotalVotes = adminYes + adminNo
finalCard._yesWeight = totalYesWeight
} catch (error) {
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
finalCard._adminTotalVotes = 0
finalCard._yesWeight = 0
}
}
// Sort descending by (adminYes + adminNo), then descending by totalYesWeight
finalCards.sort((a, b) => {
const diffAdminTotal = b._adminTotalVotes - a._adminTotalVotes
if (diffAdminTotal !== 0) return diffAdminTotal
return b._yesWeight - a._yesWeight
})
} else {
// Sort cards by timestamp (most recent first)
finalCards.sort((a, b) => {
const timestampA = a.card.updated || a.card.created || 0
const timestampB = b.card.updated || b.card.created || 0
return timestampB - timestampA;
})
}
const sortedFinalCards = await sortCards(finalCards, selectedSort, "admin")
encryptedCardsContainer.innerHTML = ""
const finalVisualFilterCards = finalCards.filter(({card}) => {
const finalVisualFilterCards = sortedFinalCards.filter(({card}) => {
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false
@ -1257,20 +1145,36 @@ const handleBanMinter = async (minterName) => {
}
}
const getNewestAdminCommentTimestamp = async (cardIdentifier) => {
const deleteAdminCard = async (cardIdentifier) => {
try {
const comments = await fetchEncryptedComments(cardIdentifier)
if (!comments || comments.length === 0) {
return 0
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
if (!confirmed) return
const blankData = {
header: "",
content: "",
links: [],
creator: userState.accountName,
timestamp: Date.now(),
poll: ""
}
const newestTimestamp = comments.reduce((acc, comment) => {
const cTime = comment.updated || comment.created || 0
return cTime > acc ? cTime : acc
}, 0)
return newestTimestamp
} catch (err) {
console.error('Failed to get newest comment timestamp:', err)
return 0
let base64Data = await objectToBase64(blankData)
if (!base64Data) {
base64Data = btoa(JSON.stringify(blankData))
}
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "MAIL_PRIVATE",
identifier: cardIdentifier,
data64: base64Data,
encrypt: true,
publicKeys: verifiedAdminPublicKeys
})
alert("Your card has been effectively deleted.")
} catch (error) {
console.error("Error deleting Admin card:", error)
alert("Failed to delete the card. Check console for details.")
}
}
@ -1352,9 +1256,9 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
showRemoveHtml = removeActionsHtml
if (userVote === 0) {
cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want
cardColorCode = "rgba(1, 128, 20, 0.35)"; // or any green you want
} else if (userVote === 1) {
cardColorCode = "rgba(55, 12, 12, 0.61)"; // or any red you want
cardColorCode = "rgba(124, 6, 6, 0.45)"; // or any red you want
}
const confirmedKick = finalKickTxs.some(
@ -1475,6 +1379,16 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div>
</div>
${creator === userState.accountName ? `
<div style="margin-top: 0.8em;">
<button
style="padding: 10px; background: darkred; color: white; border-radius: 4px; cursor: pointer;"
onclick="deleteAdminCard('${cardIdentifier}')"
>
DELETE CARD
</button>
</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>

View File

@ -366,7 +366,7 @@ const processARBoardCards = async (allValidCards) => {
const loadCards = async (cardIdentifierPrefix) => {
const cardsContainer = document.getElementById("cards-container")
let isARBoard = false
cardsContainer.innerHTML = "<p>Loading cards...</p>"
cardsContainer.innerHTML = `<p style="color:white;">Loading cards...</p>`
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
isARBoard = true
@ -418,30 +418,13 @@ const loadCards = async (cardIdentifierPrefix) => {
selectedSort = sortSelect.value
}
if (selectedSort === 'name') {
finalCards.sort((a, b) => {
const nameA = a.name?.toLowerCase() || ''
const nameB = b.name?.toLowerCase() || ''
return nameA.localeCompare(nameB)
})
} else if (selectedSort === 'recent-comments') {
// If you need the newest comment timestamp
for (let card of finalCards) {
card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier)
}
finalCards.sort((a, b) =>
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
)
} else if (selectedSort === 'least-votes') {
await applyVoteSortingData(finalCards, /* ascending= */ true)
} else if (selectedSort === 'most-votes') {
await applyVoteSortingData(finalCards, /* ascending= */ false)
}
// else 'newest' => do nothing (already sorted newest-first by your process functions).
const sortedFinalCards = isARBoard
? await sortCards(finalCards, selectedSort, "ar")
: await sortCards(finalCards, selectedSort, "minter")
// Create the 'finalCardsArray' that includes the data, etc.
let finalCardsArray = []
cardsContainer.innerHTML = ''
for (const card of finalCards) {
for (const card of sortedFinalCards) {
try {
const skeletonHTML = createSkeletonCardHTML(card.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
@ -560,71 +543,6 @@ const verifyMinter = async (minterName) => {
}
}
const applyVoteSortingData = async (cards, ascending = true) => {
const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins()
for (const card of cards) {
try {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
})
if (!cardDataResponse || !cardDataResponse.poll) {
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
continue
}
const pollResults = await fetchPollResults(cardDataResponse.poll);
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
cardDataResponse.creator,
card.identifier
)
card._adminVotes = adminYes + adminNo
card._adminYes = adminYes
card._minterVotes = minterYes + minterNo
card._minterYes = minterYes
} catch (error) {
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
}
}
if (ascending) {
// least votes first
cards.sort((a, b) => {
const diffAdminTotal = a._adminVotes - b._adminVotes
if (diffAdminTotal !== 0) return diffAdminTotal
const diffAdminYes = a._adminYes - b._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffMinterTotal = a._minterVotes - b._minterVotes
if (diffMinterTotal !== 0) return diffMinterTotal
return a._minterYes - b._minterYes
})
} else {
// most votes first
cards.sort((a, b) => {
const diffAdminTotal = b._adminVotes - a._adminVotes
if (diffAdminTotal !== 0) return diffAdminTotal
const diffAdminYes = b._adminYes - a._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffMinterTotal = b._minterVotes - a._minterVotes
if (diffMinterTotal !== 0) return diffMinterTotal
return b._minterYes - a._minterYes
})
}
}
const removeSkeleton = (cardIdentifier) => {
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
if (skeletonCard) {
@ -827,236 +745,6 @@ const publishCard = async (cardIdentifierPrefix) => {
}
}
let globalVoterMap = new Map()
const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator, cardIdentifier) => {
if (!pollData || !Array.isArray(pollData.voteWeights) || !Array.isArray(pollData.votes)) {
console.warn("Poll data is missing or invalid. pollData:", pollData)
return {
adminYes: 0,
adminNo: 0,
minterYes: 0,
minterNo: 0,
totalYes: 0,
totalNo: 0,
totalYesWeight: 0,
totalNoWeight: 0,
detailsHtml: `<p>Poll data is invalid or missing.</p>`,
userVote: null
}
}
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
let userVote = null
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 (voterAddress === userState.accountAddress) {
userVote = optionIndex
}
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 = `
<div class="poll-details-container" id'"${creator}-poll-details">
<h1 style ="color:rgb(123, 123, 85); text-align: center; font-size: 2.0rem">${creator}'s</h1><h3 style="color: white; text-align: center; font-size: 1.8rem"> Support Poll Result Details</h3>
<h4 style="color: green; text-align: center;">Yes Vote Details</h4>
${yesTableHtml}
<h4 style="color: red; text-align: center; margin-top: 2em;">No Vote Details</h4>
${noTableHtml}
</div>
`
const totalYes = adminYes + minterYes
const totalNo = adminNo + minterNo
return {
adminYes,
adminNo,
minterYes,
minterNo,
totalYes,
totalNo,
totalYesWeight: totalMinterAndAdminYesWeight,
totalNoWeight: totalMinterAndAdminNoWeight,
detailsHtml,
userVote
}
}
const createVoterMap = async (voters, cardIdentifier) => {
const voterMap = new Map()
voters.forEach((voter) => {
const voterNameOrAddress = voter.voterName || voter.voterAddress
voterMap.set(voterNameOrAddress, {
vote: voter.optionIndex === 0 ? "yes" : "no", // Use optionIndex directly
voterType: voter.isAdmin ? "Admin" : voter.isMinter ? "Minter" : "User",
blocksMinted: voter.blocksMinted,
})
})
globalVoterMap.set(cardIdentifier, voterMap)
}
const buildVotersTableHtml = (voters, tableColor) => {
if (!voters.length) {
return `<p>No voters here.</p>`
}
// Decide extremely dark background for the <tbody>
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 <thead>, bodyBackground for the <tbody>
const minterColor = 'rgb(98, 122, 167)'
const adminColor = 'rgb(44, 209, 151)'
const userColor = 'rgb(102, 102, 102)'
return `
<table style="
width: 100%;
border-style: dotted;
border-width: 0.15rem;
border-color: #576b6f;
margin-bottom: 1em;
border-collapse: collapse;
">
<thead style="background: ${tableColor}; color:rgb(238, 238, 238) ;">
<tr style="font-size: 1.5rem;">
<th style="padding: 0.1rem; text-align: center;">Voter Name/Address</th>
<th style="padding: 0.1rem; text-align: center;">Voter Type</th>
<th style="padding: 0.1rem; text-align: center;">Voter Weight(=BlocksMinted)</th>
</tr>
</thead>
<!-- Tbody with extremely dark green or red -->
<tbody style="background-color: ${bodyBackground}; color: #c6c6c6;">
${voters
.map(v => {
const userType = v.isAdmin ? "Admin" : v.isMinter ? "Minter" : "User"
const pollName = v.pollName
const displayName =
v.voterName
? v.voterName
: v.voterAddress
return `
<tr style="font-size: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; font-weight: bold;">
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${displayName}</td>
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${userType}</td>
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${v.blocksMinted}</td>
</tr>
`
})
.join("")}
</tbody>
</table>
`
}
// Post a comment on a card. ---------------------------------
const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`)
@ -1427,20 +1115,6 @@ const createInviteButtonHtml = (creator, cardIdentifier) => {
`
}
const featureTriggerCheck = async () => {
const latestBlockInfo = await getLatestBlockInfo()
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
if (isBlockPassed) {
console.warn(`featureTrigger check (verifyFeatureTrigger) determined block has PASSED:`, isBlockPassed)
featureTriggerPassed = true
return true
} else {
console.warn(`featureTrigger check (verifyFeatureTrigger) determined block has NOT PASSED:`, isBlockPassed)
featureTriggerPassed = false
return false
}
}
const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => {
const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
const isBlockPassed = await featureTriggerCheck()
@ -1563,13 +1237,14 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
getNameFromAddress
)
if (transactionType === "GROUP_INVITE" && isSomeTypaAdmin) {
if (transactionType === "GROUP_INVITE") {
const approvalButtonHtml = `
<div style="display: flex; flex-direction: column; margin-top: 1em;">
<p style="color: rgb(181, 214, 100);">
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
</p>
${tableHtml}
${isSomeTypaAdmin ? `
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button
style="
@ -1588,6 +1263,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
Approve Invite Tx
</button>
</div>
` : ''}
</div>
`
return approvalButtonHtml
@ -1653,13 +1329,14 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
return approvalButtonHtml
}
if (transactionType === "ADD_GROUP_ADMIN" && isSomeTypaAdmin) {
if (transactionType === "ADD_GROUP_ADMIN") {
const approvalButtonHtml = `
<div style="display: flex; flex-direction: column; margin-top: 1em;">
<p style="color: rgb(40, 144, 189);">
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
</p>
${tableHtml}
${isSomeTypaAdmin ? `
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button
style="
@ -1678,18 +1355,20 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
Approve Add-Admin Tx
</button>
</div>
` : ''}
</div>
`
return approvalButtonHtml
}
if (transactionType === "REMOVE_GROUP_ADMIN" && isSomeTypaAdmin) {
if (transactionType === "REMOVE_GROUP_ADMIN") {
const approvalButtonHtml = `
<div style="display: flex; flex-direction: column; margin-top: 1em;">
<p style="color: rgb(189, 40, 40);">
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
</p>
${tableHtml}
${isSomeTypaAdmin ? `
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button
style="
@ -1708,6 +1387,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
Approve Remove-Admin Tx
</button>
</div>
` : ''}
</div>
`
return approvalButtonHtml
@ -1742,9 +1422,19 @@ const buildApprovalTableHtml = async (approvalTxs, getNameFunc) => {
: "(No registered name)"
const dateStr = new Date(tx.timestamp).toLocaleString()
// Check whether this is the current user
const isCurrentUser =
userState &&
userState.accountName &&
adminName &&
adminName.toLowerCase() === userState.accountName.toLowerCase();
// If it's the current user, highlight the row (change to any color/style you prefer)
const rowStyle = isCurrentUser
? "background: rgba(178, 255, 89, 0.2);" // light green highlight
: "";
return `
<tr>
<td style="border: 1px solid rgb(255, 255, 255); padding: 4px; color: #234565">${displayName}</td>
<tr style="${rowStyle}">
<td style="border: 1px solid rgb(255, 255, 255); padding: 4px; color: dodgerblue">${displayName}</td>
<td style="border: 1px solid rgb(255, 254, 254); padding: 4px;">${dateStr}</td>
</tr>
`
@ -1863,23 +1553,33 @@ const getMinterAvatar = async (minterName) => {
}
}
const getNewestCommentTimestamp = async (cardIdentifier) => {
const deleteCard = 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
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
if (!confirmed) return
const blankData = {
header: "",
content: "",
links: [],
creator: userState.accountName,
timestamp: Date.now(),
poll: ""
}
// 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
let base64Data = await objectToBase64(blankData)
if (!base64Data) {
base64Data = btoa(JSON.stringify(blankData))
}
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: cardIdentifier,
data64: base64Data,
})
alert("Your card has been effectively deleted.")
} catch (error) {
console.error("Error deleting Minter card:", error)
alert("Failed to delete the card. Check console for details.")
}
}
@ -1990,6 +1690,16 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div>
</div>
${creator === userState.accountName ? `
<div style="margin-top: 0.8em;">
<button
style="padding: 10px; background: darkred; color: white; border-radius: 4px; cursor: pointer;"
onclick="deleteCard('${cardIdentifier}')"
>
DELETE CARD
</button>
</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="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>

View File

@ -644,6 +644,33 @@ const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImage
}
}
const handleDeleteMessage = async (room, existingMessageIdentifier) => {
try {
const blankMessageObject = {
messageHtml: "<em>This post has been deleted.</em>",
hasAttachment: false,
attachments: [],
replyTo: null
}
const base64Message = btoa(JSON.stringify(blankMessageObject))
const service = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST"
const request = {
action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName,
service: service,
identifier: existingMessageIdentifier,
data64: base64Message
}
if (room === "admins") {
request.encrypt = true
request.publicKeys = adminPublicKeys
}
console.log("Deleting forum message...")
await qortalRequest(request)
} catch (err) {
console.error("Error deleting message:", err)
}
}
function clearInputs() {
// Clear the file input elements and preview container
@ -760,6 +787,7 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
}
handleReplyLogic(fetchMessages)
handleDeleteLogic(fetchMessages, room)
await updatePaginationControls(room, limit)
} catch (error) {
@ -979,6 +1007,16 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
const replyHtml = await buildReplyHtml(message, room)
const attachmentHtml = await buildAttachmentHtml(message, room)
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`
let deleteButtonHtml = ''
if (message.name === userState.accountName) {
deleteButtonHtml = `
<button class="delete-button"
data-message-identifier="${message.identifier}"
data-room="${room}">
Delete
</button>
`
}
return `
<div class="message-item" data-identifier="${message.identifier}">
@ -995,7 +1033,10 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
<div class="attachments-gallery">
${attachmentHtml}
</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
<div class="message-actions">
${deleteButtonHtml}
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
</div>
`
}
@ -1147,6 +1188,24 @@ const handleReplyLogic = (fetchMessages) => {
})
}
const handleDeleteLogic = (fetchMessages, room) => {
// Only select buttons that do NOT already have a listener
const deleteButtons = document.querySelectorAll('.delete-button:not(.bound-delete)')
deleteButtons.forEach(button => {
button.classList.add('bound-delete')
button.addEventListener('click', async () => {
const messageId = button.dataset.messageIdentifier
const postRoom = button.dataset.room
const msg = fetchMessages.find(m => m && m.identifier === messageId)
if (msg) {
const confirmed = confirm("Are you sure you want to delete this post?")
if (!confirmed) return
await handleDeleteMessage(postRoom, messageId)
}
})
})
}
const showReplyPreview = (repliedMessage) => {
replyToMessageIdentifier = repliedMessage.identifier

View File

@ -53,7 +53,7 @@ const timestampToHumanReadableDate = async(timestamp) => {
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const formattedDate = `${day}.${month}.${year}..@${hours}:${minutes}:${seconds}`
const formattedDate = `${year}.${month}.${day} @ ${hours}:${minutes}:${seconds}`
console.log('Formatted date:', formattedDate)
return formattedDate
}

View File

@ -163,9 +163,9 @@ const fetchAllKickBanTxData = async () => {
finalBanTxs,
pendingBanTxs,
}
}
}
const partitionTransactions = (txSearchResults) => {
const partitionTransactions = (txSearchResults) => {
const finalTx = []
const pendingTx = []
@ -178,7 +178,418 @@ const fetchAllKickBanTxData = async () => {
}
return { finalTx, pendingTx };
}
const sortCards = async (cardsArray, selectedSort, board) => {
// Default sort is by newest if none provided
if (!selectedSort) selectedSort = 'newest'
switch (selectedSort) {
case 'name':
// Sort by name
cardsArray.sort((a, b) => {
const nameA = (board === "admin")
? (a.decryptedCardData?.minterName || '').toLowerCase()
: ((board === "ar")
? (a.minterName?.toLowerCase() || '')
: (a.name?.toLowerCase() || '')
)
const nameB = (board === "admin")
? (b.decryptedCardData?.minterName || '').toLowerCase()
: ((board === "ar")
? (b.minterName?.toLowerCase() || '')
: (b.name?.toLowerCase() || '')
)
return nameA.localeCompare(nameB)
})
break
case 'recent-comments':
// Sort by newest comment timestamp
for (let card of cardsArray) {
const cardIdentifier = (board === "admin")
? card.card.identifier
: card.identifier
card.newestCommentTimestamp = await getNewestCommentTimestamp(cardIdentifier, board)
}
cardsArray.sort((a, b) => {
return (b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
})
break
case 'least-votes':
await applyVoteSortingData(cardsArray, /* ascending= */ true, board)
break
case 'most-votes':
await applyVoteSortingData(cardsArray, /* ascending= */ false, board)
break
default:
// Sort by date
cardsArray.sort((a, b) => {
const timestampA = (board === "admin")
? a.card.updated || a.card.created || 0
: a.updated || a.created || 0
const timestampB = (board === "admin")
? b.card.updated || b.card.created || 0
: b.updated || b.created || 0
return timestampB - timestampA;
})
break
}
return cardsArray
}
const getNewestCommentTimestamp = async (cardIdentifier, board) => {
try {
const comments = (board === "admin") ? await fetchEncryptedComments(cardIdentifier) : await fetchCommentsForCard(cardIdentifier)
if (!comments || comments.length === 0) {
return 0
}
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
}
}
const applyVoteSortingData = async (cards, ascending = true, boardType = 'minter') => {
const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins()
for (const card of cards) {
try {
if (boardType === 'admin') {
// For the Admin board, we already have the poll name in `card.decryptedCardData.poll`
// No need to fetch the resource from BLOG_POST
const pollName = card.decryptedCardData?.poll
if (!pollName) {
// No poll => no votes
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
continue
}
// Fetch poll results
const pollResults = await fetchPollResults(pollName)
if (!pollResults) {
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
continue
}
// Process them
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
card.decryptedCardData.creator,
card.card.identifier
)
card._adminVotes = adminYes + adminNo
card._adminYes = adminYes
card._minterVotes = minterYes + minterNo
card._minterYes = minterYes
} else {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
})
if (!cardDataResponse || !cardDataResponse.poll) {
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
continue
}
const pollResults = await fetchPollResults(cardDataResponse.poll);
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
cardDataResponse.creator,
card.identifier
)
card._adminVotes = adminYes + adminNo
card._adminYes = adminYes
card._minterVotes = minterYes + minterNo
card._minterYes = minterYes
}
} catch (error) {
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
card._adminVotes = 0
card._adminYes = 0
card._minterVotes = 0
card._minterYes = 0
}
}
if (ascending) {
// least votes first
cards.sort((a, b) => {
const diffAdminTotal = a._adminVotes - b._adminVotes
if (diffAdminTotal !== 0) return diffAdminTotal
const diffAdminYes = a._adminYes - b._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffMinterTotal = a._minterVotes - b._minterVotes
if (diffMinterTotal !== 0) return diffMinterTotal
return a._minterYes - b._minterYes
})
} else {
// most votes first
cards.sort((a, b) => {
const diffAdminTotal = b._adminVotes - a._adminVotes
if (diffAdminTotal !== 0) return diffAdminTotal
const diffAdminYes = b._adminYes - a._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffMinterTotal = b._minterVotes - a._minterVotes
if (diffMinterTotal !== 0) return diffMinterTotal
return b._minterYes - a._minterYes
})
}
}
let globalVoterMap = new Map()
const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator, cardIdentifier) => {
if (!pollData || !Array.isArray(pollData.voteWeights) || !Array.isArray(pollData.votes)) {
console.warn("Poll data is missing or invalid. pollData:", pollData)
return {
adminYes: 0,
adminNo: 0,
minterYes: 0,
minterNo: 0,
totalYes: 0,
totalNo: 0,
totalYesWeight: 0,
totalNoWeight: 0,
detailsHtml: `<p>Poll data is invalid or missing.</p>`,
userVote: null
}
}
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
let userVote = null
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 (voterAddress === userState.accountAddress) {
userVote = optionIndex
}
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 = `
<div class="poll-details-container" id'"${creator}-poll-details">
<h1 style ="color:rgb(123, 123, 85); text-align: center; font-size: 2.0rem">${creator}'s</h1><h3 style="color: white; text-align: center; font-size: 1.8rem"> Support Poll Result Details</h3>
<h4 style="color: green; text-align: center;">Yes Vote Details</h4>
${yesTableHtml}
<h4 style="color: red; text-align: center; margin-top: 2em;">No Vote Details</h4>
${noTableHtml}
</div>
`
const totalYes = adminYes + minterYes
const totalNo = adminNo + minterNo
return {
adminYes,
adminNo,
minterYes,
minterNo,
totalYes,
totalNo,
totalYesWeight: totalMinterAndAdminYesWeight,
totalNoWeight: totalMinterAndAdminNoWeight,
detailsHtml,
userVote
}
}
const createVoterMap = async (voters, cardIdentifier) => {
const voterMap = new Map()
voters.forEach((voter) => {
const voterNameOrAddress = voter.voterName || voter.voterAddress
voterMap.set(voterNameOrAddress, {
vote: voter.optionIndex === 0 ? "yes" : "no", // Use optionIndex directly
voterType: voter.isAdmin ? "Admin" : voter.isMinter ? "Minter" : "User",
blocksMinted: voter.blocksMinted,
})
})
globalVoterMap.set(cardIdentifier, voterMap)
}
const buildVotersTableHtml = (voters, tableColor) => {
if (!voters.length) {
return `<p>No voters here.</p>`
}
// Decide extremely dark background for the <tbody>
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 <thead>, bodyBackground for the <tbody>
const minterColor = 'rgb(98, 122, 167)'
const adminColor = 'rgb(44, 209, 151)'
const userColor = 'rgb(102, 102, 102)'
return `
<table style="
width: 100%;
border-style: dotted;
border-width: 0.15rem;
border-color: #576b6f;
margin-bottom: 1em;
border-collapse: collapse;
">
<thead style="background: ${tableColor}; color:rgb(238, 238, 238) ;">
<tr style="font-size: 1.5rem;">
<th style="padding: 0.1rem; text-align: center;">Voter Name/Address</th>
<th style="padding: 0.1rem; text-align: center;">Voter Type</th>
<th style="padding: 0.1rem; text-align: center;">Voter Weight(=BlocksMinted)</th>
</tr>
</thead>
<!-- Tbody with extremely dark green or red -->
<tbody style="background-color: ${bodyBackground}; color: #c6c6c6;">
${voters
.map(v => {
const userType = v.isAdmin ? "Admin" : v.isMinter ? "Minter" : "User"
const pollName = v.pollName
const displayName =
v.voterName
? v.voterName
: v.voterAddress
return `
<tr style="font-size: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; font-weight: bold;">
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${displayName}</td>
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${userType}</td>
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${v.blocksMinted}</td>
</tr>
`
})
.join("")}
</tbody>
</table>
`
}
const featureTriggerCheck = async () => {
const latestBlockInfo = await getLatestBlockInfo()
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
if (isBlockPassed) {
console.warn(`featureTrigger check (verifyFeatureTrigger) determined block has PASSED:`, isBlockPassed)
featureTriggerPassed = true
return true
} else {
console.warn(`featureTrigger check (verifyFeatureTrigger) determined block has NOT PASSED:`, isBlockPassed)
featureTriggerPassed = false
return false
}
}

View File

@ -32,11 +32,7 @@
</head>
<body>
<script>
// Variable-based versioning (credit: QuickMythril)
; // Update here in the future
</script>
<section data-bs-version="5.1" class="menu menu1 boldm5 cid-ttRnktJ11Q" once="menu" id="menu1-0">
<nav class="navbar navbar-dropdown navbar-expand-lg">