Version 1.05b - see release notes published on Q-Mintership forum in General room for details. Many fixes and new features added.

This commit is contained in:
crowetic 2025-01-29 19:12:30 -08:00
parent 5a35fb0d07
commit 021c99c119
8 changed files with 586 additions and 384 deletions

View File

@ -461,6 +461,27 @@
/* General Page Styles */ /* General Page Styles */
/* Main Container for Minter Board */ /* Main Container for Minter Board */
input[type="checkbox"] {
width: 25px; /* Adjust width */
height: 25px; /* Adjust height */
appearance: none; /* Remove default styling */
background-color: rgb(15, 21, 22);
border: 2px solid rgb(98, 99, 106);
border-radius: 4px; /* Optional rounded corners */
display: inline-block;
position: relative;
}
input[type="checkbox"]:checked::after {
content: "✔"; /* Custom checkmark */
font-size: 20px;
color: rgb(201, 201, 201);
position: absolute;
top: 0;
left: 4px;
}
body { body {
background-color: black; background-color: black;
} }
@ -555,6 +576,65 @@ body {
margin-bottom: 2vh; margin-bottom: 2vh;
} }
.blocklist-form {
display: flex;
flex-direction: column;
background-color: #1b1b1b; /* or your dark color */
border: 1px solid #444;
border-radius: 5px;
padding: 20px;
margin: 20px auto; /* center horizontally */
max-width: 600px; /* limit width */
color: #ddd; /* text color */
text-align: center;
align-items: center;
}
.blocklist-display {
/* This holds the list of blocked names */
border: 1px dashed;
background-color:#000000;
width: 90%;
font-size: 1.8rem;
color: #4d0000;
text-align: center;
align-items: center;
/* you could style the list items or bullet if you like */
}
.blocklist-button-container {
/* This area will hold the text input + the row for buttons */
display: flex;
flex-direction: row;
gap: 10px;
}
.blocklist-form input.blocklist-input {
padding: 1rem;
font-size: 2rem;
line-height: 2;
background-color: #14161a;
border: 1px solid #8caeb0;
border-radius: 4px;
color: #5c0101;
}
.publish-card-button {
background-color: #529d8d84;
color: #fff;
border: none;
border-radius: 5px;
padding: 10px 14px;
cursor: pointer;
font-size: 1.5rem;
transition: background-color 0.2s ease;
}
.publish-card-button:hover {
background-color: #3b5c71; /* a darker variant */
}
.publish-card-view textarea { .publish-card-view textarea {
min-height: 15vh; min-height: 15vh;
resize: vertical; resize: vertical;
@ -570,9 +650,9 @@ body {
margin-bottom: 1.5vh; margin-bottom: 1.5vh;
} }
/* Buttons Inside Form */ /* Generic: all buttons inside .publish-card-form */
#publish-card-form button { .publish-card-form button {
background-color: #76c7c0; background-color: #359f4a;
color: #1e1e1e; color: #1e1e1e;
border: none; border: none;
border-radius: 0.5vh; border-radius: 0.5vh;
@ -583,30 +663,76 @@ body {
transition: background-color 0.3s; transition: background-color 0.3s;
} }
#publish-card-form button:hover { .publish-card-form button:hover {
background-color: #5e92a8; background-color: #5e92a8;
} }
#publish-card-form #add-link-button { /* Then specifically override the add button */
.publish-card-form #blocklist-add-button {
background-color: #233748;
color: #ffffff;
}
.publish-card-form #blocklist-add-button:hover {
background-color: #1b1936;
}
.publish-card-form #add-link-button {
background-color: #233748; background-color: #233748;
color: #ffffff; color: #ffffff;
margin-bottom: 2vh; margin-bottom: 2vh;
} }
#publish-card-form #add-link-button:hover { .publish-card-form #add-link-button:hover {
background-color: #1b1936; background-color: #1b1936;
} }
/* Cancel Button */ /* And specifically override the remove button */
#publish-card-form #cancel-publish-button { .publish-card-form #blocklist-remove-button {
background-color: #463737; background-color: #463737;
color: #ffffff; color: #ffffff;
} }
#publish-card-form #cancel-publish-button:hover { .publish-card-form #blocklist-remove-button:hover {
background-color: #281e1e; background-color: #281e1e;
} }
.publish-card-form #cancel-publish-button {
background-color: #463737;
color: #fff;
}
.publish-card-form #cancel-publish-button:hover {
background-color: #281e1e;
}
/* 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;
}
}
.refresh-cards-button {
border-color: white;
border-radius: 1.5vh;
background-color: black;
color: white;
}
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.publish-card-view { .publish-card-view {
@ -619,7 +745,7 @@ body {
padding: 1.5vh; padding: 1.5vh;
} }
#publish-card-form button { .publish-card-form button {
font-size: 1.8vh; font-size: 1.8vh;
padding: 1.2vh; padding: 1.2vh;
} }

View File

@ -37,7 +37,7 @@ const loadAddRemoveAdminPage = async () => {
Propose a Minter for Admin Position Propose a Minter for Admin Position
</button> </button>
<div id="promotion-form-container" class="publish-card-view" style="display: none; margin-top: 1em;"> <div id="promotion-form-container" class="publish-card-view" style="display: none; margin-top: 1em;">
<form id="publish-card-form"> <form id="publish-card-form" class="publish-card-form">
<h3>Create or Update Promotion/Demotion Proposal Card</h3> <h3>Create or Update Promotion/Demotion Proposal Card</h3>
<label for="minter-name-input">Input NAME (promotion):</label> <label for="minter-name-input">Input NAME (promotion):</label>
<input type="text" id="minter-name-input" maxlength="100" placeholder="input NAME of MINTER for PROMOTION" required> <input type="text" id="minter-name-input" maxlength="100" placeholder="input NAME of MINTER for PROMOTION" required>
@ -59,7 +59,7 @@ const loadAddRemoveAdminPage = async () => {
<div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;"> <div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3> <h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button> <button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button>
<select id="time-range-select" style="margin-left: 10px; padding: 5px;"> <select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;">
<option value="0">Show All</option> <option value="0">Show All</option>
<option value="1">Last 1 day</option> <option value="1">Last 1 day</option>
<option value="7">Last 7 days</option> <option value="7">Last 7 days</option>
@ -176,7 +176,7 @@ const fetchAllARTxData = async () => {
} }
} }
function partitionAddTransactions(rawTransactions) { const partitionAddTransactions = (rawTransactions) => {
const finalAddTxs = [] const finalAddTxs = []
const pendingAddTxs = [] const pendingAddTxs = []
@ -191,7 +191,7 @@ function partitionAddTransactions(rawTransactions) {
return { finalAddTxs, pendingAddTxs }; return { finalAddTxs, pendingAddTxs };
} }
function partitionRemoveTransactions(rawTransactions) { const partitionRemoveTransactions = (rawTransactions) => {
const finalRemTxs = [] const finalRemTxs = []
const pendingRemTxs = [] const pendingRemTxs = []
@ -434,15 +434,17 @@ const publishARCard = async (cardIdentifierPrefix) => {
if (exists) { if (exists) {
alert(`An existing card was found, you must update it, two cards for the samme name cannot be published! Loading card data...`) alert(`An existing card was found, you must update it, two cards for the samme name cannot be published! Loading card data...`)
await loadCardIntoForm(existingCardData) if (exists.creator != userState.accountName) {
minterName = exists.minterName alert(`You are not the original publisher of this card, exiting.`)
const nameInfo = await getNameInfo(exists.minterName) return
address = nameInfo.owner }else {
isExistingCard = true await loadCardIntoForm(existingCardData)
} else if (otherPublisher){ minterName = exists.minterName
alert(`An existing card was found, but you are NOT the publisher, you may not publish duplicates, and you may not update a non-owned card! Please try again with another name, or use the existing card for ${minterNameInput}`) const nameInfo = await getNameInfo(exists.minterName)
return address = nameInfo.owner
} isExistingCard = true
}
}
const minterGroupData = await fetchMinterGroupMembers() const minterGroupData = await fetchMinterGroupMembers()
minterGroupAddresses = minterGroupData.map(m => m.member) minterGroupAddresses = minterGroupData.map(m => m.member)
@ -481,6 +483,7 @@ const publishARCard = async (cardIdentifierPrefix) => {
const cardData = { const cardData = {
minterName, minterName,
minterAddress: address,
header, header,
content, content,
links, links,
@ -550,7 +553,7 @@ const checkAndDisplayActions = async (adminYes, name, cardIdentifier) => {
} else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){ } else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){
const totalAdmins = minterAdmins.length const totalAdmins = minterAdmins.length
const fortyPercent = totalAdmins * 0.40 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}`) 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) const addressInfo = await getNameInfo(name)
@ -735,7 +738,7 @@ const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins)
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress, illegalDuplicate) => { const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress, illegalDuplicate) => {
const { minterName, minterAddress='priorToAddition', header, content, links, creator, timestamp, poll, promotionCard } = cardData const { minterName, minterAddress='', header, content, links, creator, timestamp, poll, promotionCard } = cardData
const formattedDate = new Date(timestamp).toLocaleString() const formattedDate = new Date(timestamp).toLocaleString()
const minterAvatar = await getMinterAvatar(minterName) const minterAvatar = await getMinterAvatar(minterName)
const creatorAvatar = await getMinterAvatar(creator) const creatorAvatar = await getMinterAvatar(creator)
@ -744,6 +747,14 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
${`Link ${index + 1} - ${link}`} ${`Link ${index + 1} - ${link}`}
</button> </button>
`).join("") `).join("")
// Adding fix for accidental code in 1.04b
let publishedMinterAddress
if (!minterAddress || minterAddress ==='priorToAddition'){
publishedMinterAddress = ''
} else if (minterAddress){
console.log(`minter address found in card info: ${minterAddress}`)
publishedMinterAddress = minterAddress
}
const minterGroupMembers = await fetchMinterGroupMembers() const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins() const minterAdmins = await fetchMinterGroupAdmins()

View File

@ -10,8 +10,8 @@ let isTopic = false
let attemptLoadAdminDataCount = 0 let attemptLoadAdminDataCount = 0
let adminMemberCount = 0 let adminMemberCount = 0
let adminPublicKeys = [] let adminPublicKeys = []
let kickTransactions = [] // let kickTransactions = []
let banTransactions = [] // let banTransactions = []
let adminBoardState = { let adminBoardState = {
kickedCards: new Set(), // store identifiers kickedCards: new Set(), // store identifiers
bannedCards: new Set(), // likewise bannedCards: new Set(), // likewise
@ -76,14 +76,14 @@ const loadAdminBoardPage = async () => {
<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> <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="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> <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;"> <select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:rgb(70, 106, 105); background-color: black;">
<option value="newest" selected>Sort by Date</option> <option value="newest" selected>Sort by Date</option>
<option value="name">Sort by Name</option> <option value="name">Sort by Name</option>
<option value="recent-comments">Newest Comments</option> <option value="recent-comments">Newest Comments</option>
<option value="least-votes">Least Votes</option> <option value="least-votes">Least Votes</option>
<option value="most-votes">Most Votes</option> <option value="most-votes">Most Votes</option>
</select> </select>
<select id="time-range-select" style="margin-left: 10px; padding: 5px;"> <select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;">
<option value="0">Show All</option> <option value="0">Show All</option>
<option value="1">Last 1 day</option> <option value="1">Last 1 day</option>
<option value="7">Last 7 days</option> <option value="7">Last 7 days</option>
@ -98,7 +98,7 @@ const loadAdminBoardPage = async () => {
</div> </div>
<div id="encrypted-cards-container" class="cards-container" style="margin-top: 20px;"></div> <div id="encrypted-cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;"> <div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<form id="publish-card-form"> <form id="publish-card-form" class="publish-card-form">
<h3>Create or Update an Admin Card</h3> <h3>Create or Update an Admin Card</h3>
<div class="publish-card-checkbox" style="margin-top: 1em;"> <div class="publish-card-checkbox" style="margin-top: 1em;">
<input type="checkbox" id="topic-checkbox" name="topicMode" /> <input type="checkbox" id="topic-checkbox" name="topicMode" />
@ -195,68 +195,67 @@ const loadAdminBoardPage = async () => {
createScrollToTopButton() createScrollToTopButton()
// await fetchAndValidateAllAdminCards() // await fetchAndValidateAllAdminCards()
await updateOrSaveAdminGroupsDataLocally() await updateOrSaveAdminGroupsDataLocally()
await fetchAllKicKBanTxData()
await fetchAllEncryptedCards() await fetchAllEncryptedCards()
} }
const fetchAllKicKBanTxData = async () => { // const fetchAllKicKBanTxData = async () => {
const kickTxType = "GROUP_KICK" // const kickTxType = "GROUP_KICK"
const banTxType = "GROUP_BAN" // const banTxType = "GROUP_BAN"
// Helper function to filter transactions // // Helper function to filter transactions
const filterTransactions = (rawTransactions) => { // const filterTransactions = (rawTransactions) => {
// Group transactions by member // // Group transactions by member
const memberTxMap = rawTransactions.reduce((map, tx) => { // const memberTxMap = rawTransactions.reduce((map, tx) => {
if (!map[tx.member]) { // if (!map[tx.member]) {
map[tx.member] = [] // map[tx.member] = []
} // }
map[tx.member].push(tx) // map[tx.member].push(tx)
return map // return map
}, {}) // }, {})
// Filter out members with both pending and non-pending transactions // // Filter out members with both pending and non-pending transactions
return Object.values(memberTxMap) // return Object.values(memberTxMap)
.filter(txs => txs.every(tx => tx.approvalStatus !== 'PENDING')) // .filter(txs => txs.every(tx => tx.approvalStatus !== 'PENDING'))
.flat() // .flat()
// .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') && // // .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') &&
// txs.some(tx => tx.approvalStatus !== 'PENDING'))) // // txs.some(tx => tx.approvalStatus !== 'PENDING')))
// .flat() // // .flat()
} // }
// Fetch ban transactions // // Fetch ban transactions
const rawBanTransactions = await searchTransactions({ // const rawBanTransactions = await searchTransactions({
txTypes: [banTxType], // txTypes: [banTxType],
address: '', // address: '',
confirmationStatus: 'CONFIRMED', // confirmationStatus: 'CONFIRMED',
limit: 0, // limit: 0,
reverse: true, // reverse: true,
offset: 0, // offset: 0,
startBlock: 1990000, // startBlock: 1990000,
blockLimit: 0, // blockLimit: 0,
txGroupId: 0, // txGroupId: 0,
}) // })
// Filter transactions for bans // // Filter transactions for bans
banTransactions = filterTransactions(rawBanTransactions) // banTransactions = filterTransactions(rawBanTransactions)
console.warn('banTxData (filtered):', banTransactions) // console.warn('banTxData (filtered):', banTransactions)
// Fetch kick transactions // // Fetch kick transactions
const rawKickTransactions = await searchTransactions({ // const rawKickTransactions = await searchTransactions({
txTypes: [kickTxType], // txTypes: [kickTxType],
address: '', // address: '',
confirmationStatus: 'CONFIRMED', // confirmationStatus: 'CONFIRMED',
limit: 0, // limit: 0,
reverse: true, // reverse: true,
offset: 0, // offset: 0,
startBlock: 1990000, // startBlock: 1990000,
blockLimit: 0, // blockLimit: 0,
txGroupId: 0, // txGroupId: 0,
}) // })
// Filter transactions for kicks // // Filter transactions for kicks
kickTransactions = filterTransactions(rawKickTransactions) // kickTransactions = filterTransactions(rawKickTransactions)
console.warn('kickTxData (filtered):', kickTransactions) // console.warn('kickTxData (filtered):', kickTransactions)
} // }
// Example: fetch and save admin public keys and count // Example: fetch and save admin public keys and count
@ -744,16 +743,30 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
// If not topic mode, validate the user actually entered a valid Minter name // If not topic mode, validate the user actually entered a valid Minter name
if (!isTopic) { if (!isTopic) {
let minterAddress
publishedMinterName = await validateMinterName(minterNameInput) publishedMinterName = await validateMinterName(minterNameInput)
if (!publishedMinterName) { if (!publishedMinterName) {
alert(`"${minterNameInput}" doesn't seem to be a valid name. Please check or use topic mode.`) try {
return const addressInfo = await getAddressInfo(minterNameInput)
} if (addressInfo) {
console.warn(`checked minterNameInput and found it to be an address... proceeding accordingly.`)
minterAddress = addressInfo.address
publishedMinterName = addressInfo.address
} else {
alert(`"${minterNameInput}" doesn't seem to be a valid name or address. Please check or use topic mode.`)
return
}
} catch (error) {
console.warn(`error checking for address...?`, error)
alert(`Failed to verify name/address. Please try again, or change to topicMode to publish anything else.`)
return
}
}
// Also check for existing card if not topic // Also check for existing card if not topic
if (!isUpdateCard && existingCardMinterNames.some(item => item.minterName === publishedMinterName)) { if (!isUpdateCard && existingCardMinterNames.some(item => item.minterName === publishedMinterName)) {
const duplicateCardData = existingCardMinterNames.find(item => item.minterName === publishedMinterName) const duplicateCardData = existingCardMinterNames.find(item => item.minterName === publishedMinterName)
const updateCard = confirm( const updateCard = confirm(
`Minter Name: ${publishedMinterName} already has a card. Duplicate name-based cards are not allowed. You can OVERWRITE it or Cancel publishing. UPDATE CARD?` `Minter Name: ${publishedMinterName} already has a card. (NOTE this update functionality is no longer functional, it may or may not come back. Even if you update the card you won't see it. It is suggested to CANCEL and use topic mode.`
) )
if (updateCard) { if (updateCard) {
existingEncryptedCardIdentifier = duplicateCardData.identifier existingEncryptedCardIdentifier = duplicateCardData.identifier
@ -763,6 +776,9 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
} }
} }
} }
if (!publishedMinterName && minterAddress){
console.log(`No name was found, but an address was, publishing address in cardData, and using address as name for card.`)
}
// Determine final card identifier // Determine final card identifier
const currentTimestamp = Date.now() const currentTimestamp = Date.now()
@ -1067,7 +1083,7 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier, name
} else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){ } else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){
const totalAdmins = minterAdmins.length const totalAdmins = minterAdmins.length
const fortyPercent = totalAdmins * 0.40 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}`) 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)) { if (isBlockPassed && (userState.isMinterAdmin || userState.isAdmin)) {
@ -1260,7 +1276,7 @@ const getNewestAdminCommentTimestamp = async (cardIdentifier) => {
// Create the overall Minter Card HTML ----------------------------------------------- // Create the overall Minter Card HTML -----------------------------------------------
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => { const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
const { minterName, header, content, links, creator, timestamp, poll, topicMode } = cardData const { minterName, minterAddress = '', header, content, links, creator, timestamp, poll, topicMode } = cardData
const formattedDate = new Date(timestamp).toLocaleString() const formattedDate = new Date(timestamp).toLocaleString()
const minterAvatar = !topicMode ? await getMinterAvatar(minterName) : null const minterAvatar = !topicMode ? await getMinterAvatar(minterName) : null
const creatorAvatar = await getMinterAvatar(creator) const creatorAvatar = await getMinterAvatar(creator)
@ -1278,6 +1294,8 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
let showTopic = false let showTopic = false
const { finalKickTxs, pendingKickTxs, finalBanTxs, pendingBanTxs } = await fetchAllKickBanTxData()
if (hasTopicMode) { if (hasTopicMode) {
const modeVal = cardData.topicMode const modeVal = cardData.topicMode
showTopic = (modeVal === true || modeVal === 'true') showTopic = (modeVal === true || modeVal === 'true')
@ -1319,22 +1337,55 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
accountInfo = await getNameInfo(verifiedName) accountInfo = await getNameInfo(verifiedName)
} }
const accountAddress = verifiedAddress ? addressVerification.address : accountInfo.owner const accountAddress = verifiedAddress ? addressVerification.address : accountInfo.owner
const addressInfo = verifiedAddress ? addressVerification : await getAddressInfo(accountAddress) const addressInfo = verifiedAddress ? addressVerification : await getAddressInfo(accountAddress)
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
const adminAddresses = minterAdmins.map(m => m.member)
const existingAdmin = adminAddresses.includes(accountAddress)
const existingMinter = minterGroupAddresses.includes(accountAddress)
levelText = ` - Level ${addressInfo.level}</h3>` levelText = ` - Level ${addressInfo.level}</h3>`
console.log(`name is validated, utilizing for removal features...${verifiedName}`) console.log(`name is validated, utilizing for removal features...${verifiedName}`)
penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<p>' penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<p>'
adjustmentText = addressInfo.blocksMintedAdjustment == 0 ? '' : '<p>(has Blocks Adjustment)<p>' adjustmentText = addressInfo.blocksMintedAdjustment == 0 ? '' : '<p>(has Blocks Adjustment)<p>'
const removeActionsHtml = verifiedAddress ? await checkAndDisplayRemoveActions(adminYes, verifiedAddress, cardIdentifier) : await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier) const removeActionsHtml = verifiedAddress ? await checkAndDisplayRemoveActions(adminYes, verifiedAddress, cardIdentifier, true) : await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
showRemoveHtml = removeActionsHtml showRemoveHtml = removeActionsHtml
if (userVote === 0) { if (userVote === 0) {
cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want
} else if (userVote === 1) { } else if (userVote === 1) {
cardColorCode = "rgba(55, 12, 12, 0.61)"; // or any red you want cardColorCode = "rgba(55, 12, 12, 0.61)"; // or any red you want
} }
const confirmedKick = finalKickTxs.some(
(tx) => tx.groupId === 694 && tx.member === accountAddress
)
const pendingKick = pendingKickTxs.some(
(tx) => tx.groupId === 694 && tx.member === accountAddress
)
const confirmedBan = finalBanTxs.some(
(tx) => tx.groupId === 694 && tx.offender === accountAddress
)
const pendingBan = pendingBanTxs.some(
(tx) => tx.groupId === 694 && tx.offender === accountAddress
)
if (banTransactions.some((banTx) => banTx.groupId === 694 && banTx.offender === accountAddress)){ // If user is definitely admin (finalAdd) and not pending removal
if (confirmedKick && !pendingKick && !existingMinter) {
console.warn(`account was already kicked, displaying as such...`)
cardColorCode = 'rgb(29, 7, 4)'
altText = `<h4 style="color:rgb(143, 117, 21); margin-bottom: 0.5em;">KICKED From MINTER Group</h4>`
showRemoveHtml = ''
if (!adminBoardState.kickedCards.has(cardIdentifier)){
adminBoardState.kickedCards.add(cardIdentifier)
}
if (!showKickedBanned) {
console.warn(`kick/ban checkbox is unchecked, card is kicked, not displaying...`)
return ''
}
}
if (confirmedBan && !pendingBan && !pendingKick && !existingMinter) {
console.warn(`account was already banned, displaying as such...`) console.warn(`account was already banned, displaying as such...`)
cardColorCode = 'rgb(24, 3, 3)' cardColorCode = 'rgb(24, 3, 3)'
altText = `<h4 style="color:rgb(106, 2, 2); margin-bottom: 0.5em;">BANNED From MINTER Group</h4>` altText = `<h4 style="color:rgb(106, 2, 2); margin-bottom: 0.5em;">BANNED From MINTER Group</h4>`
@ -1348,19 +1399,33 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
} }
} }
if (kickTransactions.some((kickTx) => kickTx.groupId === 694 && kickTx.member === accountAddress)){ // if (banTransactions.some((banTx) => banTx.groupId === 694 && banTx.offender === accountAddress)){
console.warn(`account was already kicked, displaying as such...`) // console.warn(`account was already banned, displaying as such...`)
cardColorCode = 'rgb(29, 7, 4)' // cardColorCode = 'rgb(24, 3, 3)'
altText = `<h4 style="color:rgb(143, 117, 21); margin-bottom: 0.5em;">KICKED From MINTER Group</h4>` // altText = `<h4 style="color:rgb(106, 2, 2); margin-bottom: 0.5em;">BANNED From MINTER Group</h4>`
showRemoveHtml = '' // showRemoveHtml = ''
if (!adminBoardState.kickedCards.has(cardIdentifier)){ // if (!adminBoardState.bannedCards.has(cardIdentifier)){
adminBoardState.kickedCards.add(cardIdentifier) // adminBoardState.bannedCards.add(cardIdentifier)
} // }
if (!showKickedBanned) { // if (!showKickedBanned){
console.warn(`kick/ban checkbox is unchecked, card is kicked, not displaying...`) // console.warn(`kick/bank checkbox is unchecked, and card is banned, not displaying...`)
return '' // return ''
} // }
} // }
// if (kickTransactions.some((kickTx) => kickTx.groupId === 694 && kickTx.member === accountAddress)){
// console.warn(`account was already kicked, displaying as such...`)
// cardColorCode = 'rgb(29, 7, 4)'
// altText = `<h4 style="color:rgb(143, 117, 21); margin-bottom: 0.5em;">KICKED From MINTER Group</h4>`
// showRemoveHtml = ''
// if (!adminBoardState.kickedCards.has(cardIdentifier)){
// adminBoardState.kickedCards.add(cardIdentifier)
// }
// if (!showKickedBanned) {
// console.warn(`kick/ban checkbox is unchecked, card is kicked, not displaying...`)
// return ''
// }
// }
} else { } else {
console.log(`name could not be validated, assuming topic card (or some other issue with name validation) for removalActions`) console.log(`name could not be validated, assuming topic card (or some other issue with name validation) for removalActions`)

View File

@ -1,121 +1,134 @@
let currentMinterToolPage = 'overview'; // Track the current page let currentMinterToolPage = 'overview'; // Track the current page
// Load latest state for admin verification const loadMinterAdminToolsPage = async () => {
async function verifyMinterAdminState() {
const minterGroupAdmins = await fetchMinterGroupAdmins();
return minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin);
}
async function loadMinterAdminToolsPage() {
// Remove all body content except for menu elements // Remove all body content except for menu elements
const bodyChildren = document.body.children; const bodyChildren = document.body.children;
for (let i = bodyChildren.length - 1; i >= 0; i--) { for (let i = bodyChildren.length - 1; i >= 0; i--) {
const child = bodyChildren[i]; const child = bodyChildren[i];
if (!child.classList.contains('menu')) { if (!child.classList.contains('menu')) {
child.remove(); child.remove()
} }
} }
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`; const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`
// Set the background image directly from a file // Set the background image directly from a file
const mainContent = document.createElement('div'); const mainContent = document.createElement('div')
// In your 'AdminTools' code
mainContent.innerHTML = ` mainContent.innerHTML = `
<div class="tools-main mbr-parallax-background cid-ttRnlSkg2R"> <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 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;">MINTER ADMIN TOOLS </h1><a style="color: red;">Under Construction...</a></div> <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; "> <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;"> <img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
<span>${userState.accountName || 'Guest'}</span> <span>${userState.accountName || 'Guest'}</span>
</div>
<div><h2>COMING SOON...</h2></div>
<div>
<p>This page will have functionality to assist the Minter Admins in performing their duties. It will display all pending transactions (of any kind they can approve/deny) along with that ability. It can also be utilized to obtain more in-depth information about existing accounts.</p>
<p> The page will be getting a significant overhaul in the near(ish) future, as the MINTER group is now owned by null, and we are past the 'temporary state' we were in for much longer than planned.</p>
</div>
</div> </div>
<div><h2>Welcome to Admin Tools</h2></div>
<div id="tools-submenu" class="tools-submenu"> <div>
<div class="tools-buttons"> <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>
<button id="display-pending" class="tools-button">Display Pending Approval Transactions</button> <p>More features will be added as time goes on. This is the start of the functionality here.</p>
<button id="create-group-invite" class="tools-button">Create Pending Group Invite</button>
<button id="create-promotion" class="tools-button">Create Pending Promotion</button>
</div>
<div id="tools-window" class="tools-window"></div>
</div> </div>
</div> </div>
`;
document.body.appendChild(mainContent); <div id="tools-submenu" class="tools-submenu">
<div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;">
<button id="toggle-blocklist-button" class="publish-card-button">Add/Remove blockedUsers</button>
<button id="create-group-invite" class="publish-card-button">Create Pending Group Invite</button>
</div>
<div id="tools-window" class="tools-window" style="margin-top: 2em;">
<div id="blocklist-container" class="blocklist-form" style="display: none;">
<h3 style="margin-top: 0;">Comment Block List</h3>
<div id="blocklist-display" class="blocklist-display" style="margin-bottom: 1em;"></div>
<input
type="text"
id="blocklist-input"
class="blocklist-input"
placeholder="Enter name to block/unblock"
style="margin-bottom: 1em;"
/>
<div class="blocklist-button-container publish-card-form">
<button id="blocklist-add-button" class="publish-card-button">Add</button>
<button id="blocklist-remove-button" class="publish-card-button">Remove</button>
</div>
</div>
</div>
</div>
</div>
`
document.body.appendChild(mainContent)
addToolsPageEventListeners(); addToolsPageEventListeners()
} }
function addToolsPageEventListeners() { function addToolsPageEventListeners() {
document.getElementById("display-pending").addEventListener("click", async () => { document.getElementById("toggle-blocklist-button").addEventListener("click", async () => {
await displayPendingApprovals(); const container = document.getElementById("blocklist-container")
}); // toggle show/hide
container.style.display = (container.style.display === "none" ? "flex" : "none")
// if showing, load the block list
if (container.style.display === "flex") {
const currentBlockList = await fetchBlockList()
displayBlockList(currentBlockList)
}
})
document.getElementById("create-group-invite").addEventListener("click", async () => { document.getElementById("blocklist-add-button").addEventListener("click", async () => {
createPendingGroupInvite(); const blocklistInput = document.getElementById("blocklist-input")
}); const nameToAdd = blocklistInput.value.trim()
if (!nameToAdd) return
// fetch existing
const currentBlockList = await fetchBlockList()
// add if not already in list
if (!currentBlockList.includes(nameToAdd)) {
currentBlockList.push(nameToAdd)
}
// publish updated
await publishBlockList(currentBlockList)
displayBlockList(currentBlockList)
blocklistInput.value = ""
alert(`"${nameToAdd}" added to the block list!`)
})
// Remove
document.getElementById("blocklist-remove-button").addEventListener("click", async () => {
const blocklistInput = document.getElementById("blocklist-input")
const nameToRemove = blocklistInput.value.trim()
if (!nameToRemove) return
// fetch existing
let currentBlockList = await fetchBlockList()
// remove if present
currentBlockList = currentBlockList.filter(name => name !== nameToRemove)
// publish updated
await publishBlockList(currentBlockList)
displayBlockList(currentBlockList)
blocklistInput.value = ""
alert(`"${nameToRemove}" removed from the block list (if it was present).`)
})
document.getElementById("create-promotion").addEventListener("click", async () => {
createPendingPromotion();
});
} }
// Fetch and display pending approvals const displayBlockList = (blockedNames) => {
async function displayPendingApprovals() { const blocklistDisplay = document.getElementById("blocklist-display")
console.log("Fetching pending approval transactions..."); if (!blockedNames || blockedNames.length === 0) {
const response = await qortalRequest({ blocklistDisplay.innerHTML = "<p>No blocked users currently.</p>"
action: "SEARCH_TRANSACTIONS", return
txGroupId: 694,
txType: [
"ADD_GROUP_ADMIN",
"GROUP_INVITE"
],
confirmationStatus: "UNCONFIRMED",
limit: 0,
offset: 0,
reverse: false
});
console.log("Fetched pending approvals: ", response);
const toolsWindow = document.getElementById('tools-window');
if (response && response.length > 0) {
toolsWindow.innerHTML = response.map(tx => `
<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;">
<p><strong>Transaction Type:</strong> ${tx.type}</p>
<p><strong>Amount:</strong> ${tx.amount}</p>
<p><strong>Creator Address:</strong> ${tx.creatorAddress}</p>
<p><strong>Recipient:</strong> ${tx.recipient}</p>
<p><strong>Timestamp:</strong> ${new Date(tx.timestamp).toLocaleString()}</p>
<button onclick="approveTransaction('${tx.signature}')">Approve</button>
</div>
`).join('');
} else {
toolsWindow.innerHTML = '<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;"><p>No pending approvals found.</p></div>';
} }
blocklistDisplay.innerHTML = `
<ul>
${blockedNames.map(name => `<li>${name}</li>`).join("")}
</ul>
`
} }
// Placeholder function to create a pending group invite
async function createPendingGroupInvite() {
console.log("Creating a pending group invite...");
// Placeholder code for creating a pending group invite
alert('Pending group invite created (placeholder).');
}
// Placeholder function to create a pending promotion
async function createPendingPromotion() {
console.log("Creating a pending promotion...");
// Placeholder code for creating a pending promotion
alert('Pending promotion created (placeholder).');
}
// Placeholder function for approving a transaction
function approveTransaction(signature) {
console.log("Approving transaction with signature: ", signature);
// Placeholder code for approving transaction
alert(`Transaction with signature ${signature} approved (placeholder).`);
}

View File

@ -30,14 +30,14 @@ const loadMinterBoardPage = async () => {
<p style="font-size: 1.25em;"> Publish a Minter Card with Information, and obtain and view the support of the community. Welcome to the Minter Board!</p> <p style="font-size: 1.25em;"> Publish a Minter Card with Information, and obtain and view the support of the community. Welcome to the Minter Board!</p>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px; background-color: ${publishButtonColor}">Publish Minter Card</button> <button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px; background-color: ${publishButtonColor}">Publish Minter Card</button>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</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;"> <select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:rgb(38, 106, 106); background-color: black;">
<option value="newest" selected>Sort by Date</option> <option value="newest" selected>Sort by Date</option>
<option value="name">Sort by Name</option> <option value="name">Sort by Name</option>
<option value="recent-comments">Newest Comments</option> <option value="recent-comments">Newest Comments</option>
<option value="least-votes">Least Votes</option> <option value="least-votes">Least Votes</option>
<option value="most-votes">Most Votes</option> <option value="most-votes">Most Votes</option>
</select> </select>
<select id="time-range-select" style="margin-left: 10px; padding: 5px;"> <select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;">
<option value="0">Show All</option> <option value="0">Show All</option>
<option value="1">Last 1 day</option> <option value="1">Last 1 day</option>
<option value="7">Last 7 days</option> <option value="7">Last 7 days</option>
@ -46,7 +46,7 @@ const loadMinterBoardPage = async () => {
</select> </select>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div> <div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;"> <div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<form id="publish-card-form"> <form id="publish-card-form" class="publish-card-form">
<h3>Create or Update Your Card</h3> <h3>Create or Update Your Card</h3>
<label for="card-header">Header:</label> <label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required> <input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
@ -148,6 +148,7 @@ const loadMinterBoardPage = async () => {
await loadCards(minterCardIdentifierPrefix) await loadCards(minterCardIdentifierPrefix)
} }
const extractMinterCardsMinterName = async (cardIdentifier) => { const extractMinterCardsMinterName = async (cardIdentifier) => {
// Ensure the identifier starts with the prefix // Ensure the identifier starts with the prefix
if ((!cardIdentifier.startsWith(minterCardIdentifierPrefix)) && (!cardIdentifier.startsWith(addRemoveIdentifierPrefix))) { if ((!cardIdentifier.startsWith(minterCardIdentifierPrefix)) && (!cardIdentifier.startsWith(addRemoveIdentifierPrefix))) {
@ -197,7 +198,6 @@ const groupAndLabelByIdentifier = (allCards) => {
} }
mapById.get(card.identifier).push(card) mapById.get(card.identifier).push(card)
}) })
// For each identifier's group, sort oldest->newest so the first is "master" // For each identifier's group, sort oldest->newest so the first is "master"
const output = [] const output = []
for (const [identifier, group] of mapById.entries()) { for (const [identifier, group] of mapById.entries()) {
@ -206,14 +206,12 @@ const groupAndLabelByIdentifier = (allCards) => {
const bTime = b.created || 0 const bTime = b.created || 0
return aTime - bTime // oldest first return aTime - bTime // oldest first
}) })
// Mark the first as master // Mark the first as master
group[0].isMaster = true group[0].isMaster = true
// The rest are updates // The rest are updates
for (let i = 1; i < group.length; i++) { for (let i = 1; i < group.length; i++) {
group[i].isMaster = false group[i].isMaster = false
} }
// push them all to output // push them all to output
output.push(...group) output.push(...group)
} }
@ -221,7 +219,6 @@ const groupAndLabelByIdentifier = (allCards) => {
return output return output
} }
// no semicolons, using arrow functions
const groupByIdentifierOldestFirst = (allCards) => { const groupByIdentifierOldestFirst = (allCards) => {
// map of identifier => array of cards // map of identifier => array of cards
const mapById = new Map() const mapById = new Map()
@ -232,7 +229,6 @@ const groupByIdentifierOldestFirst = (allCards) => {
} }
mapById.get(card.identifier).push(card) mapById.get(card.identifier).push(card)
}) })
// sort each group oldest->newest // sort each group oldest->newest
for (const [identifier, group] of mapById.entries()) { for (const [identifier, group] of mapById.entries()) {
group.sort((a, b) => { group.sort((a, b) => {
@ -245,22 +241,18 @@ const groupByIdentifierOldestFirst = (allCards) => {
return mapById return mapById
} }
// no semicolons, arrow functions
const buildMinterNameGroups = async (mapById) => { const buildMinterNameGroups = async (mapById) => {
// We'll build an array of objects: { minterName, cards } // We'll build an array of objects: { minterName, cards }
// Then we can combine any that share the same minterName. // Then we can combine any that share the same minterName.
const nameGroups = [] const nameGroups = []
for (let [identifier, group] of mapById.entries()) { for (let [identifier, group] of mapById.entries()) {
// group[0] is the oldest => "master" card // group[0] is the oldest => "master" card
let masterCard = group[0] let masterCard = group[0]
// Filter out any cards that are not published by the 'masterPublisher' // Filter out any cards that are not published by the 'masterPublisher'
const masterPublisherName = masterCard.name const masterPublisherName = masterCard.name
// Remove any cards in this identifier group that have a different publisherName // Remove any cards in this identifier group that have a different publisherName
const filteredGroup = group.filter(c => c.name === masterPublisherName) const filteredGroup = group.filter(c => c.name === masterPublisherName)
// If filtering left zero cards, skip entire group // If filtering left zero cards, skip entire group
if (!filteredGroup.length) { if (!filteredGroup.length) {
console.warn(`All cards removed for identifier=${identifier} (different publishers). Skipping.`) console.warn(`All cards removed for identifier=${identifier} (different publishers). Skipping.`)
@ -269,7 +261,6 @@ const buildMinterNameGroups = async (mapById) => {
// Reassign group to the filtered version, then re-define masterCard // Reassign group to the filtered version, then re-define masterCard
group = filteredGroup group = filteredGroup
masterCard = group[0] // oldest after filtering masterCard = group[0] // oldest after filtering
// attempt to obtain minterName from the master card // attempt to obtain minterName from the master card
let masterMinterName let masterMinterName
try { try {
@ -278,14 +269,12 @@ const buildMinterNameGroups = async (mapById) => {
console.warn(`Skipping entire group ${identifier}, no valid minterName from master`, err) console.warn(`Skipping entire group ${identifier}, no valid minterName from master`, err)
continue continue
} }
// Store an object with the minterName we extracted, plus all cards in that group // Store an object with the minterName we extracted, plus all cards in that group
nameGroups.push({ nameGroups.push({
minterName: masterMinterName, minterName: masterMinterName,
cards: group // includes the master & updates cards: group // includes the master & updates
}) })
} }
// Combine them: minterName => array of *all* cards from all matching groups // Combine them: minterName => array of *all* cards from all matching groups
const combinedMap = new Map() const combinedMap = new Map()
for (const entry of nameGroups) { for (const entry of nameGroups) {
@ -373,7 +362,6 @@ const processARBoardCards = async (allValidCards) => {
return finalOutput return finalOutput
} }
//Main function to load the Minter Cards ---------------------------------------- //Main function to load the Minter Cards ----------------------------------------
const loadCards = async (cardIdentifierPrefix) => { const loadCards = async (cardIdentifierPrefix) => {
const cardsContainer = document.getElementById("cards-container") const cardsContainer = document.getElementById("cards-container")
@ -452,9 +440,11 @@ const loadCards = async (cardIdentifierPrefix) => {
// else 'newest' => do nothing (already sorted newest-first by your process functions). // else 'newest' => do nothing (already sorted newest-first by your process functions).
// Create the 'finalCardsArray' that includes the data, etc. // Create the 'finalCardsArray' that includes the data, etc.
let finalCardsArray = [] let finalCardsArray = []
cardsContainer.innerHTML = ''
for (const card of finalCards) { for (const card of finalCards) {
try { try {
const skeletonHTML = createSkeletonCardHTML(card.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
const cardDataResponse = await qortalRequest({ const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
name: card.name, name: card.name,
@ -465,6 +455,7 @@ const loadCards = async (cardIdentifierPrefix) => {
if (!cardDataResponse || !cardDataResponse.poll) { if (!cardDataResponse || !cardDataResponse.poll) {
// skip // skip
console.warn(`Skipping card: missing data/poll. identifier=${card.identifier}`) console.warn(`Skipping card: missing data/poll. identifier=${card.identifier}`)
removeSkeleton(card.identifier)
continue continue
} }
// Extra validation: check poll ownership matches card publisher // Extra validation: check poll ownership matches card publisher
@ -472,13 +463,22 @@ const loadCards = async (cardIdentifierPrefix) => {
const cardPublisherAddress = await fetchOwnerAddressFromName(card.name) const cardPublisherAddress = await fetchOwnerAddressFromName(card.name)
if (pollPublisherAddress !== cardPublisherAddress) { if (pollPublisherAddress !== cardPublisherAddress) {
console.warn(`Poll hijack attack found, discarding card ${card.identifier}`) console.warn(`Poll hijack attack found, discarding card ${card.identifier}`)
removeSkeleton(card.identifier)
continue continue
} }
// If ARBoard, do a quick address check // If ARBoard, do a quick address check
if (isARBoard) { if (isARBoard) {
const ok = await verifyARBoardAddress(cardDataResponse.minterName) const ok = await verifyMinter(cardDataResponse.minterName)
if (!ok) { if (!ok) {
console.warn(`Invalid minter address for AR board. identifier=${card.identifier}`) console.warn(`Card is not a minter nor an admin, not including in ARBoard. identifier: ${card.identifier}`)
removeSkeleton(card.identifier)
continue
}
} else {
const isAlreadyMinter = await verifyMinter(cardDataResponse.minterName)
if (isAlreadyMinter) {
console.warn(`card IS ALREADY a minter, NOT displaying following identifier on the MinterBoard: ${card.identifier}`)
removeSkeleton(card.identifier)
continue continue
} }
} }
@ -491,18 +491,16 @@ const loadCards = async (cardIdentifierPrefix) => {
}) })
} catch (err) { } catch (err) {
console.error(`Error preparing card ${card.identifier}`, err) console.error(`Error preparing card ${card.identifier}`, err)
removeSkeleton(card.identifier)
} }
} }
// This will now allow duplicates to be displayed, but also mark the duplicates correctly. There is one bugged identifier that must be handled opposite.
// finalCardsArray = markDuplicates(finalCardsArray)
// Next, do the actual rendering: // Next, do the actual rendering:
cardsContainer.innerHTML = "" // cardsContainer.innerHTML = ""
for (const cardObj of finalCardsArray) { for (const cardObj of finalCardsArray) {
// Insert a skeleton first if you like // Insert a skeleton first if you like
const skeletonHTML = createSkeletonCardHTML(cardObj.identifier) // const skeletonHTML = createSkeletonCardHTML(cardObj.identifier)
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) // cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
// Build final HTML // Build final HTML
const pollResults = await fetchPollResults(cardObj.cardDataResponse.poll) const pollResults = await fetchPollResults(cardObj.cardDataResponse.poll)
const commentCount = await countComments(cardObj.identifier) const commentCount = await countComments(cardObj.identifier)
@ -539,7 +537,7 @@ const loadCards = async (cardIdentifierPrefix) => {
} }
} }
const verifyARBoardAddress = async (minterName) => { const verifyMinter = async (minterName) => {
try { try {
const nameInfo = await getNameInfo(minterName) const nameInfo = await getNameInfo(minterName)
@ -557,7 +555,7 @@ const verifyARBoardAddress = async (minterName) => {
return (minterGroupAddresses.includes(minterAddress) || return (minterGroupAddresses.includes(minterAddress) ||
adminGroupAddresses.includes(minterAddress)) adminGroupAddresses.includes(minterAddress))
} catch (err) { } catch (err) {
console.warn("verifyARBoardAddress error:", err) console.warn("verifyMinter error:", err)
return false return false
} }
} }
@ -781,6 +779,7 @@ const publishCard = async (cardIdentifierPrefix) => {
content, content,
links, links,
creator: userState.accountName, creator: userState.accountName,
creatorAddress: userState.accountAddress,
timestamp: Date.now(), timestamp: Date.now(),
poll: pollName, poll: pollName,
} }
@ -812,7 +811,7 @@ const publishCard = async (cardIdentifierPrefix) => {
} }
if (isExistingCard){ if (isExistingCard){
alert("Card Updated Successfully! (No poll updates are possible at this time...)") alert("Card Updated Successfully! (No poll updates possible)")
isExistingCard = false isExistingCard = false
} }
@ -1058,7 +1057,7 @@ const buildVotersTableHtml = (voters, tableColor) => {
} }
// Post a comment on a card. --------------------------------- // Post a comment on a card. ---------------------------------
const postComment = async (cardIdentifier) => { const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`) const commentInput = document.getElementById(`new-comment-${cardIdentifier}`)
const commentText = commentInput.value.trim() const commentText = commentInput.value.trim()
@ -1067,34 +1066,44 @@ const postComment = async (cardIdentifier) => {
alert('Comment cannot be empty!') alert('Comment cannot be empty!')
return return
} }
const commentData = {
content: commentText,
creator: userState.accountName,
timestamp: Date.now(),
}
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`
try { try {
const base64CommentData = await objectToBase64(commentData) //Ensure the user is not on the blockList prior to allowing them to publish a comment.
const blockedNames = await fetchBlockList()
if (blockedNames.includes(userState.accountName)) {
alert('You are on the block list and cannot publish comments.')
return
}
const commentData = {
content: commentText,
creator: userState.accountName,
timestamp: Date.now(),
}
const uniqueCommentIdentifier = `comment-${cardIdentifier}-${await uid()}`
let base64CommentData = await objectToBase64(commentData)
if (!base64CommentData) { if (!base64CommentData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`) console.log('objectToBase64 failed, fallback to btoa()')
base64CommentData = btoa(JSON.stringify(commentData)) base64CommentData = btoa(JSON.stringify(commentData))
} }
await qortalRequest({ await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE', action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName, name: userState.accountName,
service: 'BLOG_POST', service: 'BLOG_POST',
identifier: commentIdentifier, identifier: uniqueCommentIdentifier,
data64: base64CommentData, data64: base64CommentData,
}) })
commentInput.value = '' commentInput.value = ''
} catch (error) { } catch (error) {
console.error('Error posting comment:', error) console.error('Error posting comment:', error)
alert('Failed to post comment.') alert('Failed to post comment. Error: ' + error)
} }
} }
//Fetch the comments for a card with passed card identifier ---------------------------- //Fetch the comments for a card with passed card identifier ----------------------------
const fetchCommentsForCard = async (cardIdentifier) => { const fetchCommentsForCard = async (cardIdentifier) => {
try { try {
@ -1110,9 +1119,9 @@ const displayComments = async (cardIdentifier) => {
try { try {
const comments = await fetchCommentsForCard(cardIdentifier) const comments = await fetchCommentsForCard(cardIdentifier)
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`) const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`)
commentsContainer.innerHTML = ""
commentsContainer.innerHTML = '' const blockedNames = await fetchBlockList()
console.log("Loaded block list:", blockedNames)
const voterMap = globalVoterMap.get(cardIdentifier) || new Map() const voterMap = globalVoterMap.get(cardIdentifier) || new Map()
const commentHTMLArray = await Promise.all( const commentHTMLArray = await Promise.all(
@ -1122,33 +1131,38 @@ const displayComments = async (cardIdentifier) => {
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
name: comment.name, name: comment.name,
service: "BLOG_POST", service: "BLOG_POST",
identifier: comment.identifier, identifier: comment.identifier
}) })
const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp); if (!commentDataResponse || !commentDataResponse.creator) {
return null
const commenter = commentDataResponse.creator }
const voterInfo = voterMap.get(commenter) const commenterName = commentDataResponse.creator
const voterInfo = voterMap.get(commenterName)
let commentColor = "transparent" let commentColor = "transparent"
let adminBadge = "" let adminBadge = ""
if (blockedNames.includes(commenterName)) {
console.warn(`Skipping blocked commenter: ${commenterName}`)
return null
}
if (voterInfo) { if (voterInfo) {
if (voterInfo.voterType === "Admin") { if (voterInfo.voterType === "Admin") {
commentColor = voterInfo.vote === "yes" ? "rgba(21, 150, 21, 0.6)" : "rgba(212, 37, 64, 0.6)" // Light green for yes, light red for no commentColor = voterInfo.vote === "yes" ? "rgba(21, 150, 21, 0.6)" : "rgba(212, 37, 64, 0.6)" // Light green for yes, light red for no
const badgeColor = voterInfo.vote === "yes" ? "green" : "red" const badgeColor = voterInfo.vote === "yes" ? "rgb(206, 195, 77)" : "rgb(121, 119, 90)"
adminBadge = `<span style="color: ${badgeColor}; font-weight: bold; margin-left: 0.5em;">(Admin)</span>` adminBadge = `<span style="color: ${badgeColor}; font-weight: bold; margin-left: 0.5em;">(Admin)</span>`
} else { } else {
commentColor = voterInfo.vote === "yes" ? "rgba(0, 100, 0, 0.3)" : "rgba(100, 0, 0, 0.3)" // Darker green for yes, darker red for no commentColor = voterInfo.vote === "yes" ? "rgba(0, 100, 0, 0.3)" : "rgba(100, 0, 0, 0.3)" // Darker green for yes, darker red for no
} }
} }
const timestamp = new Date(commentDataResponse.timestamp).toLocaleString()
return ` return `
<div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: ${commentColor};"> <div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: ${commentColor};">
<p> <p>
<strong><u>${commentDataResponse.creator}</u></strong> <strong>${commenterName}</strong>
${adminBadge} ${adminBadge}
</p> </p>
<p>${commentDataResponse.content}</p> <p>${commentDataResponse.content}</p>
@ -1156,22 +1170,23 @@ const displayComments = async (cardIdentifier) => {
</div> </div>
` `
} catch (err) { } catch (err) {
console.error(`Error processing comment ${comment.identifier}:`, err) console.error(`Error with comment ${comment.identifier}:`, err)
return null return null
} }
}) })
) )
commentHTMLArray commentHTMLArray
.filter((html) => html !== null) // Filter out failed comments .filter(html => html !== null)
.forEach((commentHTML) => { .forEach(commentHTML => {
commentsContainer.insertAdjacentHTML('beforeend', commentHTML) commentsContainer.insertAdjacentHTML('beforeend', commentHTML)
}) })
} catch (error) {
console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error) } catch (err) {
console.error(`Error displaying comments for ${cardIdentifier}:`, err)
} }
} }
// Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled -------------------- // Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled --------------------
const toggleComments = async (cardIdentifier) => { const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`) const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`)
@ -1206,12 +1221,10 @@ const countComments = async (cardIdentifier) => {
} }
const createModal = (modalType='') => { const createModal = (modalType='') => {
if (document.getElementById(`${modalType}-modal`)) { if (document.getElementById(`${modalType}-modal`)) {
return return
} }
const isIframe = (modalType === 'links') const isIframe = (modalType === 'links')
const modalHTML = ` const modalHTML = `
@ -1259,8 +1272,8 @@ const createModal = (modalType='') => {
</div> </div>
` `
document.body.insertAdjacentHTML('beforeend', modalHTML) document.body.insertAdjacentHTML('beforeend', modalHTML)
const modal = document.getElementById(`${modalType}-modal`) const modal = document.getElementById(`${modalType}-modal`)
window.addEventListener('click', (event) => { window.addEventListener('click', (event) => {
if (event.target === modal) { if (event.target === modal) {
closeModal(modalType) closeModal(modalType)
@ -1268,7 +1281,6 @@ const createModal = (modalType='') => {
}) })
} }
const openLinksModal = async (link) => { const openLinksModal = async (link) => {
const processedLink = await processLink(link) const processedLink = await processLink(link)
const modal = document.getElementById('links-modal') const modal = document.getElementById('links-modal')
@ -1311,9 +1323,9 @@ const togglePollDetails = (cardIdentifier) => {
if (!detailsDiv || !modal || !modalContent) return if (!detailsDiv || !modal || !modalContent) return
// modalContent.appendChild(detailsDiv)
modalContent.innerHTML = detailsDiv.innerHTML modalContent.innerHTML = detailsDiv.innerHTML
modal.style.display = 'block' modal.style.display = 'block'
window.onclick = (event) => { window.onclick = (event) => {
if (event.target === modal) { if (event.target === modal) {
modal.style.display = 'none' modal.style.display = 'none'
@ -1394,10 +1406,17 @@ const handleInviteMinter = async (minterName) => {
} }
} }
const escapeHTML = (str) => {
return str
.replace(/'/g, '&#39;')
.replace(/"/g, '&quot;')
}
const createInviteButtonHtml = (creator, cardIdentifier) => { const createInviteButtonHtml = (creator, cardIdentifier) => {
const escapedCreator = escapeHTML(creator)
return ` return `
<div id="invite-button-container-${cardIdentifier}" style="margin-top: 1em;"> <div id="invite-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button onclick="handleInviteMinter('${creator}')" <button onclick="handleInviteMinter('${escapedCreator}')"
style="padding: 10px; background:rgb(0, 109, 76) ; color: white; border: dotted; border-color: white; cursor: pointer; border-radius: 5px;" 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) '" onmouseover="this.style.backgroundColor='rgb(25, 47, 39) '"
onmouseout="this.style.backgroundColor='rgba(7, 122, 101, 0.63) '" onmouseout="this.style.backgroundColor='rgba(7, 122, 101, 0.63) '"
@ -1423,73 +1442,59 @@ const featureTriggerCheck = async () => {
} }
const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => { const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => {
const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
if (!userState.isMinterAdmin){
console.warn(`User is NOT an admin, not displaying invite/approve button...`)
return null
}
const isBlockPassed = await featureTriggerCheck() const isBlockPassed = await featureTriggerCheck()
const minterAdmins = await fetchMinterGroupAdmins() const minterAdmins = await fetchMinterGroupAdmins()
// default needed admin count = 9, or 40% if block has passed
let minAdminCount = 9 let minAdminCount = 9
if (isBlockPassed) { if (isBlockPassed) {
minAdminCount = Math.round(minterAdmins.length * 0.4) minAdminCount = Math.ceil(minterAdmins.length * 0.4)
console.warn(`Using 40% => ${minAdminCount}`) console.warn(`Using 40% => ${minAdminCount}`)
} }
// if not enough adminYes votes, no invite button
if (adminYes < minAdminCount) { if (adminYes < minAdminCount) {
console.warn(`Admin votes not high enough (have=${adminYes}, need=${minAdminCount}). No button.`) console.warn(`Admin votes not high enough (have=${adminYes}, need=${minAdminCount}). No button.`)
return null return null
} }
console.log(`passed initial button creation checks, pulling additional data...`) console.log(`passed initial button creation checks (adminYes >= ${minAdminCount})`)
// get user's address from 'creator' name
const minterNameInfo = await getNameInfo(creator) const minterNameInfo = await getNameInfo(creator)
const minterAddress = await minterNameInfo.owner if (!minterNameInfo || !minterNameInfo.owner) {
console.warn(`No valid nameInfo for ${creator}, skipping invite button.`)
return null
}
const minterAddress = minterNameInfo.owner
// fetch all final KICK/BAN tx
const { finalKickTxs, finalBanTxs } = await fetchAllKickBanTxData()
// check if there's a final (non-pending) KICK or BAN for this user
const priorKick = finalKickTxs.some(tx => tx.member === minterAddress)
const priorBan = finalBanTxs.some(tx => tx.offender === minterAddress)
const priorBanOrKick = (priorBan || priorKick)
console.warn(`PriorBanOrKick determination for ${minterAddress}:`, priorBanOrKick)
const previousBanTx = await searchTransactions({ // build the normal invite button & groupApprovalHtml
txTypes: ['GROUP_BAN'], const inviteButtonHtml = isSomeTypaAdmin ? createInviteButtonHtml(creator, cardIdentifier) : ""
address: `${minterAddress}`,
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 0,
})
const previousBan = previousBanTx.filter((tx) => tx.approvalStatus !== 'PENDING')
const previousKickTx = await searchTransactions({
txTypes: ['GROUP_KICK'],
address: `${minterAddress}`,
confirmationStatus: 'CONFIRMED',
limit: 0,
reverse: true,
offset: 0,
startBlock: 1990000,
blockLimit: 0,
txGroupId: 0,
})
const previousKick = previousKickTx.filter((tx) => tx.approvalStatus !== 'PENDING')
const priorBanOrKick = (previousKick.length > 0 || previousBan.length > 0)
console.warn(`PriorBanOrKick determination:`, priorBanOrKick)
const inviteButtonHtml = createInviteButtonHtml(creator, cardIdentifier)
const groupApprovalHtml = await checkGroupApprovalAndCreateButton(minterAddress, cardIdentifier, "GROUP_INVITE") const groupApprovalHtml = await checkGroupApprovalAndCreateButton(minterAddress, cardIdentifier, "GROUP_INVITE")
// if user had no prior KICK/BAN
if (!priorBanOrKick) { if (!priorBanOrKick) {
console.log(`No prior kick/ban found, creating invite (or approve) button...` ) console.log(`No prior kick/ban found, creating invite (or approve) button...`)
console.warn(`Existing Numbers - adminYes/minAdminCount: ${adminYes}/${minAdminCount}`) console.warn(`Existing Numbers - adminYes/minAdminCount: ${adminYes}/${minAdminCount}`)
if (groupApprovalHtml){
// if there's already a pending GROUP_INVITE, return that approval button
if (groupApprovalHtml) {
console.warn(`groupApprovalCheck found existing groupApproval, returning approval button instead of invite button...`) console.warn(`groupApprovalCheck found existing groupApproval, returning approval button instead of invite button...`)
return groupApprovalHtml return groupApprovalHtml
} }
console.warn(`No pending approvals or prior kick/ban found, but votes are high enough, returning invite button...`)
console.warn(`No pending approvals or prior kick/ban found, returning invite button...`)
return inviteButtonHtml return inviteButtonHtml
} else if (priorBanOrKick){ } else {
console.warn(`Prior kick/ban found! Including BOTH buttons (due to complexities in checking, displaying both buttons is simpler than attempting to display only one)...`) // priorBanOrKick is true => show both
console.warn(`Prior kick/ban found! Including BOTH buttons...`)
return inviteButtonHtml + groupApprovalHtml return inviteButtonHtml + groupApprovalHtml
} }
} }
@ -1540,12 +1545,13 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
blockLimit: 0, blockLimit: 0,
txGroupId: 0 txGroupId: 0
}) })
const pendingApprovals = await findPendingApprovalTxForAddress(address, transactionType); const pendingApprovals = await findPendingApprovalTxForAddress(address, transactionType)
let isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
// If no pending transaction found, return null // If no pending transaction found, return null
if (!pendingApprovals || pendingApprovals.length === 0) { if (!pendingApprovals || pendingApprovals.length === 0) {
console.warn("no pending approval transactions found, returning null...") console.warn("no pending approval transactions found, returning null...")
return null; return null
} }
const txSig = pendingApprovals[0].signature const txSig = pendingApprovals[0].signature
// Among the already-confirmed GROUP_APPROVAL, filter for those referencing this txSig // Among the already-confirmed GROUP_APPROVAL, filter for those referencing this txSig
@ -1557,7 +1563,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
getNameFromAddress getNameFromAddress
) )
if (transactionType === "GROUP_INVITE") { if (transactionType === "GROUP_INVITE" && isSomeTypaAdmin) {
const approvalButtonHtml = ` const approvalButtonHtml = `
<div style="display: flex; flex-direction: column; margin-top: 1em;"> <div style="display: flex; flex-direction: column; margin-top: 1em;">
<p style="color: rgb(181, 214, 100);"> <p style="color: rgb(181, 214, 100);">
@ -1587,7 +1593,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
return approvalButtonHtml return approvalButtonHtml
} }
if (transactionType === "GROUP_KICK") { if (transactionType === "GROUP_KICK" && isSomeTypaAdmin) {
const approvalButtonHtml = ` const approvalButtonHtml = `
<div style="display: flex; flex-direction: column; margin-top: 1em;"> <div style="display: flex; flex-direction: column; margin-top: 1em;">
<p style="color: rgb(199, 100, 64);"> <p style="color: rgb(199, 100, 64);">
@ -1617,7 +1623,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
return approvalButtonHtml return approvalButtonHtml
} }
if (transactionType === "GROUP_BAN") { if (transactionType === "GROUP_BAN" && isSomeTypaAdmin) {
const approvalButtonHtml = ` const approvalButtonHtml = `
<div style="display: flex; flex-direction: column; margin-top: 1em;"> <div style="display: flex; flex-direction: column; margin-top: 1em;">
<p style="color: rgb(189, 40, 40);"> <p style="color: rgb(189, 40, 40);">
@ -1647,7 +1653,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
return approvalButtonHtml return approvalButtonHtml
} }
if (transactionType === "ADD_GROUP_ADMIN") { if (transactionType === "ADD_GROUP_ADMIN" && isSomeTypaAdmin) {
const approvalButtonHtml = ` const approvalButtonHtml = `
<div style="display: flex; flex-direction: column; margin-top: 1em;"> <div style="display: flex; flex-direction: column; margin-top: 1em;">
<p style="color: rgb(40, 144, 189);"> <p style="color: rgb(40, 144, 189);">
@ -1677,7 +1683,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
return approvalButtonHtml return approvalButtonHtml
} }
if (transactionType === "REMOVE_GROUP_ADMIN") { if (transactionType === "REMOVE_GROUP_ADMIN" && isSomeTypaAdmin) {
const approvalButtonHtml = ` const approvalButtonHtml = `
<div style="display: flex; flex-direction: column; margin-top: 1em;"> <div style="display: flex; flex-direction: column; margin-top: 1em;">
<p style="color: rgb(189, 40, 40);"> <p style="color: rgb(189, 40, 40);">
@ -1709,7 +1715,7 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
} }
async function buildApprovalTableHtml(approvalTxs, getNameFunc) { const buildApprovalTableHtml = async (approvalTxs, getNameFunc) => {
// Build a Map of adminAddress => one transaction (to handle multiple approvals from same admin) // Build a Map of adminAddress => one transaction (to handle multiple approvals from same admin)
const approvalMap = new Map() const approvalMap = new Map()
for (const tx of approvalTxs) { for (const tx of approvalTxs) {
@ -1718,10 +1724,8 @@ async function buildApprovalTableHtml(approvalTxs, getNameFunc) {
approvalMap.set(adminAddr, tx) approvalMap.set(adminAddr, tx)
} }
} }
// Turn the map into an array for iteration // Turn the map into an array for iteration
const approvalArray = Array.from(approvalMap, ([adminAddr, tx]) => ({ adminAddr, tx })) const approvalArray = Array.from(approvalMap, ([adminAddr, tx]) => ({ adminAddr, tx }))
// Build table rows asynchronously, since we need getNameFromAddress // Build table rows asynchronously, since we need getNameFromAddress
const tableRows = await Promise.all( const tableRows = await Promise.all(
approvalArray.map(async ({ adminAddr, tx }) => { approvalArray.map(async ({ adminAddr, tx }) => {
@ -1732,15 +1736,12 @@ async function buildApprovalTableHtml(approvalTxs, getNameFunc) {
console.warn(`Error fetching name for ${adminAddr}:`, err) console.warn(`Error fetching name for ${adminAddr}:`, err)
adminName = null adminName = null
} }
const displayName = const displayName =
adminName && adminName !== adminAddr adminName && adminName !== adminAddr
? adminName ? adminName
: "(No registered name)" : "(No registered name)"
// Format the transaction timestamp
const dateStr = new Date(tx.timestamp).toLocaleString() const dateStr = new Date(tx.timestamp).toLocaleString()
return ` return `
<tr> <tr>
<td style="border: 1px solid rgb(255, 255, 255); padding: 4px; color: #234565">${displayName}</td> <td style="border: 1px solid rgb(255, 255, 255); padding: 4px; color: #234565">${displayName}</td>
@ -1749,11 +1750,9 @@ async function buildApprovalTableHtml(approvalTxs, getNameFunc) {
` `
}) })
) )
// The total unique approvals = number of entries in approvalMap // The total unique approvals = number of entries in approvalMap
const uniqueApprovalCount = approvalMap.size; const uniqueApprovalCount = approvalMap.size;
// Wrap the table in a container with horizontal scroll:
// 4) Wrap the table in a container with horizontal scroll:
// 1) max-width: 100% makes it fit the parent (card) width // 1) max-width: 100% makes it fit the parent (card) width
// 2) overflow-x: auto allows scrolling if the table is too wide // 2) overflow-x: auto allows scrolling if the table is too wide
const containerHtml = ` const containerHtml = `
@ -1771,7 +1770,6 @@ async function buildApprovalTableHtml(approvalTxs, getNameFunc) {
</table> </table>
</div> </div>
` `
// Return both the container-wrapped table and the count of unique approvals // Return both the container-wrapped table and the count of unique approvals
return { return {
tableHtml: containerHtml, tableHtml: containerHtml,
@ -1887,7 +1885,7 @@ const getNewestCommentTimestamp = async (cardIdentifier) => {
// Create the overall Minter Card HTML ----------------------------------------------- // Create the overall Minter Card HTML -----------------------------------------------
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, address) => { const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, address) => {
const { header, content, links, creator, timestamp, poll } = cardData const { header, content, links, creator, creatorAddress, timestamp, poll } = cardData
const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString() const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
const avatarHtml = await getMinterAvatar(creator) const avatarHtml = await getMinterAvatar(creator)
const linksHTML = links.map((link, index) => ` const linksHTML = links.map((link, index) => `
@ -1915,9 +1913,9 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
const invites = await fetchGroupInvitesByAddress(address) const invites = await fetchGroupInvitesByAddress(address)
const hasMinterInvite = invites.some((invite) => invite.groupId === 694) const hasMinterInvite = invites.some((invite) => invite.groupId === 694)
if (userVote === 0) { if (userVote === 0) {
finalBgColor = "rgba(0, 192, 0, 0.3)"; // or any green you want finalBgColor = "rgba(1, 65, 39, 0.41)"; // or any green you want
} else if (userVote === 1) { } else if (userVote === 1) {
finalBgColor = "rgba(192, 0, 0, 0.3)"; // or any red you want finalBgColor = "rgba(107, 3, 3, 0.3)"; // or any red you want
} else if (hasMinterInvite) { } else if (hasMinterInvite) {
// If so, override background color & add an "INVITED" label // If so, override background color & add an "INVITED" label
finalBgColor = "black"; finalBgColor = "black";
@ -1926,7 +1924,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
inviteHtmlAdd = ` inviteHtmlAdd = `
<div id="join-button-container-${cardIdentifier}" style="margin-top: 1em;"> <div id="join-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button <button
style="padding: 8px; background:rgb(37, 99, 44); color:rgb(240, 240, 240); border: 1px solid rgb(255, 255, 255); border-radius: 5px; cursor: pointer;" style="padding: 8px; background: rgb(37, 99, 44); color:rgb(240, 240, 240); border: 1px solid rgb(255, 255, 255); border-radius: 5px; cursor: pointer;"
onmouseover="this.style.backgroundColor='rgb(25, 47, 39) '" onmouseover="this.style.backgroundColor='rgb(25, 47, 39) '"
onmouseout="this.style.backgroundColor='rgb(37, 99, 44) '" onmouseout="this.style.backgroundColor='rgb(37, 99, 44) '"
onclick="handleJoinGroup('${userState.accountAddress}')"> onclick="handleJoinGroup('${userState.accountAddress}')">

View File

@ -1,3 +1,5 @@
const Q_MINTERSHIP_VERSION = "1.05"
const messageIdentifierPrefix = `mintership-forum-message` const messageIdentifierPrefix = `mintership-forum-message`
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment` const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`
@ -66,6 +68,10 @@ if (localStorage.getItem("latestMessageIdentifiers")) {
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
console.log("DOMContentLoaded fired!") console.log("DOMContentLoaded fired!")
createScrollToTopButton()
document.querySelectorAll(".version").forEach(el => {
el.textContent = `Q-Mintership (v${Q_MINTERSHIP_VERSION}b)`
})
// --- GENERAL LINKS (MINTERSHIP-FORUM and MINTER-BOARD) --- // --- GENERAL LINKS (MINTERSHIP-FORUM and MINTER-BOARD) ---
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]') const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]')
@ -94,7 +100,6 @@ document.addEventListener("DOMContentLoaded", async () => {
await loadScript("./assets/js/MinterBoard.js") await loadScript("./assets/js/MinterBoard.js")
} }
await loadMinterBoardPage() await loadMinterBoardPage()
createScrollToTopButton()
}) })
}) })

View File

@ -53,7 +53,7 @@ const timestampToHumanReadableDate = async(timestamp) => {
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0');
const formattedDate = `${year}.${month}.${day}@${hours}:${minutes}:${seconds}` const formattedDate = `${day}.${month}.${year}..@${hours}:${minutes}:${seconds}`
console.log('Formatted date:', formattedDate) console.log('Formatted date:', formattedDate)
return formattedDate return formattedDate
} }
@ -146,7 +146,7 @@ const base64ToUint8Array = async (base64) => {
} }
return bytes return bytes
} }
const uint8ArrayToObject = async (uint8Array) => { const uint8ArrayToObject = async (uint8Array) => {
// Decode the byte array using TextDecoder // Decode the byte array using TextDecoder
@ -157,7 +157,7 @@ const uint8ArrayToObject = async (uint8Array) => {
const obj = JSON.parse(jsonString) const obj = JSON.parse(jsonString)
return obj return obj
} }
const objectToBase64 = async (obj) => { const objectToBase64 = async (obj) => {
@ -331,7 +331,7 @@ const getNameInfo = async (name) => {
console.log('getNameInfo called') console.log('getNameInfo called')
console.log('name:', name) console.log('name:', name)
try { try {
const response = await fetch(`${baseUrl}/names/${name}`) const response = await fetch(`${baseUrl}/names/${encodeURIComponent(name)}`)
if (!response.ok) { if (!response.ok) {
console.warn(`Failed to fetch name info for: ${name}, status: ${response.status}`) console.warn(`Failed to fetch name info for: ${name}, status: ${response.status}`)
@ -438,17 +438,17 @@ const login = async () => {
} }
const getNameFromAddress = async (address) => { const getNameFromAddress = async (address) => {
try { try {
const response = await fetch(`${baseUrl}/names/address/${address}?limit=20`, { const response = await fetch(`${baseUrl}/names/address/${address}?limit=20`, {
method: 'GET', method: 'GET',
headers: { 'Accept': 'application/json' } headers: { 'Accept': 'application/json' }
}) })
const names = await response.json() const names = await response.json()
return names.length > 0 ? names[0].name : address // Return name if found, else return address return names.length > 0 ? names[0].name : address // Return name if found, else return address
} catch (error) { } catch (error) {
console.error(`Error fetching names for address ${address}:`, error) console.error(`Error fetching names for address ${address}:`, error)
return address return address
} }
} }
@ -491,28 +491,6 @@ const fetchMinterGroupAdmins = async () => {
//use what is returned .member to obtain each member... {"member": "memberAddress", "isAdmin": "true"} //use what is returned .member to obtain each member... {"member": "memberAddress", "isAdmin": "true"}
} }
// const fetchAllAdminGroupsMembers = async () => {
// try {
// let adminGroupMemberAddresses = [] // Declare outside loop to accumulate results
// for (const groupID of adminGroupIDs) {
// const response = await fetch(`${baseUrl}/groups/members/${groupID}?limit=0`, {
// method: 'GET',
// headers: { 'Accept': 'application/json' },
// })
// const groupData = await response.json()
// if (groupData.members && Array.isArray(groupData.members)) {
// adminGroupMemberAddresses.push(...groupData.members) // Merge members into the array
// } else {
// console.warn(`Group ${groupID} did not return valid members.`)
// }
// }
// return adminGroupMemberAddresses
// } catch (error) {
// console.log('Error fetching admin group members', error)
// }
// }
const fetchAllAdminGroupsMembers = async () => { const fetchAllAdminGroupsMembers = async () => {
try { try {
// We'll track addresses so we don't duplicate the same .member // We'll track addresses so we don't duplicate the same .member
@ -545,7 +523,7 @@ const fetchAllAdminGroupsMembers = async () => {
console.error('Error fetching admin group members', error) console.error('Error fetching admin group members', error)
return [] return []
} }
} }
const fetchMinterGroupMembers = async () => { const fetchMinterGroupMembers = async () => {
try { try {
@ -651,7 +629,7 @@ const fetchGroupInvitesByAddress = async (address) => {
console.error('Error fetching address group invites:', error) console.error('Error fetching address group invites:', error)
throw error throw error
} }
} }
// QDN data calls -------------------------------------------------------------------------------------------------- // QDN data calls --------------------------------------------------------------------------------------------------
const searchLatestDataByIdentifier = async (identifier) => { const searchLatestDataByIdentifier = async (identifier) => {
@ -1141,7 +1119,7 @@ const base64ToBlob = (base64String, mimeType) => {
} }
// Create a blob from the Uint8Array // Create a blob from the Uint8Array
return new Blob([bytes], { type: mimeType }) return new Blob([bytes], { type: mimeType })
} }
const base64ToBlobUrl = (base64, mimeType) => { const base64ToBlobUrl = (base64, mimeType) => {
const binary = atob(base64) const binary = atob(base64)
@ -1193,7 +1171,7 @@ const base64ToBlobUrl = (base64, mimeType) => {
console.error("Skipping file due to error in fetchEncryptedImageBase64:", error) console.error("Skipping file due to error in fetchEncryptedImageBase64:", error)
return null // indicates "missing or failed" return null // indicates "missing or failed"
} }
} }
@ -1363,7 +1341,7 @@ const processTransaction = async (signedTransaction) => {
console.error("Error processing transaction:", error) console.error("Error processing transaction:", error)
throw error throw error
} }
} }
// 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. // 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.
@ -1860,7 +1838,7 @@ const searchTransactions = async ({
console.error("Error in searchTransactions:", error) console.error("Error in searchTransactions:", error)
throw error throw error
} }
} }
const searchPendingTransactions = async (limit = 20, offset = 0) => { const searchPendingTransactions = async (limit = 20, offset = 0) => {
try { try {
@ -1891,5 +1869,5 @@ const searchPendingTransactions = async (limit = 20, offset = 0) => {
console.error("Error in searchPendingTransactions:", error) console.error("Error in searchPendingTransactions:", error)
throw error throw error
} }
} }

View File

@ -28,8 +28,14 @@
<link rel="preload" href="./assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'"> <link rel="preload" href="./assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="./assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap"></noscript> <noscript><link rel="stylesheet" href="./assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap"></noscript>
<link href="./assets/quill/quill.snow.css" rel="stylesheet"> <link href="./assets/quill/quill.snow.css" rel="stylesheet">
</head> </head>
<body> <body>
<script>
// Variable-based versioning (credit: QuickMythril)
; // Update here in the future
</script>
<section data-bs-version="5.1" class="menu menu1 boldm5 cid-ttRnktJ11Q" once="menu" id="menu1-0"> <section data-bs-version="5.1" class="menu menu1 boldm5 cid-ttRnktJ11Q" once="menu" id="menu1-0">
@ -42,7 +48,7 @@
</a> </a>
</span> </span>
<span class="navbar-caption-wrap"> <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 class="navbar-caption display-4 version"></span>
</a> </a>
</span> </span>
</div> </div>
@ -61,7 +67,7 @@
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt=""> <img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
</a> </a>
</span> </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 class="navbar-caption display-4 version"></span><br></a></span>
</div> </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> <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>
@ -84,7 +90,7 @@
<img src="assets/images/mbr-1623x1082.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1"> <img src="assets/images/mbr-1623x1082.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1">
<div class="item-content"> <div class="item-content">
<h2 class="card-title mbr-fonts-style display-2"> <h2 class="card-title mbr-fonts-style display-2">
Admin Board</h2> ADMIN BOARD</h2>
</div> </div>
</div> </div>
</a> </a>
@ -93,7 +99,7 @@
<div class="item-wrapper"> <div class="item-wrapper">
<img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0"> <img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
<div class="item-content"> <div class="item-content">
<h2 class="card-title mbr-fonts-style display-2">MinterBoard</h2> <h2 class="card-title mbr-fonts-style display-2">MINTER BOARD</h2>
</div> </div>
</div> </div>
</a> </a>
@ -109,7 +115,7 @@
<img src="assets/images/mbr-1818x1212.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1"> <img src="assets/images/mbr-1818x1212.jpg" alt="Admin Board" data-slide-to="1" data-bs-slide-to="1">
<div class="item-content"> <div class="item-content">
<h2 class="card-title mbr-fonts-style display-2"> <h2 class="card-title mbr-fonts-style display-2">
Minter Admin Management (MAM) Board</h2> MAM BOARD</h2>
</div> </div>
</div> </div>
</a> </a>
@ -118,7 +124,7 @@
<div class="item-wrapper"> <div class="item-wrapper">
<img src="assets/images/mbr-1-1818x1212.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0"> <img src="assets/images/mbr-1-1818x1212.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
<div class="item-content"> <div class="item-content">
<h2 class="card-title mbr-fonts-style display-2">Mintership Forum</h2> <h2 class="card-title mbr-fonts-style display-2">Q-MINTERSHIP FORUM</h2>
</div> </div>
</div> </div>
</a> </a>
@ -572,12 +578,12 @@
<div class="title-wrapper"> <div class="title-wrapper">
<div class="title-wrap"> <div class="title-wrap">
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt=""> <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 class="navbar-caption display-4 version"></span></h2>
</div> </div>
</div> </div>
<a class="link-wrap" href="#"> <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 class="navbar-caption display-4 version"></span></p>
</a> </a>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">
@ -590,7 +596,6 @@
</div> </div>
</section> </section>
<script src="./assets/bootstrap/js/bootstrap.bundle.min.js"></script> <script src="./assets/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="./assets/parallax/jarallax.js"></script> <script src="./assets/parallax/jarallax.js"></script>
<!-- <script src="./assets/smoothscroll/smooth-scroll.js"></script> --> <!-- <script src="./assets/smoothscroll/smooth-scroll.js"></script> -->
@ -598,12 +603,13 @@
<script src="./assets/dropdown/js/navbar-dropdown.js"></script> <script src="./assets/dropdown/js/navbar-dropdown.js"></script>
<script src="./assets/theme/js/script.js"></script> <script src="./assets/theme/js/script.js"></script>
<script src="./assets/quill/quill.min.js"></script> <script src="./assets/quill/quill.min.js"></script>
<!-- Order here MATTERS, we MUST load the scripts in the correct order so that all the functions will work properly -->
<script src="./assets/js/QortalApi.js"></script> <script src="./assets/js/QortalApi.js"></script>
<script src="./assets/js/Shared.js"></script>
<script src="./assets/js/MinterBoard.js"></script> <script src="./assets/js/MinterBoard.js"></script>
<script src="./assets/js/AdminTools.js"></script>
<script src="./assets/js/AdminBoard.js"></script> <script src="./assets/js/AdminBoard.js"></script>
<script src="./assets/js/ARBoard.js"></script> <script src="./assets/js/ARBoard.js"></script>
<script src="./assets/js/AdminTools.js"></script>
<script src="./assets/js/Q-Mintership.js"></script> <script src="./assets/js/Q-Mintership.js"></script>
<input name="animation" type="hidden"> <input name="animation" type="hidden">
</body> </body>