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
13 changed files with 1142 additions and 1445 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.vscode
/.sync*

BIN
.sync_b48a6eedae0b.db Normal file

Binary file not shown.

BIN
.sync_b48a6eedae0b.db-wal Normal file

Binary file not shown.

View File

@ -1,49 +0,0 @@
### Q-Mintership-Alpha
Q-Mintership-Alpha is the currently utilized version of the Q-Mintership app published on qortal://APP/Q-Mintership.
As of Feb 27 2025 Q-Mintership-Alpha is still the published and utilized version of the app.
#### Q-Mintership's 'MinterBoard'
The MinterBoard of Q-Mintership, is the primary location for users to publish 'cards' with information about themselves, links to things they have published on QDN, etc... and obtain minting rights from the Minter Admins.
- Cards are created by any non-minter (either previous minter no longer in the group, or new accounts without minting rights).
- Existing community members, existing minters, and Minter Admins, can vote/comment on the cards.
- Once a card has obtained the minimum required number of admin votes (40% of the Minter Admin count), the card will then display additional features to those that have the rights to see them. (Minter Admins, and 'Forum Admins', however 'Forum Admins' cannot actually make use of the functionality, they are only able to view it for development purposes.)
- The Minter Admins then initiate a PENDING GROUP_INVITE transaction.
- The Minter Admins are then able to issue GROUP_APPROVAL transactions to approve the invite.
- Once the required number of GROUP_APPROVAL transactions have been created, the GROUP_INVITE is no longer pending, and is active.
- The would-be minter that published a card, can then see a new 'JOIN_GROUP' button on their card upon returning to the MinterBoard.
- The user will then JOIN_GROUP to the MINTER group, ID 694. Thus allowing the ability to mint.
- Upon joining the MINTER group, the user will then have the ability to create a MINTING KEY (the same way as it was created prior to the Mintership concept, however now it no longer requires users to be level 1, only requirement now is to be part of the MINTER group)
- User then assigns their key to their node, and starts minting.
#### Q-Mintership 'AdminBoard'
- The AdminBoard is a separate board, encrypted to admins only, meant to be utilized for private decision-making between the admins.
- The AdminBoard was also adapted to allow REMOVAL of MINTER group members, via GROUP_APPROVAL from the Minter Admins.
- The REMOVAL functionality, at the moment, is private. Meaning only the admins that have access to the AdminBoard, can see the data. This will be changed in the future, and a new location where the data will be able to be seen publicly, will be created.
#### Q-Mintership Forum
- The Forum portion of Q-Mintership is a public (and private) forum, allowing communications to take place in the fashion of long-term forum messages, replies, etc.
- Publishing of images with previews, and various 'attachments' with data is also possible on the forum.
- The forum has two public rooms by default, and one private room. General and Minter rooms are public, and Admin room is private.
- The forum will be getting extensive updates in the future, and the Minter room will be made a publicly VIEWABLE room, but only able to be published to by MINTERS.
#### Q-Mintership MAM Board
- The MAM Board (or ARBoard in the code) is built to allow the adding and removal of Minter Admins from the MINTER group. Proposals for additions or removals of certain accounts from and to the Minter takes place here.
- This board also displays a list of the current Minter Admins, and has the ability to propose a removal of that user with a propose removal button.
#### Additional
Many additional features and functions are planned for Q-Mintership, and increased performance and more will be added as time goes on.
Longer-term the plan is to re-write the app into React+TypeScript, which will make it MUCH faster and able to accommodate much more, with a component-based development style similar to that of the other React-based applications on Qortal (Q-Tube, Q-Blog, Q-Mail, etc.)
A fully featured data viewer and explorer function will be built into Q-Mintership in the future, along with a comprehensive notification system, and more.

View File

@ -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 {
@ -584,7 +614,7 @@ body {
border-radius: 5px;
padding: 20px;
margin: 20px auto; /* center horizontally */
/* max-width: 600px; */
max-width: 600px; /* limit width */
color: #ddd; /* text color */
text-align: center;
align-items: center;
@ -596,7 +626,7 @@ body {
background-color:#000000;
width: 90%;
font-size: 1.8rem;
color: #fff3f3;
color: #4d0000;
text-align: center;
align-items: center;
/* you could style the list items or bullet if you like */
@ -616,17 +646,7 @@ body {
background-color: #14161a;
border: 1px solid #8caeb0;
border-radius: 4px;
color: #f19c9c;
}
.invite-form input.invite-input {
padding: 1rem;
font-size: 2rem;
line-height: 2;
background-color: #14161a;
border: 1px solid #8caeb0;
border-radius: 4px;
color: #dddddd;
color: #5c0101;
}
.publish-card-button {
@ -717,42 +737,30 @@ body {
background-color: #281e1e;
}
.approve-invite-list-button {
background-color: rgba(32, 88, 34, 0.554);
color: #fff;
border: none;
border-radius: 1vw;
padding: 1vh,2vh;
cursor: pointer;
font-size: 1.1rem;
transition: background-color 0.2s ease;
/* Responsive design */
@media (max-width: 768px) {
.publish-card-view {
width: 90%;
padding: 2vh;
}
.publish-card-button {
font-size: 1.8vh;
padding: 1.5vh;
}
.publish-card-form button {
font-size: 1.8vh;
padding: 1.2vh;
}
}
.approve-invite-list-button:hover {
background-color: rgba(34, 186, 47, 0.84); /* a darker variant */
}
.invite-approvals strong {
display: inline-block;
}
.invite-item {
margin-bottom: 0.5em;
background-color: rgba(31, 31, 31, 0.595);
border: 1px solid #444;
border-radius: 6px;
color: #ccc;
padding: 1rem;
}
/* Top row: use flex for horizontal arrangement */
.invite-top-row {
display: flex;
background-color:#173c52ae;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: center;
.refresh-cards-button {
border-color: white;
border-radius: 1.5vh;
background-color: black;
color: white;
}
/* Responsive Design */
@ -776,14 +784,8 @@ body {
.refresh-cards-button {
border-color: white;
border-radius: 1.5vh;
background-color: rgba(0, 0, 0, 0.089);
background-color: black;
color: white;
font-size: 0.9rem;
}
.refresh-cards-button:hover {
background-color: rgba(35, 129, 136, 0.137);
color: rgba(90, 201, 221, 0.793);
}
/* Two cards per row on medium screens */

View File

@ -59,14 +59,19 @@ 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">All Creation Dates</option>
<option value="1">Last 1 Day</option>
<option value="7">Last 7 Days</option>
<option value="30">...Within 30 Days</option>
<option value="45" selected>Published Within Last 45 Days</option>
<option value="60">...Within 60 Days</option>
<option value="90">...Within 90 Days</option>
<option value="0">Show All</option>
<option value="1">Last 1 day</option>
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
</div>
<div id="cards-container" class="cards-container" style="margin-top: 1rem"">
@ -97,7 +102,6 @@ const loadAddRemoveAdminPage = async () => {
document.getElementById("refresh-cards-button").addEventListener("click", async () => {
const cardsContainer = document.getElementById("cards-container")
cardsContainer.innerHTML = "<p>Refreshing cards...</p>"
await initializeCachedGroups()
await loadCards(addRemoveIdentifierPrefix)
})
@ -120,20 +124,18 @@ const loadAddRemoveAdminPage = async () => {
linksContainer.appendChild(newLinkInput)
})
const timeRangeSelectCheckbox = document.getElementById('time-range-select')
if (timeRangeSelectCheckbox) {
timeRangeSelectCheckbox.addEventListener('change', async (event) => {
await loadCards(addRemoveIdentifierPrefix)
})
}
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
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()
}
@ -143,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"
@ -169,63 +184,51 @@ const fetchAllARTxData = async () => {
txGroupId: 694,
})
const { finalAddTxs, pendingAddTxs, expiredAddTxs } = partitionAddTransactions(allAddTxs)
const { finalRemTxs, pendingRemTxs, expiredRemTxs } = partitionRemoveTransactions(allRemTxs)
const { finalAddTxs, pendingAddTxs } = partitionAddTransactions(allAddTxs)
const { finalRemTxs, pendingRemTxs } = partitionRemoveTransactions(allRemTxs)
// We are going to keep all transactions in order to filter more accurately for display purposes.
console.log('Final addAdminTxs:', finalAddTxs)
console.log('Pending addAdminTxs:', pendingAddTxs)
console.log('expired addAdminTxs', expiredAddTxs)
console.log('Final remAdminTxs:', finalRemTxs)
console.log('Pending remAdminTxs:', pendingRemTxs)
console.log('expired remAdminTxs', expiredRemTxs)
console.log('Final addAdminTxs:', finalAddTxs);
console.log('Pending addAdminTxs:', pendingAddTxs);
console.log('Final remAdminTxs:', finalRemTxs);
console.log('Pending remAdminTxs:', pendingRemTxs);
return {
finalAddTxs,
pendingAddTxs,
expiredAddTxs,
finalRemTxs,
pendingRemTxs,
expiredRemTxs
}
}
const partitionAddTransactions = (rawTransactions) => {
const finalAddTxs = []
const pendingAddTxs = []
const expiredAddTxs = []
const finalAddTxs = []
const pendingAddTxs = []
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') {
pendingAddTxs.push(tx)
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') {
pendingAddTxs.push(tx)
} else {
finalAddTxs.push(tx)
}
}
else if (tx.approvalStatus === 'EXPIRED'){
expiredAddTxs.push(tx)
} else {
finalAddTxs.push(tx)
}
}
return { finalAddTxs, pendingAddTxs, expiredAddTxs };
return { finalAddTxs, pendingAddTxs };
}
const partitionRemoveTransactions = (rawTransactions) => {
const finalRemTxs = []
const pendingRemTxs = []
const expiredRemTxs = []
const finalRemTxs = []
const pendingRemTxs = []
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') {
for (const tx of rawTransactions) {
if (tx.approvalStatus === 'PENDING') {
pendingRemTxs.push(tx)
}
else if (tx.approvalStatus === 'EXPIRED'){
expiredRemTxs.push(tx)
} else {
} else {
finalRemTxs.push(tx)
}
}
}
}
return { finalRemTxs, pendingRemTxs, expiredRemTxs }
return { finalRemTxs, pendingRemTxs }
}
@ -237,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) {
@ -257,16 +282,8 @@ const displayExistingMinterAdmins = async () => {
</tr>
`
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>
@ -274,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>
@ -284,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;">
@ -296,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 =
@ -736,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) {
@ -842,9 +910,9 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
actionsHtml = actionsHtmlCheck
const { finalAddTxs, pendingAddTxs, expiredAddTxs, finalRemTxs, pendingRemTxs, expiredRemTxs } = await fetchAllARTxData()
const { finalAddTxs, pendingAddTxs, finalRemTxs, pendingRemTxs } = await fetchAllARTxData()
const userConfirmedAdd = finalAddTxs.some(
const confirmedAdd = finalAddTxs.some(
(tx) => tx.groupId === 694 && tx.member === accountAddress
)
const userPendingAdd = pendingAddTxs.some(
@ -856,88 +924,31 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
const userPendingRemove = pendingRemTxs.some(
(tx) => tx.groupId === 694 && tx.admin === accountAddress
)
const userExpiredAdd = expiredAddTxs.some(
(tx) => tx.groupId === 694 && tx.member === accountAddress
)
const userExpiredRem = expiredRemTxs.some(
(tx) => tx.groupId === 694 && tx.admin === accountAddress
)
const noExpired = (!userExpiredAdd && !userExpiredRem)
// If user is definitely admin (finalAdd) and not pending removal
if (userConfirmedAdd && !userPendingRemove && !userPendingAdd && noExpired && existingAdmin && promotionCard) {
if (confirmedAdd && !userPendingRemove && existingAdmin) {
console.warn(`account was already admin, final. no add/remove pending.`);
cardColorCode = 'rgb(3, 11, 24)'
altText = `<h4 style="color:rgb(89, 191, 204); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`;
altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`;
actionsHtml = ''
}
if (userConfirmedAdd && !userPendingRemove && userExpiredRem && existingAdmin && promotionCard) {
console.warn(`Account has previously had a removal attempt expire`);
cardColorCode = 'rgb(33, 40, 11)'
altText = `<h4 style="color:rgb(136, 114, 146); margin-bottom: 0.5em;">PROMOTED, (+Previous Failed Demotion).</h4>`;
actionsHtml = ''
}
if (userConfirmedAdd && !userPendingRemove && userExpiredAdd && existingAdmin && promotionCard) {
console.warn(`Account has previously had a removal attempt expire`);
cardColorCode = 'rgb(14, 3, 24)'
altText = `<h4 style="color:rgb(114, 117, 146); margin-bottom: 0.5em;">PROMOTED, (+Previous Failed Promotion).</h4>`;
actionsHtml = ''
}
if (userConfirmedAdd && userPendingRemove && existingAdmin && noExpired && !promotionCard) {
if (confirmedAdd && userPendingRemove && existingAdmin) {
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
altText = `<h4 style="color:rgb(85, 34, 34); margin-bottom: 0.5em;">Pending REMOVAL in progress...</h4>`;
}
if (userConfirmedAdd && userPendingRemove && existingAdmin && userExpiredAdd && !promotionCard) {
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
altText = `<h4 style="color:rgb(85, 74, 34); margin-bottom: 0.5em;">Pending REMOVAL in progress... (+Previous Failed Promotion)</h4>`;
}
if (userConfirmedAdd && userPendingRemove && existingAdmin && userExpiredRem && !promotionCard) {
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
altText = `<h4 style="color:rgb(198, 26, 13); margin-bottom: 0.5em;">Pending REMOVAL in progress... (+Previous Failed Demotion)</h4>`;
}
// If user has a final "remove" and no pending additions or removals and no expired transactions
if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && noExpired && !promotionCard) {
console.warn(`account was demoted, final. no add pending, existingMinter, no expired add/remove.`);
// If user has a final "remove" and no pending additions or removals
if (confirmedRemove && !userPendingAdd && existingMinter) {
console.warn(`account was demoted, final. no add pending, existingMinter.`);
cardColorCode = 'rgb(29, 4, 6)'
altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
actionsHtml = ''
}
if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && userExpiredRem && !promotionCard) {
console.warn(`account was demoted, final. no add pending, existingMinter, no expired add/remove.`);
cardColorCode = 'rgb(29, 4, 6)'
altText = `<h4 style="color:rgb(170, 32, 48); margin-bottom: 0.5em;">DEMOTED (+Previous Failed Demotion)</h4>`
actionsHtml = ''
}
if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && userExpiredAdd && !promotionCard) {
console.warn(`account was demoted, final. no add pending, existingMinter, no expired add/remove.`);
cardColorCode = 'rgb(29, 4, 6)'
altText = `<h4 style="color:rgb(119, 170, 32); margin-bottom: 0.5em;">DEMOTED (+Previous Failed Promotion)</h4>`
actionsHtml = ''
}
// If user has both final remove and pending add, do something else
if (confirmedRemove && userPendingAdd && existingMinter && noExpired && promotionCard) {
if (confirmedRemove && userPendingAdd && existingMinter) {
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
altText = `<h4 style="color:rgb(73, 68, 24); margin-bottom: 0.5em;">Previously DEMOTED from ADMIN, attempted re-add in progress...</h4>`
}
if (confirmedRemove && userPendingAdd && existingMinter && userExpiredAdd && promotionCard) {
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
altText = `<h4 style="color:rgb(73, 68, 24); margin-bottom: 0.5em;">Previously DEMOTED from ADMIN, attempted re-add in progress...(+Previous Failed Promotion)</h4>`
}
if (confirmedRemove && userPendingAdd && existingMinter && userExpiredRem && promotionCard) {
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
altText = `<h4 style="color:rgb(73, 68, 24); margin-bottom: 0.5em;">Previously DEMOTED from ADMIN, attempted re-add in progress...(+Previous Failed Demotion)</h4>`
// Possibly show "DEMOTED but re-add in progress" or something
}
} else if ( verifiedName && illegalDuplicate) {
@ -999,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>

View File

@ -72,7 +72,8 @@ const loadAdminBoardPage = async () => {
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">AdminBoard</h1>
<p style="font-size: 0.95rem; color:rgba(255, 255, 255, 0.53)"> The Admin Board was meant to be utilized for DECISIONS regarding Minters or would-be Minters, and is encrypted to the Admins so that the data for the DECISIONS remains private. However, it later became the location to REMOVE minters as well. This, not being the original intended purpose has become problematic, as the removal data SHOULD be public. In the future, this data WILL be made public. The Admin Board will continue to be utilized for decision-making, but will NOT be a place for hidden removal data only. </p>
<p style="font-size: 1.25em;"> The Admin Board is an encrypted card publishing board to keep track of minter data for the Minter Admins. Any Admin may publish a card, and related data, make comments on existing cards, and vote on existing card data in support or not of the name on the card. It is essentially a 'project management' tool to assist the Minter Admins in keeping track of the data related to minters they are adding/removing from the minter group. </p>
<p> More functionality will be added over time. One of the first features will be the ability to output the existing card data 'decisions', to a json formatted list in order to allow crowetic to run his script easily until the final Mintership proposal changes are completed, and the MINTER group is transferred to 'null'.</p>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Encrypted Card</button>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button>
<select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:rgb(70, 106, 105); background-color: black;">
@ -83,14 +84,12 @@ const loadAdminBoardPage = async () => {
<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">All Creation Dates</option>
<option value="1">Last 1 Day</option>
<option value="7">Last 7 Days</option>
<option value="30">...Within 30 Days</option>
<option value="45" selected>Published Within Last 45 Days</option>
<option value="60">...Within 60 Days</option>
<option value="90">...Within 90 Days</option>
</select>
<option value="0">Show All</option>
<option value="1">Last 1 day</option>
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
<div class="show-card-checkbox" style="margin-top: 1em;">
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
<label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label>
@ -381,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
@ -1079,7 +966,7 @@ const createRemoveButtonHtml = (name, cardIdentifier) => {
const handleKickMinter = async (minterName) => {
try {
let isAddress = await getAddressInfo(minterName)
isAddress = await getAddressInfo(minterName)
// Optional block check
let txGroupId = 0
@ -1092,7 +979,7 @@ const handleKickMinter = async (minterName) => {
// Get the minter address from name info
let minterAddress
if (!isAddress.address || !isAddress.address != minterName){
if (!isAddress){
const minterNameInfo = await getNameInfo(minterName)
minterAddress = minterNameInfo?.owner
} else {
@ -1108,7 +995,7 @@ const handleKickMinter = async (minterName) => {
const reason = 'Kicked by Minter Admins'
const fee = 0.01
const rawKickTransaction = await createGroupKickTransaction(adminPublicKey, 694, minterAddress, reason, txGroupId, fee)
const rawKickTransaction = await createGroupKickTransaction(minterAddress, adminPublicKey, 694, minterAddress, reason, txGroupId, fee)
const signedKickTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
@ -1139,7 +1026,7 @@ const handleKickMinter = async (minterName) => {
}
const handleBanMinter = async (minterName) => {
let isAddress = await getAddressInfo(minterName)
isAddress = await getAddressInfo(minterName)
try {
let txGroupId = 0
// const { height: currentHeight } = await getLatestBlockInfo()
@ -1152,9 +1039,9 @@ const handleBanMinter = async (minterName) => {
txGroupId = 694
}
let minterAddress
if (!isAddress.address || !isAddress.address != minterName){
if (!isAddress) {
const minterNameInfo = await getNameInfo(minterName)
minterAddress = minterNameInfo?.owner
const minterAddress = minterNameInfo?.owner
} else {
minterAddress = minterName
}
@ -1197,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.")
}
}
@ -1304,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(
@ -1399,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>

View File

@ -18,23 +18,22 @@ const loadMinterAdminToolsPage = async () => {
mainContent.innerHTML = `
<div class="tools-main mbr-parallax-background cid-ttRnlSkg2R">
<div class="tools-header" style="color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 10px;">
<div><h1 style="font-size: 50px; margin: 0;">Admin Tools</h1></div>
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; 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;">
<span>${userState.accountName || 'Guest'}'s Admin Tools</span>
<span>${userState.accountName || 'Guest'}</span>
</div>
<div><h2>Welcome to Admin Tools</h2></div>
<div>
<p style="color:rgba(80, 9, 9, 0.63)"></p>
<p style="color:rgb(82, 114, 145)"> The approve feature allows invite by name, and shows ALL existing approvals ongoing, whether initiated on this page manually or not. Allowing for easy displaying and approving without loading the MinterBoard and scrolling through cards. </p>
<p style="font-size: 0.85rem"> This is NOT a substitute for the AdminBoard, as obviously no data regarding the account is published here. However, if you have already read the data there, and wish to see it easily in one place, here, that is fine. It can also obviously be utilized to manually invite users that require such actions to be taken, however, this action as well should be extremely limited in usage, and not leveraged without extensive provided rationale.</p>
<p>On this page you will find admin functionality for the Q-Mintership App. Including the 'blockList' for blocking comments from certain names, and manual creation of invite transactions.</p>
<p>More features will be added as time goes on. This is the start of the functionality here.</p>
</div>
</div>
<div id="tools-submenu" class="tools-submenu">
<div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;">
<button id="toggle-blocklist-button" style="background-color:rgba(80, 9, 9, 0.63)" class="publish-card-button">Add/Remove blockedUsers</button>
<button id="create-group-invite" class="publish-card-button" style="background-color:rgb(82, 114, 145)">Create and Display Pending Group Invites</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>
<div id="tools-window" class="tools-window" style="margin-top: 2em;">
@ -57,30 +56,6 @@ const loadMinterAdminToolsPage = async () => {
</div>
</div>
<div id="invite-container" class="invite-form" style="display: none; flex-direction: column; padding: 0.75em; align-items: center; justify-content: center;">
<!-- Existing pending invites display -->
<div id="pending-invites-display" class="pending-invites-display" style="margin-bottom: 1em;">
<!-- We will fill this dynamically with a list/table of pending invites -->
</div>
<!-- Input for name/address -->
<h3 style="margin-top: 0;">Manual Group Invite</h3>
<input
type="text"
id="invite-input"
class="invite-input"
placeholder="Enter name or address to invite"
style="margin-bottom: 1em;"
/>
<!-- Button to create the invite transaction -->
<div class="invite-button-container publish-card-form">
<button id="invite-user-button" class="publish-card-button">Invite User</button>
</div>
</div>
</div>
</div>
</div>
@ -88,10 +63,10 @@ const loadMinterAdminToolsPage = async () => {
document.body.appendChild(mainContent)
await addToolsPageEventListeners()
addToolsPageEventListeners()
}
const addToolsPageEventListeners= async () => {
function addToolsPageEventListeners() {
document.getElementById("toggle-blocklist-button").addEventListener("click", async () => {
const container = document.getElementById("blocklist-container")
// toggle show/hide
@ -141,32 +116,6 @@ const addToolsPageEventListeners= async () => {
alert(`"${nameToRemove}" removed from the block list (if it was present).`)
})
document.getElementById("invite-user-button").addEventListener("click", async () => {
const inviteInput = document.getElementById("invite-input")
const nameOrAddress = inviteInput.value.trim()
if (!nameOrAddress) return
try {
// We'll call some function handleManualInvite(nameOrAddress)
await handleManualInvite(nameOrAddress)
inviteInput.value = ""
} catch (err) {
console.error("Error inviting user:", err)
alert("Failed to invite user.")
}
})
document.getElementById("create-group-invite").addEventListener("click", async () => {
const inviteContainer = document.getElementById("invite-container")
// Toggle display
inviteContainer.style.display = (inviteContainer.style.display === "none" ? "flex" : "none")
// If showing, load the pending invites
if (inviteContainer.style.display === "flex") {
const pendingInvites = await fetchPendingInvites()
await displayPendingInviteDetails(pendingInvites)
}
})
}
const displayBlockList = (blockedNames) => {
@ -182,139 +131,4 @@ const displayBlockList = (blockedNames) => {
`
}
const fetchPendingInvites = async () => {
try {
const { finalInviteTxs, pendingInviteTxs } = await fetchAllInviteTransactions()
return pendingInviteTxs
} catch (err) {
console.error("Error fetching pending invites:", err)
return []
}
}
const handleManualInvite = async (nameOrAddress) => {
const addressInfo = await getAddressInfo(nameOrAddress)
let address = addressInfo.address
if (addressInfo && address) {
console.log(`address is ${address}`)
} else {
// it might be a Qortal name => getNameInfo
const nameData = await getNameInfo(nameOrAddress)
if (!nameData || !nameData.owner) {
throw new Error(`Cannot find valid address for ${nameOrAddress}`)
}
address = nameData.owner
}
const adminPublicKey = await getPublicKeyByName(userState.accountName)
const timeToLive = 864000 // e.g. 10 days in seconds
const fee = 0.01
let txGroupId = 694
// build the raw invite transaction
const rawInviteTransaction = await createGroupInviteTransaction(
address,
adminPublicKey,
694,
address,
timeToLive,
txGroupId,
fee
)
// sign
const signedTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
unsignedBytes: rawInviteTransaction
})
if (!signedTransaction) {
throw new Error("SIGN_TRANSACTION returned null. Possibly user canceled or an older UI?")
}
// process
const processResponse = await processTransaction(signedTransaction)
if (!processResponse) {
throw new Error("Failed to process transaction. Possibly canceled or error from Qortal Core.")
}
alert(`Invite transaction submitted for ${nameOrAddress}. Wait for confirmation.`)
}
const displayPendingInviteDetails = async (pendingInvites) => {
const invitesContainer = document.getElementById('pending-invites-display')
if (!pendingInvites || pendingInvites.length === 0) {
invitesContainer.innerHTML = "<p>No pending invites found.</p>"
return
}
let html = `<h4>Current Pending Invites:</h4><div class="pending-invites-list">`
for (const inviteTx of pendingInvites) {
const inviteeAddress = inviteTx.invitee
const dateStr = new Date(inviteTx.timestamp).toLocaleString()
let inviteeName = ""
const txSig = inviteTx.signature
const creatorName = await getNameFromAddress(inviteTx.creatorAddress)
if (!creatorName) {
creatorName = inviteTx.creatorAddress
}
try {
// fetch the name from address, if it fails we keep it blank or fallback to the address
inviteeName = await getNameFromAddress(inviteeAddress)
if (!inviteeName || inviteeName === inviteeAddress) {
inviteeName = inviteeAddress // fallback
}
} catch (err) {
inviteeName = inviteeAddress // fallback if getName fails
}
const approvalSearchResults = await searchTransactions({
txTypes: ['GROUP_APPROVAL'],
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: false,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 0
})
const approvals = approvalSearchResults.filter(
(approvalTx) => approvalTx.pendingSignature === txSig
)
const { tableHtml, approvalCount = approvals.length } = await buildApprovalTableHtml(approvals, getNameFromAddress)
const finalTable = approvals.length > 0 ? tableHtml : "<p>No Approvals Found</p>"
html += `
<div class="invite-item">
<div class="invite-top-row">
<span><strong>Invite Tx</strong>:<p style="color:lightblue"> ${inviteTx.signature.slice(0, 8)}...</p></span>
<span> <strong>Invitee</strong>:<p style="color:lightblue"> ${inviteeName}</p></span>
<span> <strong>Date</strong>:<p style="color:lightblue"> ${dateStr}</p></span>
<span> <strong>CreatorName</strong>:<p style="color:lightblue"> ${creatorName}</p></span>
<span> <strong>Total Approvals</strong>:<p style="color:lightblue"> ${approvalCount}</p></span>
</div>
<!-- Next line for approvals -->
<div class="invite-approvals">
<strong>Existing Approvals:</strong>
${finalTable}
</div>
<button
class="approve-invite-list-button"
onclick="handleGroupApproval('${inviteTx.signature}')"
>
Approve Invite
</button>
</div>
`
}
html += "</div>"
invitesContainer.innerHTML = html
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
const Q_MINTERSHIP_VERSION = "1.22"
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;">
<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>
</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"
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
: `${messageIdentifierPrefix}-${room}-${randomID()}`
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,7 +1079,10 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
<div class="attachments-gallery">
${attachmentHtml}
</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
<div class="message-actions">
${deleteButtonHtml}${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

View File

@ -6,11 +6,6 @@ let baseUrl = ''
let isOutsideOfUiDevelopment = false
let nullAddress = 'QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG'
// Caching to improve performance
const nameInfoCache = new Map(); // name -> nameInfo
const addressInfoCache = new Map(); // address -> addressInfo
const pollResultsCache = new Map(); // pollName -> pollResults
if (typeof qortalRequest === 'function') {
console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.')
isOutsideOfUiDevelopment = false
@ -58,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
}
@ -228,20 +223,7 @@ const getUserAddress = async () => {
}
}
const getAddressInfoCached = async (address) => {
if (addressInfoCache.has(address)) return addressInfoCache.get(address)
const result = await getAddressInfo(address)
addressInfoCache.set(address, result)
return result
}
const getAddressInfo = async (address) => {
const qortalAddressPattern = /^Q[A-Za-z0-9]{33}$/ // Q + 33 almum = 34 total length
if (!qortalAddressPattern.test(address)) {
console.warn(`Not a valid Qortal address format, returning same thing that was passed to not break other functions: ${address}`)
return address
}
try {
const response = await fetch (`${baseUrl}/addresses/${address}`, {
headers: { 'Accept': 'application/json' },
@ -266,19 +248,6 @@ const getAddressInfo = async (address) => {
}
}
const nameToAddressCache = new Map()
const fetchOwnerAddressFromNameCached = async (name) => {
if (nameToAddressCache.has(name)) {
return nameToAddressCache.get(name)
}
const address = await fetchOwnerAddressFromName(name)
nameToAddressCache.set(name, address)
return address
}
const fetchOwnerAddressFromName = async (name) => {
console.log('fetchOwnerAddressFromName called')
console.log('name:', name)
@ -358,15 +327,6 @@ const verifyAddressIsAdmin = async (address) => {
}
}
const getNameInfoCached = async (name) => {
if (nameInfoCache.has(name)) {
return nameInfoCache.get(name)
}
const result = await getNameInfo(name)
nameInfoCache.set(name, result)
return result
}
const getNameInfo = async (name) => {
console.log('getNameInfo called')
console.log('name:', name)
@ -826,20 +786,18 @@ const searchSimple = async (service, identifier, name, limit=1500, offset=0, roo
if (name && !identifier && !room) {
console.log('name only searchSimple', name)
urlSuffix = `service=${service}&name=${name}&limit=${limit}&prefix=true&reverse=${reverse}&after=${after}`
console.log(`urlSuffix used: ${urlSuffix}`)
urlSuffix = `service=${service}&name=${name}&limit=${limit}&prefix=true&reverse=${reverse}`
} else if (!name && identifier && !room) {
console.log('identifier only searchSimple', identifier)
urlSuffix = `service=${service}&identifier=${identifier}&limit=${limit}&prefix=true&reverse=${reverse}&after=${after}`
console.log(`urlSuffix used: ${urlSuffix}`)
urlSuffix = `service=${service}&identifier=${identifier}&limit=${limit}&prefix=true&reverse=${reverse}`
} else if (!name && !identifier && !room) {
console.error(`name: ${name} AND identifier: ${identifier} not passed. Must include at least one...`)
return null
} else {
console.log(`final searchSimple params = service: '${service}', identifier: '${identifier}', name: '${name}', limit: '${limit}', offset: '${offset}', room: '${room}', reverse: '${reverse}', after: ${after}`)
console.log(`final searchSimple params = service: '${service}', identifier: '${identifier}', name: '${name}', limit: '${limit}', offset: '${offset}', room: '${room}', reverse: '${reverse}'`)
}
const response = await fetch(`${baseUrl}/arbitrary/resources/searchsimple?${urlSuffix}`, {
@ -1273,20 +1231,6 @@ const getProductDetails = async (service, name, identifier) => {
// Qortal poll-related calls ----------------------------------------------------------------------
const pollOwnerAddrCache = new Map()
const getPollOwnerAddressCached = async (pollName) => {
if (pollOwnerAddrCache.has(pollName)) {
return pollOwnerAddrCache.get(pollName)
}
const ownerAddress = await getPollOwnerAddress(pollName)
// Store in cache
pollOwnerAddrCache.set(pollName, ownerAddress)
return ownerAddress
}
const getPollOwnerAddress = async (pollName) => {
try {
const response = await fetch(`${baseUrl}/polls/${pollName}`, {
@ -1315,15 +1259,6 @@ const getPollPublisherPublicKey = async (pollName) => {
}
}
const fetchPollResultsCached = async (pollName) => {
if (pollResultsCache.has(pollName)) {
return pollResultsCache.get(pollName)
}
const result = await fetchPollResults(pollName)
pollResultsCache.set(pollName, result)
return result
}
const fetchPollResults = async (pollName) => {
try {
const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {
@ -1411,7 +1346,7 @@ const processTransaction = async (signedTransaction) => {
// Create a group invite transaction. This will utilize a default timeToLive (which is how long the tx will be alive, not the time until it IS live...) of 10 days in seconds, as the legacy UI has a bug that doesn't display invites older than 10 days.
// We will also default to the MINTER group for groupId, AFTER the GROUP_APPROVAL changes, the txGroupId will need to be set for tx that require approval.
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive=0, txGroupId, fee) => {
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive, txGroupId, fee) => {
try {
// Fetch account reference correctly
@ -1460,16 +1395,16 @@ const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, gr
}
}
const createGroupKickTransaction = async (adminPublicKey, groupId=694, member, reason='Kicked by admins', txGroupId=694, fee=0.01) => {
const createGroupKickTransaction = async (recipientAddress, adminPublicKey, groupId=694, member, reason='Kicked by admins', txGroupId, fee) => {
try {
// Fetch account reference correctly
const accountInfo = await getAddressInfo(member)
const accountInfo = await getAddressInfo(recipientAddress)
const accountReference = accountInfo.reference
// Validate inputs before making the request
if (!adminPublicKey || !accountReference || !member) {
throw new Error("Missing required parameters for group kick transaction.")
if (!adminPublicKey || !accountReference || !recipientAddress) {
throw new Error("Missing required parameters for group invite transaction.")
}
const payload = {
@ -1477,10 +1412,11 @@ const createGroupKickTransaction = async (adminPublicKey, groupId=694, member, r
reference: accountReference,
fee,
txGroupId,
recipient: null,
adminPublicKey,
groupId,
member,
reason
groupId: groupId,
member: member || recipientAddress,
reason: reason
}
console.log("Sending GROUP_KICK transaction payload:", payload)

View File

@ -196,8 +196,8 @@ const fetchAllInviteTransactions = async () => {
const { finalTx: finalInviteTxs, pendingTx: pendingInviteTxs } = partitionTransactions(allInviteTx)
console.log('Final InviteTxs:', finalInviteTxs)
console.log('Pending InviteTxs:', pendingInviteTxs)
console.log('Final kickTxs:', finalInviteTxs)
console.log('Pending kickTxs:', pendingInviteTxs)
return {
finalInviteTxs,
@ -205,16 +205,417 @@ const fetchAllInviteTransactions = async () => {
}
}
const findPendingApprovalsForTxSignature = async (txSignature, txType='GROUP_APPROVAL', limit=0, offset=0) => {
const pendingTxs = await searchPendingTransactions(limit, offset)
// Filter only the relevant GROUP_APPROVAL TX referencing txSignature
const approvals = pendingTxs.filter(tx =>
tx.type === txType && tx.pendingSignature === txSignature
)
console.log(`approvals found:`,approvals)
return approvals
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>
<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">
@ -141,7 +136,7 @@
<div class="row">
<div class="col-12 col-lg-4">
<div class="card-wrapper" style="justify-content:center;">
<div class="card-wrapper" style="justify-content:center; align: center; align-text: center;">
<div class="icon-wrapper">
<span class="mbr-iconfont mbr-iconfont-btn mbri-file" style="color:aliceblue;"></span>
</div>
@ -200,14 +195,14 @@
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2 version">
</h2>
<h2 class="mbr-section-title mbr-fonts-style display-2">
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 class="version"><u>v1.06.4b</u></b>- <b>various improvements</b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for RELEASE NOTES. -->
<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>