Compare commits

...

13 Commits

5 changed files with 264 additions and 25 deletions

View File

@ -28,7 +28,6 @@ const loadAddRemoveAdminPage = async () => {
</p>
<div id="admin-table-section" class="admin-table-section" style="margin-top: 2em;">
<h3 style="color:rgb(212, 212, 212);">Existing Minter Admins</h3>
<div id="admin-list-container" style="margin: 1em auto; max-width: 600px;"></div>
</div>
@ -59,6 +58,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;">
<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;">
<option value="0">Show All</option>
<option value="1">Last 1 day</option>
@ -98,6 +104,14 @@ const loadAddRemoveAdminPage = async () => {
await loadCards(addRemoveIdentifierPrefix)
})
document.getElementById("sort-select").addEventListener("change", async () => {
// Optionally clear or show a message while loading
const cardsContainer = document.getElementById("cards-container")
cardsContainer.innerHTML = "<p>Refreshing cards...</p>"
// Re-load the cards using the same function that handles sorting logic
await loadCards(addRemoveIdentifierPrefix)
})
document.getElementById("cancel-publish-button").addEventListener("click", async () => {
// const cardsContainer = document.getElementById("existing-proposals-section")
// cardsContainer.style.display = "flex" // Restore visibility
@ -133,6 +147,19 @@ const toggleProposeButton = () => {
proposeButton.style.display === 'flex' ? 'none' : 'flex'
}
const toggleAdminTable = () => {
const tableContainer = document.getElementById("adminTableContainer")
const toggleBtn = document.getElementById("toggleAdminTableButton")
if (tableContainer.style.display === "none") {
tableContainer.style.display = "block"
toggleBtn.textContent = "Hide Minter Admins"
} else {
tableContainer.style.display = "none"
toggleBtn.textContent = "Show Minter Admins"
}
}
const fetchAllARTxData = async () => {
const addAdmTx = "ADD_GROUP_ADMIN"
const remAdmTx = "REMOVE_GROUP_ADMIN"
@ -216,6 +243,9 @@ const displayExistingMinterAdmins = async () => {
// 1) Fetch addresses
const admins = await fetchMinterGroupAdmins()
minterAdminAddresses = admins.map(m => m.member)
// Compute total admin count and signatures needed (40%, rounded up)
const totalAdmins = admins.length;
const signaturesNeeded = Math.ceil(totalAdmins * 0.40);
let rowsHtml = "";
for (const adminAddr of admins) {
if (adminAddr.member === nullAddress) {
@ -262,6 +292,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 +320,13 @@ const displayExistingMinterAdmins = async () => {
${rowsHtml}
</tbody>
</table>
<div>
`
adminListContainer.innerHTML = tableHtml
adminListContainer.innerHTML = `
<h3 style="color:rgb(212, 212, 212);">Existing Minter Admins: ${totalAdmins}</h3>
<h4 style="color:rgb(212, 212, 212);">Signatures for Group Approval (40%): ${signaturesNeeded}</h4>
${tableHtml}
`;
} catch (err) {
console.error("Error fetching minter admins:", err)
adminListContainer.innerHTML =
@ -550,7 +601,7 @@ const checkAndDisplayActions = async (adminYes, name, cardIdentifier) => {
} else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){
const totalAdmins = minterAdmins.length
const fortyPercent = totalAdmins * 0.40
minAdminCount = Math.round(fortyPercent)
minAdminCount = Math.ceil(fortyPercent)
console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`)
}
const addressInfo = await getNameInfo(name)
@ -711,6 +762,47 @@ const handleRemoveMinterGroupAdmin = async (name, address) => {
}
}
// ADDED: A simple function to effectively 'delete' an AR Board card
// by publishing an empty card with the same identifier and prefix
const deleteARCard = async (cardIdentifier, prefix) => {
try {
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
if (!confirmed) return
// A minimal blank object
const blankData = {
header: "",
content: "",
links: [],
creator: userState.accountName,
timestamp: Date.now(),
poll: "" // or null. This ensures it won't appear as a valid poll card
}
let base64Data = await objectToBase64(blankData)
if (!base64Data) {
base64Data = btoa(JSON.stringify(blankData))
}
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST", // same as all ARBoard content
identifier: cardIdentifier,
data64: base64Data,
})
alert("Your card has been effectively deleted.")
// Now reload the existing ARBoard cards so the UI no longer shows the old card
await loadCards(prefix)
} 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) {
@ -909,6 +1001,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}', '${addRemoveIdentifierPrefix}')"
>
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

@ -1067,7 +1067,7 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier, name
} else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){
const totalAdmins = minterAdmins.length
const fortyPercent = totalAdmins * 0.40
minAdminCount = Math.round(fortyPercent)
minAdminCount = Math.ceil(fortyPercent)
console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`)
}
if (isBlockPassed && (userState.isMinterAdmin || userState.isAdmin)) {
@ -1258,6 +1258,51 @@ const getNewestAdminCommentTimestamp = async (cardIdentifier) => {
}
}
// ADDED: A simple function to effectively 'delete' an Admin Board card
// by publishing an empty card with the same identifier and prefix
const deleteAdminCard = async (cardIdentifier) => {
try {
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
if (!confirmed) return
// A minimal blank object
const blankData = {
header: "",
content: "",
links: [],
creator: userState.accountName,
timestamp: Date.now(),
poll: "" // or null. This ensures it won't appear as a valid poll card
}
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.")
// Now reload the existing Admin Board cards so the UI no longer shows the old card
await fetchAllEncryptedCards(true)
} catch (error) {
console.error("Error deleting Admin card:", error)
alert("Failed to delete the card. Check console for details.")
}
}
// Create the overall Minter Card HTML -----------------------------------------------
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
const { minterName, header, content, links, creator, timestamp, poll, topicMode } = cardData
@ -1329,9 +1374,9 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
const removeActionsHtml = verifiedAddress ? await checkAndDisplayRemoveActions(adminYes, verifiedAddress, cardIdentifier) : await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
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
}
if (banTransactions.some((banTx) => banTx.groupId === 694 && banTx.offender === accountAddress)){
@ -1410,6 +1455,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

@ -8,7 +8,7 @@ const MIN_ADMIN_YES_VOTES = 9;
const GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT = 2012800 //TODO update this to correct featureTrigger height when known, either that, or pull from core.
let featureTriggerPassed = false
let isApproved = false
const spamNames = ["Exorcist"]
const loadMinterBoardPage = async () => {
// Clear existing content on the page
@ -378,7 +378,7 @@ const processARBoardCards = async (allValidCards) => {
const loadCards = async (cardIdentifierPrefix) => {
const cardsContainer = document.getElementById("cards-container")
let isARBoard = false
cardsContainer.innerHTML = "<p>Loading cards...</p>"
cardsContainer.innerHTML = `<p style="color:dodgerblue;">Loading cards...</p>`
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
isARBoard = true
@ -1118,6 +1118,11 @@ const displayComments = async (cardIdentifier) => {
const commentHTMLArray = await Promise.all(
comments.map(async (comment) => {
try {
// If the name of the commenter is in the "spamNames" array, return null
if (spamNames.includes(comment.name)) {
console.warn(`Commenter ${comment.name} is in the spamNames array, skipping...`)
return null
}
const commentDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: comment.name,
@ -1394,10 +1399,18 @@ const handleInviteMinter = async (minterName) => {
}
}
function escapeForHtmlAttribute(str) {
return str
.replace(/'/g, '&#39;')
.replace(/"/g, '&quot;');
}
const createInviteButtonHtml = (creator, cardIdentifier) => {
// Safely escape special chars so they won't break the HTML attribute
const safeCreator = escapeForHtmlAttribute(creator);
return `
<div id="invite-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button onclick="handleInviteMinter('${creator}')"
<button onclick="handleInviteMinter('${safeCreator}')"
style="padding: 10px; background:rgb(0, 109, 76) ; color: white; border: dotted; border-color: white; cursor: pointer; border-radius: 5px;"
onmouseover="this.style.backgroundColor='rgb(25, 47, 39) '"
onmouseout="this.style.backgroundColor='rgba(7, 122, 101, 0.63) '"
@ -1424,17 +1437,12 @@ const featureTriggerCheck = async () => {
const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => {
if (!userState.isMinterAdmin){
console.warn(`User is NOT an admin, not displaying invite/approve button...`)
return null
}
const isBlockPassed = await featureTriggerCheck()
const minterAdmins = await fetchMinterGroupAdmins()
let minAdminCount = 9
if (isBlockPassed) {
minAdminCount = Math.round(minterAdmins.length * 0.4)
minAdminCount = Math.ceil(minterAdmins.length * 0.4)
console.warn(`Using 40% => ${minAdminCount}`)
}
@ -1475,7 +1483,7 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) =>
console.warn(`PriorBanOrKick determination:`, priorBanOrKick)
const inviteButtonHtml = createInviteButtonHtml(creator, cardIdentifier)
const inviteButtonHtml = userState.isMinterAdmin ? createInviteButtonHtml(creator, cardIdentifier) : ''
const groupApprovalHtml = await checkGroupApprovalAndCreateButton(minterAddress, cardIdentifier, "GROUP_INVITE")
if (!priorBanOrKick) {
@ -1564,7 +1572,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
Existing ${transactionType} Approvals: ${uniqueApprovalCount}
</p>
${tableHtml}
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
${userState.isMinterAdmin ?
`<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button
style="
padding: 8px;
@ -1581,7 +1590,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
>
Approve Invite Tx
</button>
</div>
</div>`
: ''}
</div>
`
return approvalButtonHtml
@ -1741,9 +1751,19 @@ async function buildApprovalTableHtml(approvalTxs, getNameFunc) {
// Format the transaction timestamp
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>
`
@ -1885,6 +1905,47 @@ const getNewestCommentTimestamp = async (cardIdentifier) => {
}
}
// ADDED: A simple function to effectively 'delete' a Minter Board card
// by publishing an empty card with the same identifier and prefix
const deleteCard = async (cardIdentifier, prefix) => {
try {
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
if (!confirmed) return
// A minimal blank object
const blankData = {
header: "",
content: "",
links: [],
creator: userState.accountName,
timestamp: Date.now(),
poll: "" // or null. This ensures it won't appear as a valid poll card
}
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.")
// Now reload the existing Minter Board cards so the UI no longer shows the old card
await loadCards(prefix)
} 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 { header, content, links, creator, timestamp, poll } = cardData
@ -1992,6 +2053,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}', '${minterCardIdentifierPrefix}')"
>
DELETE CARD
</button>
</div>
` : ''}
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
<textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>

View File

@ -331,7 +331,8 @@ const getNameInfo = async (name) => {
console.log('getNameInfo called')
console.log('name:', name)
try {
const response = await fetch(`${baseUrl}/names/${name}`)
// Encode the name for URL safety
const response = await fetch(`${baseUrl}/names/${encodeURIComponent(name)}`)
if (!response.ok) {
console.warn(`Failed to fetch name info for: ${name}, status: ${response.status}`)

View File

@ -30,6 +30,10 @@
<link href="./assets/quill/quill.snow.css" rel="stylesheet">
</head>
<body>
<script>
// Define one variable for your version string:
const Q_MINTERSHIP_VERSION = "1.04"; // Update here in the future
</script>
<section data-bs-version="5.1" class="menu menu1 boldm5 cid-ttRnktJ11Q" once="menu" id="menu1-0">
@ -42,7 +46,7 @@
</a>
</span>
<span class="navbar-caption-wrap">
<a class="navbar-caption display-4" href="index.html">Q-Mintership (v1.04b)
<a class="navbar-caption display-4" href="index.html"><span id="versionString1" class="navbar-caption display-4"></span>
</a>
</span>
</div>
@ -61,7 +65,7 @@
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
</a>
</span>
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership v1.04b<br></a></span>
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html"><span id="versionString2" class="navbar-caption text-primary display-4"></span></a></span>
</div>
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
@ -572,12 +576,12 @@
<div class="title-wrapper">
<div class="title-wrap">
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership (v1.04b)</h2>
<h2 class="mbr-section-title mbr-fonts-style display-5"><span id="versionString3" class="mbr-section-title mbr-fonts-style display-5"></span></h2>
</div>
</div>
<a class="link-wrap" href="#">
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v1.04beta</p>
<p class="mbr-link mbr-fonts-style display-4"><span id="versionString4" class="mbr-link mbr-fonts-style display-4"></span></p>
</a>
</div>
<div class="col-12 col-lg-6">
@ -590,6 +594,12 @@
</div>
</section>
<script>
document.getElementById("versionString1").textContent = `Q-Mintership (v${Q_MINTERSHIP_VERSION}b)`;
document.getElementById("versionString2").textContent = `Q-Mintership v${Q_MINTERSHIP_VERSION}b`;
document.getElementById("versionString3").textContent = `Q-Mintership (v${Q_MINTERSHIP_VERSION}b)`;
document.getElementById("versionString4").textContent = `Q-Mintership v${Q_MINTERSHIP_VERSION}beta`;
</script>
<script src="./assets/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="./assets/parallax/jarallax.js"></script>