Compare commits

...

23 Commits

Author SHA1 Message Date
QuickMythril
1aa4985375 sort votes by most Yes & least No 2025-02-03 01:46:31 -05:00
QuickMythril
7cfd0357b5 fix blocklist toggle button text 2025-02-03 01:21:17 -05:00
QuickMythril
41e1369d86 prevent cards from automatically loading 2025-02-03 01:15:18 -05:00
QuickMythril
9f645f5582 add message editing to all forums 2025-02-03 01:04:19 -05:00
QuickMythril
3a083f99f6 sort MAM board admin list by name with NULL first 2025-02-03 00:51:49 -05:00
QuickMythril
1b5e8c38e1 load admins before cards in MAM board 2025-02-03 00:48:59 -05:00
QuickMythril
e79e0bf4b1 show public boards when not logged in 2025-02-03 00:48:15 -05:00
QuickMythril
02868171e3 fix applyVoteSortingData for admin board 2025-02-03 00:45:37 -05:00
QuickMythril
9971c6d595 move more code to Shared.js 2025-02-03 00:43:21 -05:00
QuickMythril
0df227d63d move duplicate sorting code & related functions to Shared.js 2025-02-03 00:39:38 -05:00
QuickMythril
057b41af1d fix date format 2025-02-03 00:31:16 -05:00
QuickMythril
eb56e67232 add message deletion to all forums 2025-02-03 00:30:12 -05:00
QuickMythril
b63b894dcb add card deletion to all boards 2025-02-03 00:26:54 -05:00
QuickMythril
001f762266 fix dark gray loading text on MAM board 2025-02-03 00:24:26 -05:00
QuickMythril
1bfa938caf always show group approval table on Minter & MAM boards 2025-02-03 00:23:31 -05:00
QuickMythril
ef8770c5ca add button to show/hide minter admin list in MAM board 2025-02-03 00:21:14 -05:00
QuickMythril
aabb6ab0d4 add count of minter admins & signatures needed to MAM board 2025-02-03 00:19:22 -05:00
QuickMythril
daf5400aea remove unused versioning code 2025-02-03 00:17:28 -05:00
QuickMythril
c5dfd29d94 highlight current approval admin row 2025-02-03 00:16:18 -05:00
QuickMythril
2a14248e3a add brighter approval admin name color 2025-02-03 00:15:08 -05:00
QuickMythril
dcc9046059 add sorting to MAM board 2025-02-03 00:14:15 -05:00
QuickMythril
bcab208528 set voted card colors to avg of current & previous 2025-02-03 00:11:56 -05:00
QuickMythril
e37246974e v1.06.1 from QDN 2025-02-02 02:36:40 -05:00
9 changed files with 874 additions and 531 deletions

View File

@ -163,6 +163,36 @@
background-color: #19403d; 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;
}
.edit-button {
align-self: flex-end;
margin-top: 1vh;
background-color: #897016;
color: #ffffff;
border: none;
border-radius: 1vh;
padding: 0.3vh 0.6vh;
cursor: pointer;
}
.edit-button:hover {
background-color: #402d09;
}
/* forum-styles.css additions */ /* forum-styles.css additions */
.message-input-section { .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;"> <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> <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> <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;"> <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="0">Show All</option>
<option value="1">Last 1 day</option> <option value="1">Last 1 day</option>
@ -121,9 +128,14 @@ const loadAddRemoveAdminPage = async () => {
event.preventDefault() event.preventDefault()
await publishARCard(addRemoveIdentifierPrefix) await publishARCard(addRemoveIdentifierPrefix)
}) })
document.getElementById("sort-select").addEventListener("change", async () => {
// Only re-load the cards whenever user presses the refresh button.
})
await featureTriggerCheck() await featureTriggerCheck()
await loadCards(addRemoveIdentifierPrefix)
await displayExistingMinterAdmins() await displayExistingMinterAdmins()
// Only load the cards whenever user presses the refresh button.
await fetchAllARTxData() await fetchAllARTxData()
} }
@ -133,6 +145,19 @@ const toggleProposeButton = () => {
proposeButton.style.display === 'flex' ? 'none' : 'flex' 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 fetchAllARTxData = async () => {
const addAdmTx = "ADD_GROUP_ADMIN" const addAdmTx = "ADD_GROUP_ADMIN"
const remAdmTx = "REMOVE_GROUP_ADMIN" const remAdmTx = "REMOVE_GROUP_ADMIN"
@ -215,7 +240,29 @@ const displayExistingMinterAdmins = async () => {
try { try {
// 1) Fetch addresses // 1) Fetch addresses
const admins = await fetchMinterGroupAdmins() const admins = await fetchMinterGroupAdmins()
// Get names for each admin first
for (const admin of admins) {
try {
admin.name = await getNameFromAddress(admin.member)
} catch (err) {
console.warn(`Error fetching name for ${admin.member}:`, err)
admin.name = null
}
}
// Sort admin list by name with NULL ACCOUNT at the top
admins.sort((a, b) => {
if (a.member === nullAddress && b.member !== nullAddress) {
return -1;
}
if (b.member === nullAddress && a.member !== nullAddress) {
return 1;
}
return (a.name || '').localeCompare(b.name || '');
})
minterAdminAddresses = admins.map(m => m.member) 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 = ""; let rowsHtml = "";
for (const adminAddr of admins) { for (const adminAddr of admins) {
if (adminAddr.member === nullAddress) { if (adminAddr.member === nullAddress) {
@ -235,16 +282,8 @@ const displayExistingMinterAdmins = async () => {
</tr> </tr>
` `
continue continue
}
// Attempt to get name
let adminName
try {
adminName = await getNameFromAddress(adminAddr.member)
} catch (err) {
console.warn(`Error fetching name for ${adminAddr.member}:`, err)
adminName = null
} }
const displayName = adminName && adminName !== adminAddr.member ? adminName : "(No Name)" const displayName = adminAddr.name && adminAddr.name !== adminAddr.member ? adminAddr.name : "(No Name)"
rowsHtml += ` rowsHtml += `
<tr> <tr>
<td style="border: 1px solid rgb(150, 199, 224); font-size: 1.5rem; padding: 4px; color:rgb(70, 156, 196)">${displayName}</td> <td style="border: 1px solid rgb(150, 199, 224); font-size: 1.5rem; padding: 4px; color:rgb(70, 156, 196)">${displayName}</td>
@ -252,7 +291,7 @@ const displayExistingMinterAdmins = async () => {
<td style="border: 1px solid rgb(231, 112, 112); padding: 4px;"> <td style="border: 1px solid rgb(231, 112, 112); padding: 4px;">
<button <button
style="padding: 5px; background: red; color: white; border-radius: 3px; cursor: pointer;" style="padding: 5px; background: red; color: white; border-radius: 3px; cursor: pointer;"
onclick="handleProposeDemotionWrapper('${adminName}', '${adminAddr.member}')" onclick="handleProposeDemotionWrapper('${adminAddr.name}', '${adminAddr.member}')"
> >
Propose Demotion Propose Demotion
</button> </button>
@ -262,6 +301,22 @@ const displayExistingMinterAdmins = async () => {
} }
// 3) Build the table // 3) Build the table
const tableHtml = ` 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;"> <table style="width: 100%; border-collapse: collapse;">
<thead> <thead>
<tr style="background:rgb(21, 36, 18); color:rgb(183, 208, 173); font-size: 1.5rem;"> <tr style="background:rgb(21, 36, 18); color:rgb(183, 208, 173); font-size: 1.5rem;">
@ -274,8 +329,13 @@ const displayExistingMinterAdmins = async () => {
${rowsHtml} ${rowsHtml}
</tbody> </tbody>
</table> </table>
</div>
`
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}
` `
adminListContainer.innerHTML = tableHtml
} catch (err) { } catch (err) {
console.error("Error fetching minter admins:", err) console.error("Error fetching minter admins:", err)
adminListContainer.innerHTML = adminListContainer.innerHTML =
@ -714,6 +774,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) => { const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins) => {
// Ensure we have addresses // Ensure we have addresses
if (!minterGroupMembers) { if (!minterGroupMembers) {
@ -920,6 +1010,16 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button> <button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div> </div>
</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-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardIdentifier}" class="comments-container"></div> <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> <textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>

View File

@ -380,123 +380,11 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
selectedSort = sortSelect.value selectedSort = sortSelect.value
} }
if (selectedSort === 'name') { const sortedFinalCards = await sortCards(finalCards, selectedSort, "admin")
// 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;
})
}
encryptedCardsContainer.innerHTML = "" encryptedCardsContainer.innerHTML = ""
const finalVisualFilterCards = finalCards.filter(({card}) => { const finalVisualFilterCards = sortedFinalCards.filter(({card}) => {
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false
@ -1196,20 +1084,36 @@ const handleBanMinter = async (minterName) => {
} }
} }
const getNewestAdminCommentTimestamp = async (cardIdentifier) => { const deleteAdminCard = async (cardIdentifier) => {
try { try {
const comments = await fetchEncryptedComments(cardIdentifier) const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
if (!comments || comments.length === 0) { if (!confirmed) return
return 0 const blankData = {
header: "",
content: "",
links: [],
creator: userState.accountName,
timestamp: Date.now(),
poll: ""
} }
const newestTimestamp = comments.reduce((acc, comment) => { let base64Data = await objectToBase64(blankData)
const cTime = comment.updated || comment.created || 0 if (!base64Data) {
return cTime > acc ? cTime : acc base64Data = btoa(JSON.stringify(blankData))
}, 0) }
return newestTimestamp const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
} catch (err) { await qortalRequest({
console.error('Failed to get newest comment timestamp:', err) action: "PUBLISH_QDN_RESOURCE",
return 0 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.")
} }
} }
@ -1303,9 +1207,9 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
showRemoveHtml = removeActionsHtml showRemoveHtml = removeActionsHtml
if (userVote === 0) { 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) { } 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( const confirmedKick = finalKickTxs.some(
@ -1398,6 +1302,16 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button> <button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div> </div>
</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-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardIdentifier}" class="comments-container"></div> <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> <textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>

View File

@ -32,7 +32,7 @@ const loadMinterAdminToolsPage = async () => {
<div id="tools-submenu" class="tools-submenu"> <div id="tools-submenu" class="tools-submenu">
<div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;"> <div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;">
<button id="toggle-blocklist-button" class="publish-card-button">Add/Remove blockedUsers</button> <button id="toggle-blocklist-button" class="publish-card-button">Show/Hide blockedUsers</button>
<button id="create-group-invite" class="publish-card-button">Create Pending Group Invite</button> <button id="create-group-invite" class="publish-card-button">Create Pending Group Invite</button>
</div> </div>

View File

@ -37,6 +37,7 @@ const loadMinterBoardPage = async () => {
<option value="least-votes">Least Votes</option> <option value="least-votes">Least Votes</option>
<option value="most-votes">Most Votes</option> <option value="most-votes">Most Votes</option>
</select> </select>
<span id="board-card-counter" style="font-size: 1rem; color: #999ccc; padding: 0.5em;"></span>
<select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;"> <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="0">Show All</option>
<option value="1">Last 1 day</option> <option value="1">Last 1 day</option>
@ -44,6 +45,10 @@ const loadMinterBoardPage = async () => {
<option value="30" selected>Last 30 days</option> <option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option> <option value="90">Last 90 days</option>
</select> </select>
<label style="color:rgb(181, 181, 181); margin-left: 10px;">
<input type="checkbox" id="show-existing-checkbox" />
Show Existing Minter Cards (history)
</label>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div> <div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;"> <div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<form id="publish-card-form" class="publish-card-form"> <form id="publish-card-form" class="publish-card-form">
@ -135,20 +140,17 @@ const loadMinterBoardPage = async () => {
}) })
document.getElementById("time-range-select").addEventListener("change", async () => { document.getElementById("time-range-select").addEventListener("change", async () => {
// Re-load the cards whenever user chooses a new sort option. // Only re-load the cards whenever user presses the refresh button.
await loadCards(minterCardIdentifierPrefix)
}) })
document.getElementById("sort-select").addEventListener("change", async () => { document.getElementById("sort-select").addEventListener("change", async () => {
// Re-load the cards whenever user chooses a new sort option. // Only re-load the cards whenever user presses the refresh button.
await loadCards(minterCardIdentifierPrefix)
}) })
await featureTriggerCheck() await featureTriggerCheck()
await loadCards(minterCardIdentifierPrefix) // Only load the cards whenever user presses the refresh button.
} }
const extractMinterCardsMinterName = async (cardIdentifier) => { const extractMinterCardsMinterName = async (cardIdentifier) => {
// Ensure the identifier starts with the prefix // Ensure the identifier starts with the prefix
if ((!cardIdentifier.startsWith(minterCardIdentifierPrefix)) && (!cardIdentifier.startsWith(addRemoveIdentifierPrefix))) { if ((!cardIdentifier.startsWith(minterCardIdentifierPrefix)) && (!cardIdentifier.startsWith(addRemoveIdentifierPrefix))) {
@ -366,7 +368,13 @@ const processARBoardCards = async (allValidCards) => {
const loadCards = async (cardIdentifierPrefix) => { const loadCards = async (cardIdentifierPrefix) => {
const cardsContainer = document.getElementById("cards-container") const cardsContainer = document.getElementById("cards-container")
let isARBoard = false let isARBoard = false
cardsContainer.innerHTML = "<p>Loading cards...</p>" cardsContainer.innerHTML = `<p style="color:white;">Loading cards...</p>`
const counterSpan = document.getElementById("board-card-counter")
if (counterSpan) {
// Clear or show "Loading..."
counterSpan.textContent = "(loading...)"
}
if (cardIdentifierPrefix.startsWith("QM-AR-card")) { if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
isARBoard = true isARBoard = true
@ -375,6 +383,9 @@ const loadCards = async (cardIdentifierPrefix) => {
let afterTime = 0 let afterTime = 0
const timeRangeSelect = document.getElementById("time-range-select") const timeRangeSelect = document.getElementById("time-range-select")
const showExistingCheckbox = document.getElementById("show-existing-checkbox")
const showExisting = showExistingCheckbox && showExistingCheckbox.checked
if (timeRangeSelect) { if (timeRangeSelect) {
const days = parseInt(timeRangeSelect.value, 10) const days = parseInt(timeRangeSelect.value, 10)
if (days > 0) { if (days > 0) {
@ -418,30 +429,15 @@ const loadCards = async (cardIdentifierPrefix) => {
selectedSort = sortSelect.value selectedSort = sortSelect.value
} }
if (selectedSort === 'name') { const sortedFinalCards = isARBoard
finalCards.sort((a, b) => { ? await sortCards(finalCards, selectedSort, "ar")
const nameA = a.name?.toLowerCase() || '' : await sortCards(finalCards, selectedSort, "minter")
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).
// Create the 'finalCardsArray' that includes the data, etc. // Create the 'finalCardsArray' that includes the data, etc.
let finalCardsArray = [] let finalCardsArray = []
let alreadyMinterCards = []
cardsContainer.innerHTML = '' cardsContainer.innerHTML = ''
for (const card of finalCards) { for (const card of sortedFinalCards) {
try { try {
const skeletonHTML = createSkeletonCardHTML(card.identifier) const skeletonHTML = createSkeletonCardHTML(card.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
@ -477,8 +473,14 @@ const loadCards = async (cardIdentifierPrefix) => {
} else { } else {
const isAlreadyMinter = await verifyMinter(cardDataResponse.creator) const isAlreadyMinter = await verifyMinter(cardDataResponse.creator)
if (isAlreadyMinter) { if (isAlreadyMinter) {
console.warn(`card IS ALREADY a minter, NOT displaying following identifier on the MinterBoard: ${card.identifier}`) console.warn(`card IS ALREADY a minter, adding to alreadyMinterCards array: ${card.identifier}`)
removeSkeleton(card.identifier) removeSkeleton(card.identifier)
alreadyMinterCards.push({
...card,
cardDataResponse,
pollPublisherAddress,
cardPublisherAddress
})
continue continue
} }
} }
@ -489,6 +491,12 @@ const loadCards = async (cardIdentifierPrefix) => {
pollPublisherAddress, pollPublisherAddress,
cardPublisherAddress, cardPublisherAddress,
}) })
if (counterSpan) {
const displayedCount = finalCardsArray.length
const alreadyMinterCount = alreadyMinterCards.length
// If you want to show both
counterSpan.textContent = `(${displayedCount} cards, ${alreadyMinterCount} existingMinters)`
}
} catch (err) { } catch (err) {
console.error(`Error preparing card ${card.identifier}`, err) console.error(`Error preparing card ${card.identifier}`, err)
removeSkeleton(card.identifier) removeSkeleton(card.identifier)
@ -531,9 +539,39 @@ const loadCards = async (cardIdentifierPrefix) => {
replaceSkeleton(cardObj.identifier, finalCardHTML) replaceSkeleton(cardObj.identifier, finalCardHTML)
} }
if (showExisting && alreadyMinterCards.length > 0) {
console.warn(`Rendering Existing Minter cards because user selected showExisting`)
for (const mintedCardObj of alreadyMinterCards) {
const skeletonHTML = createSkeletonCardHTML(mintedCardObj.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
const pollResults = await fetchPollResults(mintedCardObj.cardDataResponse.poll)
const commentCount = await countComments(mintedCardObj.identifier)
const cardUpdatedTime = mintedCardObj.updated || null
const bgColor = generateDarkPastelBackgroundBy(mintedCardObj.name)
const isExistingMinter = true
const finalCardHTML = await createCardHTML(
mintedCardObj.cardDataResponse,
pollResults,
mintedCardObj.identifier,
commentCount,
cardUpdatedTime,
bgColor,
mintedCardObj.cardPublisherAddress,
isExistingMinter
)
replaceSkeleton(mintedCardObj.identifier, finalCardHTML)
}
}
} catch (error) { } catch (error) {
console.error("Error loading cards:", error) console.error("Error loading cards:", error)
cardsContainer.innerHTML = "<p>Failed to load cards.</p>" cardsContainer.innerHTML = "<p>Failed to load cards.</p>"
if (counterSpan) {
counterSpan.textContent = "(error loading)"
}
} }
} }
@ -560,71 +598,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 removeSkeleton = (cardIdentifier) => {
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`) const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
if (skeletonCard) { if (skeletonCard) {
@ -827,236 +800,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. --------------------------------- // Post a comment on a card. ---------------------------------
const postComment = async (cardIdentifier) => { const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`) const commentInput = document.getElementById(`new-comment-${cardIdentifier}`)
@ -1193,6 +936,7 @@ const toggleComments = async (cardIdentifier) => {
const commentButton = document.getElementById(`comment-button-${cardIdentifier}`) const commentButton = document.getElementById(`comment-button-${cardIdentifier}`)
if (!commentsSection || !commentButton) return if (!commentsSection || !commentButton) return
const count = commentButton.dataset.commentCount const count = commentButton.dataset.commentCount
const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display) const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display)
@ -1427,20 +1171,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 checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => {
const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
const isBlockPassed = await featureTriggerCheck() const isBlockPassed = await featureTriggerCheck()
@ -1582,14 +1312,15 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
getNameFromAddress getNameFromAddress
) )
if (transactionType === "GROUP_INVITE" && isSomeTypaAdmin) { if (transactionType === "GROUP_INVITE") {
const approvalButtonHtml = ` const approvalButtonHtml = `
<div style="display: flex; flex-direction: column; margin-top: 1em;"> <div style="display: flex; flex-direction: column; margin-top: 1em;">
<p style="color: rgb(181, 214, 100);"> <p style="color: rgb(181, 214, 100);">
Existing ${transactionType} Approvals: ${uniqueApprovalCount} Existing ${transactionType} Approvals: ${uniqueApprovalCount}
</p> </p>
${tableHtml} ${tableHtml}
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;"> ${isSomeTypaAdmin ? `
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button <button
style=" style="
padding: 8px; padding: 8px;
@ -1606,7 +1337,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
> >
Approve Invite Tx Approve Invite Tx
</button> </button>
</div> </div>
` : ''}
</div> </div>
` `
return approvalButtonHtml return approvalButtonHtml
@ -1679,7 +1411,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
Existing ${transactionType} Approvals: ${uniqueApprovalCount} Existing ${transactionType} Approvals: ${uniqueApprovalCount}
</p> </p>
${tableHtml} ${tableHtml}
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;"> ${isSomeTypaAdmin ? `
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button <button
style=" style="
padding: 8px; padding: 8px;
@ -1696,7 +1429,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
> >
Approve Add-Admin Tx Approve Add-Admin Tx
</button> </button>
</div> </div>
` : ''}
</div> </div>
` `
return approvalButtonHtml return approvalButtonHtml
@ -1709,7 +1443,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
Existing ${transactionType} Approvals: ${uniqueApprovalCount} Existing ${transactionType} Approvals: ${uniqueApprovalCount}
</p> </p>
${tableHtml} ${tableHtml}
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;"> ${isSomeTypaAdmin ? `
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button <button
style=" style="
padding: 8px; padding: 8px;
@ -1726,7 +1461,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
> >
Approve Remove-Admin Tx Approve Remove-Admin Tx
</button> </button>
</div> </div>
` : ''}
</div> </div>
` `
return approvalButtonHtml return approvalButtonHtml
@ -1761,9 +1497,19 @@ const buildApprovalTableHtml = async (approvalTxs, getNameFunc) => {
: "(No registered name)" : "(No registered name)"
const dateStr = new Date(tx.timestamp).toLocaleString() 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 ` return `
<tr> <tr style="${rowStyle}">
<td style="border: 1px solid rgb(255, 255, 255); padding: 4px; color: #234565">${displayName}</td> <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> <td style="border: 1px solid rgb(255, 254, 254); padding: 4px;">${dateStr}</td>
</tr> </tr>
` `
@ -1882,28 +1628,38 @@ const getMinterAvatar = async (minterName) => {
} }
} }
const getNewestCommentTimestamp = async (cardIdentifier) => { const deleteCard = async (cardIdentifier) => {
try { try {
// fetchCommentsForCard returns resources each with at least 'created' or 'updated' const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
const comments = await fetchCommentsForCard(cardIdentifier) if (!confirmed) return
if (!comments || comments.length === 0) { const blankData = {
// No comments => fallback to 0 (or card's own date, if you like) header: "",
return 0 content: "",
links: [],
creator: userState.accountName,
timestamp: Date.now(),
poll: ""
} }
// The newest can be determined by comparing 'updated' or 'created' let base64Data = await objectToBase64(blankData)
const newestTimestamp = comments.reduce((acc, c) => { if (!base64Data) {
const cTime = c.updated || c.created || 0 base64Data = btoa(JSON.stringify(blankData))
return (cTime > acc) ? cTime : acc }
}, 0) await qortalRequest({
return newestTimestamp action: "PUBLISH_QDN_RESOURCE",
} catch (err) { name: userState.accountName,
console.error('Failed to get newest comment timestamp:', err) service: "BLOG_POST",
return 0 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.")
} }
} }
// Create the overall Minter Card HTML ----------------------------------------------- // Create the overall Minter Card HTML -----------------------------------------------
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, address) => { const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, address, isExistingMinter=false) => {
const { header, content, links, creator, creatorAddress, timestamp, poll } = cardData const { header, content, links, creator, creatorAddress, timestamp, poll } = cardData
const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString() const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
const avatarHtml = await getMinterAvatar(creator) const avatarHtml = await getMinterAvatar(creator)
@ -1919,7 +1675,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
createModal('links') createModal('links')
createModal('poll-details') createModal('poll-details')
const inviteButtonHtml = await checkAndDisplayInviteButton(adminYes, creator, cardIdentifier) const inviteButtonHtml = isExistingMinter ? "" : await checkAndDisplayInviteButton(adminYes, creator, cardIdentifier)
let inviteHtmlAdd = (inviteButtonHtml) ? inviteButtonHtml : '' let inviteHtmlAdd = (inviteButtonHtml) ? inviteButtonHtml : ''
let finalBgColor = bgColor let finalBgColor = bgColor
@ -1935,6 +1691,9 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
finalBgColor = "rgba(1, 65, 39, 0.41)"; // or any green you want finalBgColor = "rgba(1, 65, 39, 0.41)"; // or any green you want
} else if (userVote === 1) { } else if (userVote === 1) {
finalBgColor = "rgba(107, 3, 3, 0.3)"; // or any red you want finalBgColor = "rgba(107, 3, 3, 0.3)"; // or any red you want
} else if (isExistingMinter){
finalBgColor = "rgb(99, 99, 99)"
invitedText = `<h4 style="color:rgb(135, 55, 16); margin-bottom: 0.5em;">EXISTING MINTER</h4>`
} else if (hasMinterInvite) { } else if (hasMinterInvite) {
// If so, override background color & add an "INVITED" label // If so, override background color & add an "INVITED" label
finalBgColor = "black"; finalBgColor = "black";
@ -2009,6 +1768,16 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button> <button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div> </div>
</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-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardIdentifier}" class="comments-container"></div> <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> <textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>

View File

@ -1,4 +1,4 @@
const Q_MINTERSHIP_VERSION = "1.06" const Q_MINTERSHIP_VERSION = "1.06.1"
const messageIdentifierPrefix = `mintership-forum-message` const messageIdentifierPrefix = `mintership-forum-message`
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment` const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`
@ -78,28 +78,28 @@ document.addEventListener("DOMContentLoaded", async () => {
mintershipForumLinks.forEach(link => { mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => { link.addEventListener('click', async (event) => {
event.preventDefault() event.preventDefault()
if (!userState.isLoggedIn) { await loadForumPage()
await login()
}
await loadForumPage();
loadRoomContent("general") loadRoomContent("general")
startPollingForNewMessages() startPollingForNewMessages()
createScrollToTopButton() createScrollToTopButton()
if (!userState.isLoggedIn) {
await login()
}
}) })
}) })
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]') const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]')
minterBoardLinks.forEach(link => { minterBoardLinks.forEach(link => {
link.addEventListener("click", async (event) => { link.addEventListener("click", async (event) => {
event.preventDefault(); event.preventDefault()
if (!userState.isLoggedIn) {
await login()
}
if (typeof loadMinterBoardPage === "undefined") { if (typeof loadMinterBoardPage === "undefined") {
console.log("loadMinterBoardPage not found, loading script dynamically...") console.log("loadMinterBoardPage not found, loading script dynamically...")
await loadScript("./assets/js/MinterBoard.js") await loadScript("./assets/js/MinterBoard.js")
} }
await loadMinterBoardPage() await loadMinterBoardPage()
if (!userState.isLoggedIn) {
await login()
}
}) })
}) })
@ -107,15 +107,14 @@ document.addEventListener("DOMContentLoaded", async () => {
addRemoveAdminLinks.forEach(link => { addRemoveAdminLinks.forEach(link => {
link.addEventListener('click', async (event) => { link.addEventListener('click', async (event) => {
event.preventDefault() event.preventDefault()
// Possibly require user to login if not logged
if (!userState.isLoggedIn) {
await login()
}
if (typeof loadMinterBoardPage === "undefined") { if (typeof loadMinterBoardPage === "undefined") {
console.log("loadMinterBoardPage not found, loading script dynamically...") console.log("loadMinterBoardPage not found, loading script dynamically...")
await loadScript("./assets/js/MinterBoard.js") await loadScript("./assets/js/MinterBoard.js")
} }
await loadAddRemoveAdminPage() await loadAddRemoveAdminPage()
if (!userState.isLoggedIn) {
await login()
}
}) })
}) })
@ -240,7 +239,10 @@ const loadForumPage = async () => {
<div class="forum-main mbr-parallax-background cid-ttRnlSkg2R"> <div class="forum-main mbr-parallax-background cid-ttRnlSkg2R">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;"> <div class="forum-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: white; display: flex; align-items: center; justify-content: center;"> <div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: white; display: flex; align-items: center; justify-content: center;">
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;"> ${userState.isLoggedIn ? `
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
` : ''
}
<span>${userState.accountName || 'Guest'}</span> <span>${userState.accountName || 'Guest'}</span>
</div> </div>
</div> </div>
@ -468,6 +470,7 @@ let selectedImages = []
let selectedFiles = [] let selectedFiles = []
let multiResource = [] let multiResource = []
let attachmentIdentifiers = [] let attachmentIdentifiers = []
let editMessageIdentifier = null
// Set up file input handling // Set up file input handling
const setupFileInputs = (room) => { const setupFileInputs = (room) => {
@ -558,9 +561,14 @@ const processSelectedImages = async (selectedImages, multiResource, room) => {
// Handle send message // Handle send message
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => { const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
const messageIdentifier = room === "admins" let messageIdentifier
? `${messageIdentifierPrefix}-${room}-e-${randomID()}` if (editMessageIdentifier) {
: `${messageIdentifierPrefix}-${room}-${randomID()}` messageIdentifier = editMessageIdentifier
} else {
messageIdentifier = room === "admins"
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
: `${messageIdentifierPrefix}-${room}-${randomID()}`
}
try { try {
// Process selected images // Process selected images
@ -644,6 +652,61 @@ 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)
}
}
const handleEditMessage = async (room, existingMessageIdentifier) => {
try {
const editedMessageObject = {
messageHtml: "", // TODO: Add Quill editor content here
hasAttachment: false,
attachments: [],
replyTo: null
}
const base64Message = btoa(JSON.stringify(editedMessageObject))
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("Editing forum message...")
await qortalRequest(request)
} catch (err) {
console.error("Error editing message:", err)
}
}
function clearInputs() { function clearInputs() {
// Clear the file input elements and preview container // Clear the file input elements and preview container
@ -662,6 +725,7 @@ function clearInputs() {
attachmentIdentifiers = [] attachmentIdentifiers = []
selectedImages = [] selectedImages = []
selectedFiles = [] selectedFiles = []
editMessageIdentifier = null
// Remove the reply container // Remove the reply container
const replyContainer = document.querySelector('.reply-container') const replyContainer = document.querySelector('.reply-container')
@ -760,6 +824,8 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
} }
handleReplyLogic(fetchMessages) handleReplyLogic(fetchMessages)
handleDeleteLogic(fetchMessages, room)
handleEditLogic(fetchMessages, room)
await updatePaginationControls(room, limit) await updatePaginationControls(room, limit)
} catch (error) { } catch (error) {
@ -979,6 +1045,24 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
const replyHtml = await buildReplyHtml(message, room) const replyHtml = await buildReplyHtml(message, room)
const attachmentHtml = await buildAttachmentHtml(message, room) const attachmentHtml = await buildAttachmentHtml(message, room)
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar` const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`
let deleteButtonHtml = ''
let editButtonHtml = ''
if (message.name === userState.accountName) {
deleteButtonHtml = `
<button class="delete-button"
data-message-identifier="${message.identifier}"
data-room="${room}">
Delete
</button>
`
editButtonHtml = `
<button class="edit-button"
data-message-identifier="${message.identifier}"
data-room="${room}">
Edit
</button>
`
}
return ` return `
<div class="message-item" data-identifier="${message.identifier}"> <div class="message-item" data-identifier="${message.identifier}">
@ -995,7 +1079,10 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
<div class="attachments-gallery"> <div class="attachments-gallery">
${attachmentHtml} ${attachmentHtml}
</div> </div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button> <div class="message-actions">
${deleteButtonHtml}${editButtonHtml}
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
</div> </div>
` `
} }
@ -1147,6 +1234,41 @@ 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 handleEditLogic = (fetchMessages, room) => {
// Only select buttons that do NOT already have a listener
const editButtons = document.querySelectorAll('.edit-button:not(.bound-edit)')
editButtons.forEach(button => {
button.classList.add('bound-edit')
button.addEventListener('click', async () => {
const existingMessageIdentifier = button.dataset.messageIdentifier
const originalMessage = messagesById[existingMessageIdentifier]
if (!originalMessage) return
editMessageIdentifier = existingMessageIdentifier
const quill = new Quill('#editor')
quill.clipboard.dangerouslyPasteHTML(originalMessage.content)
// Optionally show a small notice: "Editing message..."
})
})
}
const showReplyPreview = (repliedMessage) => { const showReplyPreview = (repliedMessage) => {
replyToMessageIdentifier = repliedMessage.identifier replyToMessageIdentifier = repliedMessage.identifier

View File

@ -53,7 +53,7 @@ const timestampToHumanReadableDate = async(timestamp) => {
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).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) console.log('Formatted date:', formattedDate)
return formattedDate return formattedDate
} }

View File

@ -205,4 +205,417 @@ const fetchAllInviteTransactions = async () => {
} }
} }
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._adminYes = 0
card._adminNo = 0
card._minterYes = 0
card._minterNo = 0
continue
}
// Fetch poll results
const pollResults = await fetchPollResults(pollName)
if (!pollResults) {
card._adminYes = 0
card._adminNo = 0
card._minterYes = 0
card._minterNo = 0
continue
}
// Process them
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
card.decryptedCardData.creator,
card.card.identifier
)
card._adminYes = adminYes
card._adminNo = adminNo
card._minterYes = minterYes
card._minterNo = minterNo
} else {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
})
if (!cardDataResponse || !cardDataResponse.poll) {
card._adminYes = 0
card._adminNo = 0
card._minterYes = 0
card._minterNo = 0
continue
}
const pollResults = await fetchPollResults(cardDataResponse.poll);
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
pollResults,
minterGroupMembers,
minterAdmins,
cardDataResponse.creator,
card.identifier
)
card._adminYes = adminYes
card._adminNo = adminNo
card._minterYes = minterYes
card._minterNo = minterNo
}
} catch (error) {
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
card._adminYes = 0
card._adminNo = 0
card._minterYes = 0
card._minterNo = 0
}
}
if (ascending) {
// least votes first
cards.sort((a, b) => {
const diffAdminYes = a._adminYes - b._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffAdminNo = b._adminNo - a._adminNo
if (diffAdminNo !== 0) return diffAdminNo
const diffMinterYes = a._minterYes - b._minterYes
if (diffMinterYes !== 0) return diffMinterYes
return b._minterNo - a._minterNo
})
} else {
// most votes first
cards.sort((a, b) => {
const diffAdminYes = b._adminYes - a._adminYes
if (diffAdminYes !== 0) return diffAdminYes
const diffAdminNo = a._adminNo - b._adminNo
if (diffAdminNo !== 0) return diffAdminNo
const diffMinterYes = b._minterYes - a._minterYes
if (diffMinterYes !== 0) return diffMinterYes
return a._minterNo - b._minterNo
})
}
}
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,6 @@
</head> </head>
<body> <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"> <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"> <nav class="navbar navbar-dropdown navbar-expand-lg">
@ -201,13 +196,13 @@
<div class="col-12 col-lg-7 card"> <div class="col-12 col-lg-7 card">
<div class="title-wrapper"> <div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2"> <h2 class="mbr-section-title mbr-fonts-style display-2">
v1.06beta 01-31-2025</h2> v1.06.1beta Feb 1 2025</h2>
</div> </div>
</div> </div>
<div class="col-12 col-lg-5 card"> <div class="col-12 col-lg-5 card">
<div class="text-wrapper"> <div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7"> <p class="mbr-text mbr-fonts-style display-7">
<b><u>v1.06b Fixes</u></b>- <b>EMERGENCY UPDATE </b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for RELEASE NOTES, This is an emergency update that is meant to prevent the issue that took place yesterday and ended up stalling quite a few nodes. This means that Q-Mintership should be the ONLY APP UTILIZED FOR THE FUNCTIONALITY IT PROVIDES. <b><u>v1.06.1b Counter</u></b>- <b>NEW COUNTER </b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for RELEASE NOTES, This is a simple update focused on adding a 'counter' to the MinterBoard. The same functionality will be added to the other boards in the future. But this should assist with people realizing how many cards they are 'missing'. Also added 'automatic refresh' when selecting time/date range on ARBoard(MAM) and MinterBoard when selecting ShowExistingMinters.
</p> </p>
</div> </div>
</div> </div>