Compare commits
23 Commits
main
...
testing-20
Author | SHA1 | Date | |
---|---|---|---|
|
1aa4985375 | ||
|
7cfd0357b5 | ||
|
41e1369d86 | ||
|
9f645f5582 | ||
|
3a083f99f6 | ||
|
1b5e8c38e1 | ||
|
e79e0bf4b1 | ||
|
02868171e3 | ||
|
9971c6d595 | ||
|
0df227d63d | ||
|
057b41af1d | ||
|
eb56e67232 | ||
|
b63b894dcb | ||
|
001f762266 | ||
|
1bfa938caf | ||
|
ef8770c5ca | ||
|
aabb6ab0d4 | ||
|
daf5400aea | ||
|
c5dfd29d94 | ||
|
2a14248e3a | ||
|
dcc9046059 | ||
|
bcab208528 | ||
|
e37246974e |
@ -163,6 +163,36 @@
|
||||
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 */
|
||||
|
||||
.message-input-section {
|
||||
|
@ -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,9 +128,14 @@ const loadAddRemoveAdminPage = async () => {
|
||||
event.preventDefault()
|
||||
await publishARCard(addRemoveIdentifierPrefix)
|
||||
})
|
||||
|
||||
document.getElementById("sort-select").addEventListener("change", async () => {
|
||||
// Only re-load the cards whenever user presses the refresh button.
|
||||
})
|
||||
|
||||
await featureTriggerCheck()
|
||||
await loadCards(addRemoveIdentifierPrefix)
|
||||
await displayExistingMinterAdmins()
|
||||
// Only load the cards whenever user presses the refresh button.
|
||||
await fetchAllARTxData()
|
||||
}
|
||||
|
||||
@ -133,6 +145,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"
|
||||
@ -215,7 +240,29 @@ const displayExistingMinterAdmins = async () => {
|
||||
try {
|
||||
// 1) Fetch addresses
|
||||
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)
|
||||
// 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) {
|
||||
@ -236,15 +283,7 @@ const displayExistingMinterAdmins = async () => {
|
||||
`
|
||||
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 += `
|
||||
<tr>
|
||||
<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;">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -262,6 +301,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 +329,13 @@ const displayExistingMinterAdmins = async () => {
|
||||
${rowsHtml}
|
||||
</tbody>
|
||||
</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) {
|
||||
console.error("Error fetching minter admins:", err)
|
||||
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) => {
|
||||
// Ensure we have addresses
|
||||
if (!minterGroupMembers) {
|
||||
@ -920,6 +1010,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>
|
||||
|
@ -380,123 +380,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
|
||||
|
||||
@ -1196,20 +1084,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.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1303,9 +1207,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(
|
||||
@ -1398,6 +1302,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>
|
||||
|
@ -32,7 +32,7 @@ const loadMinterAdminToolsPage = async () => {
|
||||
|
||||
<div id="tools-submenu" class="tools-submenu">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
@ -37,6 +37,7 @@ const loadMinterBoardPage = async () => {
|
||||
<option value="least-votes">Least Votes</option>
|
||||
<option value="most-votes">Most Votes</option>
|
||||
</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;">
|
||||
<option value="0">Show All</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="90">Last 90 days</option>
|
||||
</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="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||
<form id="publish-card-form" class="publish-card-form">
|
||||
@ -135,20 +140,17 @@ const loadMinterBoardPage = async () => {
|
||||
})
|
||||
|
||||
document.getElementById("time-range-select").addEventListener("change", async () => {
|
||||
// Re-load the cards whenever user chooses a new sort option.
|
||||
await loadCards(minterCardIdentifierPrefix)
|
||||
// Only re-load the cards whenever user presses the refresh button.
|
||||
})
|
||||
|
||||
document.getElementById("sort-select").addEventListener("change", async () => {
|
||||
// Re-load the cards whenever user chooses a new sort option.
|
||||
await loadCards(minterCardIdentifierPrefix)
|
||||
// Only re-load the cards whenever user presses the refresh button.
|
||||
})
|
||||
|
||||
await featureTriggerCheck()
|
||||
await loadCards(minterCardIdentifierPrefix)
|
||||
// Only load the cards whenever user presses the refresh button.
|
||||
}
|
||||
|
||||
|
||||
const extractMinterCardsMinterName = async (cardIdentifier) => {
|
||||
// Ensure the identifier starts with the prefix
|
||||
if ((!cardIdentifier.startsWith(minterCardIdentifierPrefix)) && (!cardIdentifier.startsWith(addRemoveIdentifierPrefix))) {
|
||||
@ -366,7 +368,13 @@ 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>`
|
||||
const counterSpan = document.getElementById("board-card-counter")
|
||||
|
||||
if (counterSpan) {
|
||||
// Clear or show "Loading..."
|
||||
counterSpan.textContent = "(loading...)"
|
||||
}
|
||||
|
||||
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
|
||||
isARBoard = true
|
||||
@ -375,6 +383,9 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
let afterTime = 0
|
||||
const timeRangeSelect = document.getElementById("time-range-select")
|
||||
|
||||
const showExistingCheckbox = document.getElementById("show-existing-checkbox")
|
||||
const showExisting = showExistingCheckbox && showExistingCheckbox.checked
|
||||
|
||||
if (timeRangeSelect) {
|
||||
const days = parseInt(timeRangeSelect.value, 10)
|
||||
if (days > 0) {
|
||||
@ -418,30 +429,15 @@ 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 = []
|
||||
let alreadyMinterCards = []
|
||||
cardsContainer.innerHTML = ''
|
||||
for (const card of finalCards) {
|
||||
for (const card of sortedFinalCards) {
|
||||
try {
|
||||
const skeletonHTML = createSkeletonCardHTML(card.identifier)
|
||||
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
||||
@ -477,8 +473,14 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
} else {
|
||||
const isAlreadyMinter = await verifyMinter(cardDataResponse.creator)
|
||||
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)
|
||||
alreadyMinterCards.push({
|
||||
...card,
|
||||
cardDataResponse,
|
||||
pollPublisherAddress,
|
||||
cardPublisherAddress
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -489,6 +491,12 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
pollPublisherAddress,
|
||||
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) {
|
||||
console.error(`Error preparing card ${card.identifier}`, err)
|
||||
removeSkeleton(card.identifier)
|
||||
@ -531,9 +539,39 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
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) {
|
||||
console.error("Error loading cards:", error)
|
||||
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 skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
|
||||
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. ---------------------------------
|
||||
const postComment = async (cardIdentifier) => {
|
||||
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`)
|
||||
@ -1193,6 +936,7 @@ const toggleComments = async (cardIdentifier) => {
|
||||
const commentButton = document.getElementById(`comment-button-${cardIdentifier}`)
|
||||
|
||||
if (!commentsSection || !commentButton) return
|
||||
|
||||
const count = commentButton.dataset.commentCount
|
||||
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 isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
|
||||
const isBlockPassed = await featureTriggerCheck()
|
||||
@ -1582,13 +1312,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="
|
||||
@ -1607,6 +1338,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
Approve Invite Tx
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
return approvalButtonHtml
|
||||
@ -1679,6 +1411,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
|
||||
</p>
|
||||
${tableHtml}
|
||||
${isSomeTypaAdmin ? `
|
||||
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||
<button
|
||||
style="
|
||||
@ -1697,6 +1430,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
Approve Add-Admin Tx
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
return approvalButtonHtml
|
||||
@ -1709,6 +1443,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
|
||||
</p>
|
||||
${tableHtml}
|
||||
${isSomeTypaAdmin ? `
|
||||
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||
<button
|
||||
style="
|
||||
@ -1727,6 +1462,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
||||
Approve Remove-Admin Tx
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
return approvalButtonHtml
|
||||
@ -1761,9 +1497,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>
|
||||
`
|
||||
@ -1882,28 +1628,38 @@ 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.")
|
||||
}
|
||||
}
|
||||
|
||||
// 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 formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
|
||||
const avatarHtml = await getMinterAvatar(creator)
|
||||
@ -1919,7 +1675,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
|
||||
createModal('links')
|
||||
createModal('poll-details')
|
||||
|
||||
const inviteButtonHtml = await checkAndDisplayInviteButton(adminYes, creator, cardIdentifier)
|
||||
const inviteButtonHtml = isExistingMinter ? "" : await checkAndDisplayInviteButton(adminYes, creator, cardIdentifier)
|
||||
let inviteHtmlAdd = (inviteButtonHtml) ? inviteButtonHtml : ''
|
||||
|
||||
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
|
||||
} else if (userVote === 1) {
|
||||
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) {
|
||||
// If so, override background color & add an "INVITED" label
|
||||
finalBgColor = "black";
|
||||
@ -2009,6 +1768,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>
|
||||
|
@ -1,4 +1,4 @@
|
||||
const Q_MINTERSHIP_VERSION = "1.06"
|
||||
const Q_MINTERSHIP_VERSION = "1.06.1"
|
||||
|
||||
const messageIdentifierPrefix = `mintership-forum-message`
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`
|
||||
@ -78,28 +78,28 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault()
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
await loadForumPage();
|
||||
await loadForumPage()
|
||||
loadRoomContent("general")
|
||||
startPollingForNewMessages()
|
||||
createScrollToTopButton()
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]')
|
||||
minterBoardLinks.forEach(link => {
|
||||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
event.preventDefault()
|
||||
if (typeof loadMinterBoardPage === "undefined") {
|
||||
console.log("loadMinterBoardPage not found, loading script dynamically...")
|
||||
await loadScript("./assets/js/MinterBoard.js")
|
||||
}
|
||||
await loadMinterBoardPage()
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -107,15 +107,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
addRemoveAdminLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault()
|
||||
// Possibly require user to login if not logged
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
if (typeof loadMinterBoardPage === "undefined") {
|
||||
console.log("loadMinterBoardPage not found, loading script dynamically...")
|
||||
await loadScript("./assets/js/MinterBoard.js")
|
||||
}
|
||||
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-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;">
|
||||
${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>
|
||||
</div>
|
||||
</div>
|
||||
@ -468,6 +470,7 @@ let selectedImages = []
|
||||
let selectedFiles = []
|
||||
let multiResource = []
|
||||
let attachmentIdentifiers = []
|
||||
let editMessageIdentifier = null
|
||||
|
||||
// Set up file input handling
|
||||
const setupFileInputs = (room) => {
|
||||
@ -558,9 +561,14 @@ const processSelectedImages = async (selectedImages, multiResource, room) => {
|
||||
|
||||
// Handle send message
|
||||
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
|
||||
const messageIdentifier = room === "admins"
|
||||
let messageIdentifier
|
||||
if (editMessageIdentifier) {
|
||||
messageIdentifier = editMessageIdentifier
|
||||
} else {
|
||||
messageIdentifier = room === "admins"
|
||||
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
|
||||
: `${messageIdentifierPrefix}-${room}-${randomID()}`
|
||||
}
|
||||
|
||||
try {
|
||||
// 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() {
|
||||
// Clear the file input elements and preview container
|
||||
@ -662,6 +725,7 @@ function clearInputs() {
|
||||
attachmentIdentifiers = []
|
||||
selectedImages = []
|
||||
selectedFiles = []
|
||||
editMessageIdentifier = null
|
||||
|
||||
// Remove the reply container
|
||||
const replyContainer = document.querySelector('.reply-container')
|
||||
@ -760,6 +824,8 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
}
|
||||
|
||||
handleReplyLogic(fetchMessages)
|
||||
handleDeleteLogic(fetchMessages, room)
|
||||
handleEditLogic(fetchMessages, room)
|
||||
|
||||
await updatePaginationControls(room, limit)
|
||||
} catch (error) {
|
||||
@ -979,6 +1045,24 @@ 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 = ''
|
||||
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 `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
@ -995,8 +1079,11 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
|
||||
<div class="attachments-gallery">
|
||||
${attachmentHtml}
|
||||
</div>
|
||||
<div class="message-actions">
|
||||
${deleteButtonHtml}${editButtonHtml}
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</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) => {
|
||||
replyToMessageIdentifier = repliedMessage.identifier
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -32,11 +32,6 @@
|
||||
|
||||
</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">
|
||||
@ -201,13 +196,13 @@
|
||||
<div class="col-12 col-lg-7 card">
|
||||
<div class="title-wrapper">
|
||||
<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 class="col-12 col-lg-5 card">
|
||||
<div class="text-wrapper">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user