Massive refactoring to search/display code on multiple boards, and many other features. More changes coming soon.
This commit is contained in:
parent
10f8230619
commit
5a35fb0d07
@ -59,6 +59,13 @@ const loadAddRemoveAdminPage = async () => {
|
|||||||
<div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
<div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||||
<h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3>
|
<h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3>
|
||||||
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button>
|
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button>
|
||||||
|
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
|
||||||
|
<option value="0">Show All</option>
|
||||||
|
<option value="1">Last 1 day</option>
|
||||||
|
<option value="7">Last 7 days</option>
|
||||||
|
<option value="30" selected>Last 30 days</option>
|
||||||
|
<option value="90">Last 90 days</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="cards-container" class="cards-container" style="margin-top: 1rem"">
|
<div id="cards-container" class="cards-container" style="margin-top: 1rem"">
|
||||||
<!-- We'll fill this with existing proposal cards -->
|
<!-- We'll fill this with existing proposal cards -->
|
||||||
@ -80,8 +87,8 @@ const loadAddRemoveAdminPage = async () => {
|
|||||||
// proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
// proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking for existing card:", error)
|
console.error("Error opening propose form", error)
|
||||||
alert("Failed to check for existing card. Please try again.")
|
alert("Failed to open proposal form. Please try again.")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -126,79 +133,79 @@ const toggleProposeButton = () => {
|
|||||||
proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
||||||
}
|
}
|
||||||
|
|
||||||
let addAdminTxs
|
|
||||||
let remAdminTxs
|
|
||||||
|
|
||||||
const fetchAllARTxData = async () => {
|
const fetchAllARTxData = async () => {
|
||||||
const addAdmTx = "ADD_GROUP_ADMIN"
|
const addAdmTx = "ADD_GROUP_ADMIN"
|
||||||
const remAdmTx = "REMOVE_GROUP_ADMIN"
|
const remAdmTx = "REMOVE_GROUP_ADMIN"
|
||||||
|
|
||||||
const filterAddTransactions = (rawTransactions) => {
|
|
||||||
// Group transactions by member
|
|
||||||
const memberTxMap = rawTransactions.reduce((map, tx) => {
|
|
||||||
if (!map[tx.member]) {
|
|
||||||
map[tx.member] = []
|
|
||||||
}
|
|
||||||
map[tx.member].push(tx)
|
|
||||||
return map
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
// Filter out members with both pending and non-pending transactions
|
|
||||||
return Object.values(memberTxMap)
|
|
||||||
.filter(txs => txs.every(tx => tx.approvalStatus !== 'PENDING'))
|
|
||||||
.flat()
|
|
||||||
// .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') &&
|
|
||||||
// txs.some(tx => tx.approvalStatus !== 'PENDING')))
|
|
||||||
// .flat()
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterRemoveTransactions = (rawTransactions) => {
|
|
||||||
// Group transactions by member
|
|
||||||
const adminTxMap = rawTransactions.reduce((map, tx) => {
|
|
||||||
if (!map[tx.admin]) {
|
|
||||||
map[tx.admin] = []
|
|
||||||
}
|
|
||||||
map[tx.admin].push(tx)
|
|
||||||
return map
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
// Filter out members with both pending and non-pending transactions
|
|
||||||
return Object.values(adminTxMap)
|
|
||||||
.filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') &&
|
|
||||||
txs.some(tx => tx.approvalStatus !== 'PENDING')))
|
|
||||||
.flat()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch ban transactions
|
|
||||||
const allAddTxs = await searchTransactions({
|
const allAddTxs = await searchTransactions({
|
||||||
txTypes: [addAdmTx],
|
txTypes: [addAdmTx],
|
||||||
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: 694,
|
txGroupId: 694,
|
||||||
})
|
})
|
||||||
// Filter out 'PENDING'
|
|
||||||
addAdminTxs = filterAddTransactions(allAddTxs)
|
const allRemTxs = await searchTransactions({
|
||||||
console.warn('addAdminTxData (no PENDING nor past+PENDING):', addAdminTxs)
|
txTypes: [remAdmTx],
|
||||||
|
confirmationStatus: 'CONFIRMED',
|
||||||
|
limit: 0,
|
||||||
|
reverse: true,
|
||||||
|
offset: 0,
|
||||||
|
startBlock: 1990000,
|
||||||
|
blockLimit: 0,
|
||||||
|
txGroupId: 694,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { finalAddTxs, pendingAddTxs } = partitionAddTransactions(allAddTxs)
|
||||||
|
const { finalRemTxs, pendingRemTxs } = partitionRemoveTransactions(allRemTxs)
|
||||||
|
|
||||||
|
// We are going to keep all transactions in order to filter more accurately for display purposes.
|
||||||
|
console.log('Final addAdminTxs:', finalAddTxs);
|
||||||
|
console.log('Pending addAdminTxs:', pendingAddTxs);
|
||||||
|
console.log('Final remAdminTxs:', finalRemTxs);
|
||||||
|
console.log('Pending remAdminTxs:', pendingRemTxs);
|
||||||
|
|
||||||
|
return {
|
||||||
|
finalAddTxs,
|
||||||
|
pendingAddTxs,
|
||||||
|
finalRemTxs,
|
||||||
|
pendingRemTxs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function partitionAddTransactions(rawTransactions) {
|
||||||
|
const finalAddTxs = []
|
||||||
|
const pendingAddTxs = []
|
||||||
|
|
||||||
|
for (const tx of rawTransactions) {
|
||||||
|
if (tx.approvalStatus === 'PENDING') {
|
||||||
|
pendingAddTxs.push(tx)
|
||||||
|
} else {
|
||||||
|
finalAddTxs.push(tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { finalAddTxs, pendingAddTxs };
|
||||||
|
}
|
||||||
|
|
||||||
|
function partitionRemoveTransactions(rawTransactions) {
|
||||||
|
const finalRemTxs = []
|
||||||
|
const pendingRemTxs = []
|
||||||
|
|
||||||
|
for (const tx of rawTransactions) {
|
||||||
|
if (tx.approvalStatus === 'PENDING') {
|
||||||
|
pendingRemTxs.push(tx)
|
||||||
|
} else {
|
||||||
|
finalRemTxs.push(tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { finalRemTxs, pendingRemTxs }
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch kick transactions
|
|
||||||
const allRemTxs = await searchTransactions({
|
|
||||||
txTypes: [remAdmTx],
|
|
||||||
confirmationStatus: 'CONFIRMED',
|
|
||||||
limit: 0,
|
|
||||||
reverse: true,
|
|
||||||
offset: 0,
|
|
||||||
startBlock: 1990000,
|
|
||||||
blockLimit: 0,
|
|
||||||
txGroupId: 694,
|
|
||||||
})
|
|
||||||
// Filter out 'PENDING'
|
|
||||||
remAdminTxs = filterRemoveTransactions(allRemTxs)
|
|
||||||
console.warn('remAdminTxData (no PENDING nor past+PENDING):', remAdminTxs)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayExistingMinterAdmins = async () => {
|
const displayExistingMinterAdmins = async () => {
|
||||||
const adminListContainer = document.getElementById("admin-list-container")
|
const adminListContainer = document.getElementById("admin-list-container")
|
||||||
@ -309,9 +316,10 @@ const handleProposeDemotion = async (adminName, adminAddress) => {
|
|||||||
|
|
||||||
// Notify the user to fill out the rest
|
// Notify the user to fill out the rest
|
||||||
alert(`Admin "${adminName}" has been selected for demotion. Please fill out the rest of the form.`)
|
alert(`Admin "${adminName}" has been selected for demotion. Please fill out the rest of the form.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const fetchExistingARCard = async (cardIdentifierPrefix, minterName) => {
|
const fetchExistingARCard = async (cardIdentifierPrefix, minterName) => {
|
||||||
try {
|
try {
|
||||||
const response = await searchSimple(
|
const response = await searchSimple(
|
||||||
'BLOG_POST',
|
'BLOG_POST',
|
||||||
@ -381,7 +389,7 @@ const handleProposeDemotion = async (adminName, adminAddress) => {
|
|||||||
console.error("Error fetching existing AR card:", error)
|
console.error("Error fetching existing AR card:", error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const publishARCard = async (cardIdentifierPrefix) => {
|
const publishARCard = async (cardIdentifierPrefix) => {
|
||||||
@ -726,8 +734,8 @@ const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
|
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress, illegalDuplicate) => {
|
||||||
const { minterName, header, content, links, creator, timestamp, poll, promotionCard } = cardData
|
const { minterName, minterAddress='priorToAddition', 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,7 +752,7 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
|
|||||||
// showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers, minterAdmins)
|
// showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers, minterAdmins)
|
||||||
|
|
||||||
if (typeof promotionCard === 'boolean') {
|
if (typeof promotionCard === 'boolean') {
|
||||||
showPromotionCard = promotionCard;
|
showPromotionCard = promotionCard
|
||||||
} else if (typeof promotionCard === 'string') {
|
} else if (typeof promotionCard === 'string') {
|
||||||
// Could be "true" or "false" or something else
|
// Could be "true" or "false" or something else
|
||||||
const lower = promotionCard.trim().toLowerCase()
|
const lower = promotionCard.trim().toLowerCase()
|
||||||
@ -790,41 +798,63 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
|
|||||||
let altText = ''
|
let altText = ''
|
||||||
const verifiedName = await validateMinterName(minterName)
|
const verifiedName = await validateMinterName(minterName)
|
||||||
|
|
||||||
if (verifiedName) {
|
if (verifiedName && !illegalDuplicate) {
|
||||||
const accountInfo = await getNameInfo(verifiedName)
|
const accountInfo = await getNameInfo(verifiedName)
|
||||||
const accountAddress = accountInfo.owner
|
const accountAddress = accountInfo.owner
|
||||||
|
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
|
||||||
|
const adminAddresses = minterAdmins.map(m => m.member)
|
||||||
|
const existingAdmin = adminAddresses.includes(accountAddress)
|
||||||
|
const existingMinter = minterGroupAddresses.includes(accountAddress)
|
||||||
console.log(`name is validated, utilizing for removal features...${verifiedName}`)
|
console.log(`name is validated, utilizing for removal features...${verifiedName}`)
|
||||||
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
|
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
|
||||||
actionsHtml = actionsHtmlCheck
|
actionsHtml = actionsHtmlCheck
|
||||||
|
|
||||||
if (!addAdminTxs || !remAdminTxs) {
|
const { finalAddTxs, pendingAddTxs, finalRemTxs, pendingRemTxs } = await fetchAllARTxData()
|
||||||
await fetchAllARTxData()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addAdminTxs.some((addTx) => addTx.groupId === 694 && addTx.member === accountAddress)){
|
const confirmedAdd = finalAddTxs.some(
|
||||||
console.warn(`account was already adminified(PROMOTED), displaying as such...`)
|
(tx) => tx.groupId === 694 && tx.member === accountAddress
|
||||||
|
)
|
||||||
|
const userPendingAdd = pendingAddTxs.some(
|
||||||
|
(tx) => tx.groupId === 694 && tx.member === accountAddress
|
||||||
|
)
|
||||||
|
const confirmedRemove = finalRemTxs.some(
|
||||||
|
(tx) => tx.groupId === 694 && tx.admin === accountAddress
|
||||||
|
)
|
||||||
|
const userPendingRemove = pendingRemTxs.some(
|
||||||
|
(tx) => tx.groupId === 694 && tx.admin === accountAddress
|
||||||
|
)
|
||||||
|
|
||||||
|
// If user is definitely admin (finalAdd) and not pending removal
|
||||||
|
if (confirmedAdd && !userPendingRemove && existingAdmin) {
|
||||||
|
console.warn(`account was already admin, final. no add/remove pending.`);
|
||||||
cardColorCode = 'rgb(3, 11, 24)'
|
cardColorCode = 'rgb(3, 11, 24)'
|
||||||
altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`
|
altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`;
|
||||||
actionsHtml = ''
|
actionsHtml = ''
|
||||||
// =============================================================== if 'showPromotedDemoted' is wanted to be added.
|
}
|
||||||
// if (!showPromotedDemoted){
|
|
||||||
// console.warn(`promoted/demoted show checkbox is unchecked, and card is promoted, not displaying...`)
|
if (confirmedAdd && userPendingRemove && existingAdmin) {
|
||||||
// return ''
|
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remAdminTxs.some((remTx) => remTx.groupId === 694 && remTx.admin === accountAddress)){
|
// If user has a final "remove" and no pending additions or removals
|
||||||
console.warn(`account was already UNadminified(DEMOTED), displaying as such...`)
|
if (confirmedRemove && !userPendingAdd && existingMinter) {
|
||||||
|
console.warn(`account was demoted, final. no add pending, existingMinter.`);
|
||||||
cardColorCode = 'rgb(29, 4, 6)'
|
cardColorCode = 'rgb(29, 4, 6)'
|
||||||
altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
|
altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
|
||||||
actionsHtml = ''
|
actionsHtml = ''
|
||||||
|
|
||||||
// if (!showPromotedDemoted) {
|
|
||||||
// console.warn(`promoted/demoted show checkbox is unchecked, card is demoted, not displaying...`)
|
|
||||||
// return ''
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If user has both final remove and pending add, do something else
|
||||||
|
if (confirmedRemove && userPendingAdd && existingMinter) {
|
||||||
|
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
|
||||||
|
// Possibly show "DEMOTED but re-add in progress" or something
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if ( verifiedName && illegalDuplicate) {
|
||||||
|
console.warn(`illegalDuplicate detected (this card was somehow allowed to be published twice, keeping newest as active to prevent issues with old cards and updates, but displaying without actions...)`)
|
||||||
|
cardColorCode = 'rgb(82, 81, 81)'
|
||||||
|
altText = `<h4 style="color:rgb(21, 30, 39); margin-bottom: 0.5em;">DUPLICATE (diplayed for data only)</h4>`
|
||||||
|
actionsHtml = ''
|
||||||
} else {
|
} else {
|
||||||
console.warn(`name could not be validated, not setting actionsHtml`)
|
console.warn(`name could not be validated, not setting actionsHtml`)
|
||||||
actionsHtml = ''
|
actionsHtml = ''
|
||||||
|
@ -83,6 +83,13 @@ const loadAdminBoardPage = async () => {
|
|||||||
<option value="least-votes">Least Votes</option>
|
<option value="least-votes">Least Votes</option>
|
||||||
<option value="most-votes">Most Votes</option>
|
<option value="most-votes">Most Votes</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
|
||||||
|
<option value="0">Show All</option>
|
||||||
|
<option value="1">Last 1 day</option>
|
||||||
|
<option value="7">Last 7 days</option>
|
||||||
|
<option value="30" selected>Last 30 days</option>
|
||||||
|
<option value="90">Last 90 days</option>
|
||||||
|
</select>
|
||||||
<div class="show-card-checkbox" style="margin-top: 1em;">
|
<div class="show-card-checkbox" style="margin-top: 1em;">
|
||||||
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
|
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
|
||||||
<label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label>
|
<label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label>
|
||||||
@ -327,8 +334,20 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
|||||||
const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
|
const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
|
||||||
encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>"
|
encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>"
|
||||||
|
|
||||||
|
let afterTime = null
|
||||||
|
const timeRangeSelect = document.getElementById("time-range-select")
|
||||||
|
if (timeRangeSelect) {
|
||||||
|
const days = parseInt(timeRangeSelect.value, 10)
|
||||||
|
if (days > 0) {
|
||||||
|
const now = Date.now()
|
||||||
|
const dayMs = 24 * 60 * 60 * 1000
|
||||||
|
afterTime = now - days * dayMs // e.g. last X days
|
||||||
|
console.log(`afterTime for last ${days} days = ${new Date(afterTime).toLocaleString()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0)
|
const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0, 0, '', false, true, afterTime)
|
||||||
|
|
||||||
if (!response || !Array.isArray(response) || response.length === 0) {
|
if (!response || !Array.isArray(response) || response.length === 0) {
|
||||||
encryptedCardsContainer.innerHTML = "<p>No cards found.</p>"
|
encryptedCardsContainer.innerHTML = "<p>No cards found.</p>"
|
||||||
@ -379,12 +398,13 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
|||||||
const latestCardsMap = new Map()
|
const latestCardsMap = new Map()
|
||||||
|
|
||||||
validCardsWithData.forEach(({ card, decryptedCardData }) => {
|
validCardsWithData.forEach(({ card, decryptedCardData }) => {
|
||||||
const timestamp = card.updated || card.created || 0
|
const timestamp = card.created || 0
|
||||||
const existingCard = latestCardsMap.get(card.identifier)
|
const existingCard = latestCardsMap.get(card.identifier)
|
||||||
|
|
||||||
if (!existingCard || timestamp > (existingCard.card.updated || existingCard.card.created || 0)) {
|
if (!existingCard || timestamp < (existingCard.card.updated || existingCard.card.created || 0)) {
|
||||||
latestCardsMap.set(card.identifier, { card, decryptedCardData })
|
latestCardsMap.set(card.identifier, { card, decryptedCardData })
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const uniqueValidCards = Array.from(latestCardsMap.values())
|
const uniqueValidCards = Array.from(latestCardsMap.values())
|
||||||
@ -396,13 +416,11 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
|||||||
const obtainedMinterName = decryptedCardData.minterName
|
const obtainedMinterName = decryptedCardData.minterName
|
||||||
// Only check for cards that are NOT topic-based cards
|
// Only check for cards that are NOT topic-based cards
|
||||||
if ((!decryptedCardData.isTopic) || decryptedCardData.isTopic === 'false') {
|
if ((!decryptedCardData.isTopic) || decryptedCardData.isTopic === 'false') {
|
||||||
const cardTimestamp = card.updated || card.created || 0
|
|
||||||
|
|
||||||
if (obtainedMinterName) {
|
if (obtainedMinterName) {
|
||||||
const existingEntry = mostRecentCardsMap.get(obtainedMinterName)
|
const existingEntry = mostRecentCardsMap.get(obtainedMinterName)
|
||||||
|
|
||||||
// Replace only if the current card is more recent
|
if (!existingEntry) {
|
||||||
if (!existingEntry || cardTimestamp > (existingEntry.card.updated || existingEntry.card.created || 0)) {
|
|
||||||
mostRecentCardsMap.set(obtainedMinterName, { card, decryptedCardData })
|
mostRecentCardsMap.set(obtainedMinterName, { card, decryptedCardData })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,7 +432,7 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Convert the map into an array of final cards
|
// Convert the map into an array of final cards
|
||||||
const finalCards = Array.from(mostRecentCardsMap.values());
|
const finalCards = Array.from(mostRecentCardsMap.values())
|
||||||
|
|
||||||
let selectedSort = 'newest'
|
let selectedSort = 'newest'
|
||||||
const sortSelect = document.getElementById('sort-select')
|
const sortSelect = document.getElementById('sort-select')
|
||||||
@ -1037,7 +1055,7 @@ const processQortalLinkForRendering = async (link) => {
|
|||||||
return link
|
return link
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
|
const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier, nameIsActuallyAddress = false) => {
|
||||||
const latestBlockInfo = await getLatestBlockInfo()
|
const latestBlockInfo = await getLatestBlockInfo()
|
||||||
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
|
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
|
||||||
let minAdminCount
|
let minAdminCount
|
||||||
@ -1052,10 +1070,15 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
|
|||||||
minAdminCount = Math.round(fortyPercent)
|
minAdminCount = Math.round(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) {
|
if (isBlockPassed && (userState.isMinterAdmin || userState.isAdmin)) {
|
||||||
console.warn(`feature trigger has passed, checking for approval requirements`)
|
console.warn(`feature trigger has passed, checking for approval requirements`)
|
||||||
const addressInfo = await getNameInfo(name)
|
let address
|
||||||
const address = addressInfo.owner
|
if (!nameIsActuallyAddress){
|
||||||
|
const nameInfo = await getNameInfo(name)
|
||||||
|
address = nameInfo.owner
|
||||||
|
} else {
|
||||||
|
address = name
|
||||||
|
}
|
||||||
const kickApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_KICK")
|
const kickApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_KICK")
|
||||||
const banApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_BAN")
|
const banApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_BAN")
|
||||||
|
|
||||||
@ -1068,7 +1091,7 @@ const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adminYes >= minAdminCount && userState.isMinterAdmin) {
|
if (adminYes >= minAdminCount && (userState.isMinterAdmin || userState.isAdmin)) {
|
||||||
const removeButtonHtml = createRemoveButtonHtml(name, cardIdentifier)
|
const removeButtonHtml = createRemoveButtonHtml(name, cardIdentifier)
|
||||||
return removeButtonHtml
|
return removeButtonHtml
|
||||||
} else{
|
} else{
|
||||||
@ -1098,6 +1121,8 @@ const createRemoveButtonHtml = (name, cardIdentifier) => {
|
|||||||
|
|
||||||
const handleKickMinter = async (minterName) => {
|
const handleKickMinter = async (minterName) => {
|
||||||
try {
|
try {
|
||||||
|
isAddress = await getAddressInfo(minterName)
|
||||||
|
|
||||||
// Optional block check
|
// Optional block check
|
||||||
let txGroupId = 0
|
let txGroupId = 0
|
||||||
// const { height: currentHeight } = await getLatestBlockInfo()
|
// const { height: currentHeight } = await getLatestBlockInfo()
|
||||||
@ -1108,8 +1133,14 @@ const handleKickMinter = async (minterName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the minter address from name info
|
// Get the minter address from name info
|
||||||
const minterNameInfo = await getNameInfo(minterName)
|
let minterAddress
|
||||||
const minterAddress = minterNameInfo?.owner
|
if (!isAddress){
|
||||||
|
const minterNameInfo = await getNameInfo(minterName)
|
||||||
|
minterAddress = minterNameInfo?.owner
|
||||||
|
} else {
|
||||||
|
minterAddress = minterName
|
||||||
|
}
|
||||||
|
|
||||||
if (!minterAddress) {
|
if (!minterAddress) {
|
||||||
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
|
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
|
||||||
return
|
return
|
||||||
@ -1150,6 +1181,7 @@ const handleKickMinter = async (minterName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleBanMinter = async (minterName) => {
|
const handleBanMinter = async (minterName) => {
|
||||||
|
isAddress = await getAddressInfo(minterName)
|
||||||
try {
|
try {
|
||||||
let txGroupId = 0
|
let txGroupId = 0
|
||||||
// const { height: currentHeight } = await getLatestBlockInfo()
|
// const { height: currentHeight } = await getLatestBlockInfo()
|
||||||
@ -1161,9 +1193,13 @@ const handleBanMinter = async (minterName) => {
|
|||||||
console.log(`featureTrigger block is passed, using txGroupId 694`)
|
console.log(`featureTrigger block is passed, using txGroupId 694`)
|
||||||
txGroupId = 694
|
txGroupId = 694
|
||||||
}
|
}
|
||||||
|
let minterAddress
|
||||||
const minterNameInfo = await getNameInfo(minterName)
|
if (!isAddress) {
|
||||||
const minterAddress = minterNameInfo?.owner
|
const minterNameInfo = await getNameInfo(minterName)
|
||||||
|
const minterAddress = minterNameInfo?.owner
|
||||||
|
} else {
|
||||||
|
minterAddress = minterName
|
||||||
|
}
|
||||||
|
|
||||||
if (!minterAddress) {
|
if (!minterAddress) {
|
||||||
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
|
alert(`No valid address found for minter name: ${minterName}, this should NOT have happened, please report to developers...`)
|
||||||
@ -1236,7 +1272,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
|||||||
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
|
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
|
||||||
const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false
|
const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false
|
||||||
|
|
||||||
const isUndefinedUser = (minterName === 'undefined')
|
const isUndefinedUser = (minterName === 'undefined' || minterName === 'null')
|
||||||
|
|
||||||
const hasTopicMode = Object.prototype.hasOwnProperty.call(cardData, 'topicMode')
|
const hasTopicMode = Object.prototype.hasOwnProperty.call(cardData, 'topicMode')
|
||||||
|
|
||||||
@ -1254,7 +1290,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
|||||||
let cardColorCode = showTopic ? '#0e1b15' : '#151f28'
|
let cardColorCode = showTopic ? '#0e1b15' : '#151f28'
|
||||||
|
|
||||||
const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
|
const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
|
||||||
<div class="support-header"><h5> REGARDING (Topic): </h5></div>
|
<div class="support-header"><h5> REGARDING (Topic / Address): </h5></div>
|
||||||
<h3>${minterName}` :
|
<h3>${minterName}` :
|
||||||
`
|
`
|
||||||
<div class="support-header"><h5> REGARDING (Name): </h5></div>
|
<div class="support-header"><h5> REGARDING (Name): </h5></div>
|
||||||
@ -1274,16 +1310,23 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
|||||||
let adjustmentText = ''
|
let adjustmentText = ''
|
||||||
const verifiedName = await validateMinterName(minterName)
|
const verifiedName = await validateMinterName(minterName)
|
||||||
let levelText = '</h3>'
|
let levelText = '</h3>'
|
||||||
|
const addressVerification = await getAddressInfo(minterName)
|
||||||
|
const verifiedAddress = addressVerification.address
|
||||||
|
|
||||||
if (verifiedName) {
|
if (verifiedName || verifiedAddress) {
|
||||||
const accountInfo = await getNameInfo(verifiedName)
|
let accountInfo
|
||||||
const accountAddress = accountInfo.owner
|
if (!verifiedAddress){
|
||||||
const addressInfo = await getAddressInfo(accountAddress)
|
accountInfo = await getNameInfo(verifiedName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountAddress = verifiedAddress ? addressVerification.address : accountInfo.owner
|
||||||
|
const addressInfo = verifiedAddress ? addressVerification : await getAddressInfo(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 = await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
|
const removeActionsHtml = verifiedAddress ? await checkAndDisplayRemoveActions(adminYes, verifiedAddress, cardIdentifier) : 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
|
||||||
|
@ -28,10 +28,10 @@ async function loadMinterAdminToolsPage() {
|
|||||||
<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>
|
||||||
<div><h2>No Functionality Here Yet</h2></div>
|
<div><h2>COMING SOON...</h2></div>
|
||||||
<div>
|
<div>
|
||||||
<p>This page is still under development. Until the final Mintership proposal modifications are made, and the MINTER group is transferred to null, there is no need for this page's functionality. The page will be updated when the final modifications are made.</p>
|
<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> This page until then is simply a placeholder.</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>
|
||||||
|
|
||||||
|
@ -37,6 +37,13 @@ const loadMinterBoardPage = async () => {
|
|||||||
<option value="least-votes">Least Votes</option>
|
<option value="least-votes">Least Votes</option>
|
||||||
<option value="most-votes">Most Votes</option>
|
<option value="most-votes">Most Votes</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select id="time-range-select" style="margin-left: 10px; padding: 5px;">
|
||||||
|
<option value="0">Show All</option>
|
||||||
|
<option value="1">Last 1 day</option>
|
||||||
|
<option value="7">Last 7 days</option>
|
||||||
|
<option value="30" selected>Last 30 days</option>
|
||||||
|
<option value="90">Last 90 days</option>
|
||||||
|
</select>
|
||||||
<div 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">
|
||||||
@ -127,6 +134,11 @@ const loadMinterBoardPage = async () => {
|
|||||||
await publishCard(minterCardIdentifierPrefix)
|
await publishCard(minterCardIdentifierPrefix)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.getElementById("time-range-select").addEventListener("change", async () => {
|
||||||
|
// Re-load the cards whenever user chooses a new sort option.
|
||||||
|
await loadCards(minterCardIdentifierPrefix)
|
||||||
|
})
|
||||||
|
|
||||||
document.getElementById("sort-select").addEventListener("change", async () => {
|
document.getElementById("sort-select").addEventListener("change", async () => {
|
||||||
// Re-load the cards whenever user chooses a new sort option.
|
// Re-load the cards whenever user chooses a new sort option.
|
||||||
await loadCards(minterCardIdentifierPrefix)
|
await loadCards(minterCardIdentifierPrefix)
|
||||||
@ -149,11 +161,11 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (cardIdentifier.startsWith(minterCardIdentifierPrefix)){
|
if (cardIdentifier.startsWith(minterCardIdentifierPrefix)){
|
||||||
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1)
|
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1, 0, '', false, true)
|
||||||
const minterName = await searchSimpleResults.name
|
const minterName = await searchSimpleResults.name
|
||||||
return minterName
|
return minterName
|
||||||
} else if (cardIdentifier.startsWith(addRemoveIdentifierPrefix)) {
|
} else if (cardIdentifier.startsWith(addRemoveIdentifierPrefix)) {
|
||||||
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1)
|
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1, 0, '', false, true)
|
||||||
const publisherName = searchSimpleResults.name
|
const publisherName = searchSimpleResults.name
|
||||||
const cardDataResponse = await qortalRequest({
|
const cardDataResponse = await qortalRequest({
|
||||||
action: "FETCH_QDN_RESOURCE",
|
action: "FETCH_QDN_RESOURCE",
|
||||||
@ -161,103 +173,204 @@ const extractMinterCardsMinterName = async (cardIdentifier) => {
|
|||||||
service: "BLOG_POST",
|
service: "BLOG_POST",
|
||||||
identifier: cardIdentifier,
|
identifier: cardIdentifier,
|
||||||
})
|
})
|
||||||
|
let nameInvalid = false
|
||||||
const minterName = cardDataResponse.minterName
|
const minterName = cardDataResponse.minterName
|
||||||
return minterName
|
if (minterName){
|
||||||
|
return minterName
|
||||||
|
} else {
|
||||||
|
nameInvalid = true
|
||||||
|
console.warn(`fuckery detected on identifier: ${cardIdentifier}, hello dipshit Mythril!, name invalid? Name doesn't match publisher? Returning invalid flag + publisherName...`)
|
||||||
|
return publisherName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const processMinterCards = async (validMinterCards) => {
|
const groupAndLabelByIdentifier = (allCards) => {
|
||||||
const latestCardsMap = new Map()
|
// Group by identifier
|
||||||
|
const mapById = new Map()
|
||||||
// Deduplicate by identifier, keeping the most recent
|
allCards.forEach(card => {
|
||||||
validMinterCards.forEach(card => {
|
if (!mapById.has(card.identifier)) {
|
||||||
const timestamp = card.updated || card.created || 0
|
mapById.set(card.identifier, [])
|
||||||
const existingCard = latestCardsMap.get(card.identifier)
|
|
||||||
|
|
||||||
if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
|
|
||||||
latestCardsMap.set(card.identifier, card)
|
|
||||||
}
|
}
|
||||||
|
mapById.get(card.identifier).push(card)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Convert Map back to array
|
// For each identifier's group, sort oldest->newest so the first is "master"
|
||||||
const uniqueValidCards = Array.from(latestCardsMap.values())
|
const output = []
|
||||||
|
for (const [identifier, group] of mapById.entries()) {
|
||||||
|
group.sort((a, b) => {
|
||||||
|
const aTime = a.created || 0
|
||||||
|
const bTime = b.created || 0
|
||||||
|
return aTime - bTime // oldest first
|
||||||
|
})
|
||||||
|
|
||||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
// Mark the first as master
|
||||||
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
|
group[0].isMaster = true
|
||||||
const minterNameMap = new Map()
|
// The rest are updates
|
||||||
|
for (let i = 1; i < group.length; i++) {
|
||||||
|
group[i].isMaster = false
|
||||||
|
}
|
||||||
|
|
||||||
// For each card, extract minterName safely
|
// push them all to output
|
||||||
for (const card of uniqueValidCards) {
|
output.push(...group)
|
||||||
let minterName
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
// no semicolons, using arrow functions
|
||||||
|
const groupByIdentifierOldestFirst = (allCards) => {
|
||||||
|
// map of identifier => array of cards
|
||||||
|
const mapById = new Map()
|
||||||
|
|
||||||
|
allCards.forEach(card => {
|
||||||
|
if (!mapById.has(card.identifier)) {
|
||||||
|
mapById.set(card.identifier, [])
|
||||||
|
}
|
||||||
|
mapById.get(card.identifier).push(card)
|
||||||
|
})
|
||||||
|
|
||||||
|
// sort each group oldest->newest
|
||||||
|
for (const [identifier, group] of mapById.entries()) {
|
||||||
|
group.sort((a, b) => {
|
||||||
|
const aTime = a.created || 0
|
||||||
|
const bTime = b.created || 0
|
||||||
|
return aTime - bTime // oldest first
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapById
|
||||||
|
}
|
||||||
|
|
||||||
|
// no semicolons, arrow functions
|
||||||
|
const buildMinterNameGroups = async (mapById) => {
|
||||||
|
// We'll build an array of objects: { minterName, cards }
|
||||||
|
// Then we can combine any that share the same minterName.
|
||||||
|
|
||||||
|
const nameGroups = []
|
||||||
|
|
||||||
|
for (let [identifier, group] of mapById.entries()) {
|
||||||
|
// group[0] is the oldest => "master" card
|
||||||
|
let masterCard = group[0]
|
||||||
|
|
||||||
|
// Filter out any cards that are not published by the 'masterPublisher'
|
||||||
|
const masterPublisherName = masterCard.name
|
||||||
|
// Remove any cards in this identifier group that have a different publisherName
|
||||||
|
const filteredGroup = group.filter(c => c.name === masterPublisherName)
|
||||||
|
|
||||||
|
// If filtering left zero cards, skip entire group
|
||||||
|
if (!filteredGroup.length) {
|
||||||
|
console.warn(`All cards removed for identifier=${identifier} (different publishers). Skipping.`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Reassign group to the filtered version, then re-define masterCard
|
||||||
|
group = filteredGroup
|
||||||
|
masterCard = group[0] // oldest after filtering
|
||||||
|
|
||||||
|
// attempt to obtain minterName from the master card
|
||||||
|
let masterMinterName
|
||||||
try {
|
try {
|
||||||
// If this throws, we catch below and skip
|
masterMinterName = await extractMinterCardsMinterName(masterCard.identifier)
|
||||||
minterName = await extractMinterCardsMinterName(card.identifier)
|
} catch (err) {
|
||||||
} catch (error) {
|
console.warn(`Skipping entire group ${identifier}, no valid minterName from master`, err)
|
||||||
console.warn(
|
|
||||||
`Skipping card ${card.identifier} because extractMinterCardsMinterName failed:`,
|
|
||||||
error
|
|
||||||
)
|
|
||||||
continue // Skip this card and move on
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`minterName`, minterName)
|
|
||||||
// Next, get minterNameInfo
|
|
||||||
const minterNameInfo = await getNameInfo(minterName)
|
|
||||||
if (!minterNameInfo) {
|
|
||||||
console.warn(`minterNameInfo is null for minter: ${minterName}, skipping card.`)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const minterAddress = minterNameInfo.owner
|
// Store an object with the minterName we extracted, plus all cards in that group
|
||||||
// Validate the address
|
nameGroups.push({
|
||||||
const addressValid = await getAddressInfo(minterAddress)
|
minterName: masterMinterName,
|
||||||
if (!minterAddress || !addressValid) {
|
cards: group // includes the master & updates
|
||||||
console.warn(`minterAddress invalid or missing for: ${minterName}, skipping card.`, minterAddress)
|
})
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If this is a 'regular' minter card, skip if user is already a minter
|
// Combine them: minterName => array of *all* cards from all matching groups
|
||||||
if (!card.identifier.includes('QM-AR-card')) {
|
const combinedMap = new Map()
|
||||||
if (minterGroupAddresses.includes(minterAddress)) {
|
for (const entry of nameGroups) {
|
||||||
console.log(
|
const mName = entry.minterName
|
||||||
`existing minter found or fake name detected. Not including minter card: ${card.identifier}`
|
if (!combinedMap.has(mName)) {
|
||||||
)
|
combinedMap.set(mName, [])
|
||||||
continue
|
}
|
||||||
|
combinedMap.get(mName).push(...entry.cards)
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedMap
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getNewestCardPerMinterName = (combinedMap) => {
|
||||||
|
// We'll produce an array of the newest card for each minterName, this will be utilized as the 'final filter' to display cards published/updated by unique minters.
|
||||||
|
const finalOutput = []
|
||||||
|
|
||||||
|
for (const [mName, cardArray] of combinedMap.entries()) {
|
||||||
|
// sort by updated or created, descending => newest first
|
||||||
|
cardArray.sort((a, b) => {
|
||||||
|
const aTime = a.updated || a.created || 0
|
||||||
|
const bTime = b.updated || b.created || 0
|
||||||
|
return bTime - aTime
|
||||||
|
})
|
||||||
|
// newest is [0]
|
||||||
|
finalOutput.push(cardArray[0])
|
||||||
|
}
|
||||||
|
// Then maybe globally sort them newest first
|
||||||
|
finalOutput.sort((a, b) => {
|
||||||
|
const aTime = a.updated || a.created || 0
|
||||||
|
const bTime = b.updated || b.created || 0
|
||||||
|
return bTime - aTime
|
||||||
|
})
|
||||||
|
|
||||||
|
return finalOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
const processMinterBoardCards = async (allValidCards) => {
|
||||||
|
// group by identifier, sorted oldest->newest
|
||||||
|
const mapById = groupByIdentifierOldestFirst(allValidCards)
|
||||||
|
// build a map of minterName => all cards from those identifiers
|
||||||
|
const minterNameMap = await buildMinterNameGroups(mapById)
|
||||||
|
// from that map, keep only the single newest card per minterName
|
||||||
|
const newestCards = getNewestCardPerMinterName(minterNameMap)
|
||||||
|
// return final array of all newest cards
|
||||||
|
return newestCards
|
||||||
|
}
|
||||||
|
|
||||||
|
const processARBoardCards = async (allValidCards) => {
|
||||||
|
const mapById = groupByIdentifierOldestFirst(allValidCards)
|
||||||
|
// build a map of minterName => all cards from those identifiers
|
||||||
|
const mapByName = await buildMinterNameGroups(mapById)
|
||||||
|
// For each minterName group, we might want to sort them newest->oldest
|
||||||
|
const finalOutput = []
|
||||||
|
for (const [minterName, group] of mapByName.entries()) {
|
||||||
|
group.sort((a, b) => {
|
||||||
|
const aTime = a.updated || a.created || 0
|
||||||
|
const bTime = b.updated || b.created || 0
|
||||||
|
return bTime - aTime
|
||||||
|
})
|
||||||
|
// both resolution for the duplicate QuickMythril card, and handling of all future duplicates that may be published...
|
||||||
|
if (group[0].identifier === 'QM-AR-card-Xw3dxL') {
|
||||||
|
console.warn(`This is a bug that allowed a duplicate prior to the logic displaying them based on original publisher only... displaying in reverse order...`)
|
||||||
|
group[0].isDuplicate = true
|
||||||
|
for (let i = 1; i < group.length; i++) {
|
||||||
|
group[i].isDuplicate = false
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
group[0].isDuplicate = false
|
||||||
|
for (let i = 1; i < group.length; i++) {
|
||||||
|
group[i].isDuplicate = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// push them all
|
||||||
// Keep only the most recent card for each minterName
|
finalOutput.push(...group)
|
||||||
const existingCard = minterNameMap.get(minterName)
|
|
||||||
const cardTimestamp = card.updated || card.created || 0
|
|
||||||
const existingTimestamp = existingCard?.updated || existingCard?.created || 0
|
|
||||||
|
|
||||||
if (!existingCard || cardTimestamp > existingTimestamp) {
|
|
||||||
minterNameMap.set(minterName, card)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Sort final by newest overall
|
||||||
// Convert minterNameMap to final array
|
finalOutput.sort((a, b) => {
|
||||||
const finalCards = []
|
const aTime = a.updated || a.created || 0
|
||||||
const seenMinterNames = new Set()
|
const bTime = b.updated || b.created || 0
|
||||||
|
return bTime - aTime
|
||||||
for (const [minterName, card] of minterNameMap.entries()) {
|
|
||||||
if (!seenMinterNames.has(minterName)) {
|
|
||||||
finalCards.push(card)
|
|
||||||
seenMinterNames.add(minterName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by timestamp descending
|
|
||||||
finalCards.sort((a, b) => {
|
|
||||||
const timestampA = a.updated || a.created || 0
|
|
||||||
const timestampB = b.updated || b.created || 0
|
|
||||||
return timestampB - timestampA
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return finalCards
|
return finalOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -266,37 +379,51 @@ const loadCards = async (cardIdentifierPrefix) => {
|
|||||||
const cardsContainer = document.getElementById("cards-container")
|
const cardsContainer = document.getElementById("cards-container")
|
||||||
let isARBoard = false
|
let isARBoard = false
|
||||||
cardsContainer.innerHTML = "<p>Loading cards...</p>"
|
cardsContainer.innerHTML = "<p>Loading cards...</p>"
|
||||||
if ((cardIdentifierPrefix.startsWith(`QM-AR-card`))) {
|
|
||||||
|
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
|
||||||
isARBoard = true
|
isARBoard = true
|
||||||
console.warn(`ARBoard determined:`, isARBoard)
|
console.warn(`ARBoard determined:`, isARBoard)
|
||||||
}
|
}
|
||||||
|
let afterTime = 0
|
||||||
|
const timeRangeSelect = document.getElementById("time-range-select")
|
||||||
|
|
||||||
|
if (timeRangeSelect) {
|
||||||
|
const days = parseInt(timeRangeSelect.value, 10)
|
||||||
|
if (days > 0) {
|
||||||
|
const now = Date.now()
|
||||||
|
const dayMs = 24 * 60 * 60 * 1000
|
||||||
|
afterTime = now - days * dayMs // e.g. last X days
|
||||||
|
console.log(`afterTime for last ${days} days = ${new Date(afterTime).toLocaleString()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, '' , 0)
|
// 1) Fetch raw "BLOG_POST" entries
|
||||||
|
const response = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime)
|
||||||
|
|
||||||
if (!response || !Array.isArray(response) || response.length === 0) {
|
if (!response || !Array.isArray(response) || response.length === 0) {
|
||||||
cardsContainer.innerHTML = "<p>No cards found.</p>"
|
cardsContainer.innerHTML = "<p>No cards found.</p>"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Validate cards and filter
|
// 2) Validate structure
|
||||||
const validatedCards = await Promise.all(
|
const validatedCards = await Promise.all(
|
||||||
response.map(async card => {
|
response.map(async (card) => {
|
||||||
const isValid = await validateCardStructure(card)
|
const isValid = await validateCardStructure(card)
|
||||||
return isValid ? card : null
|
return isValid ? card : null
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const validCards = validatedCards.filter(card => card !== null)
|
const validCards = validatedCards.filter((card) => card !== null)
|
||||||
|
|
||||||
if (validCards.length === 0) {
|
if (validCards.length === 0) {
|
||||||
cardsContainer.innerHTML = "<p>No valid cards found.</p>"
|
cardsContainer.innerHTML = "<p>No valid cards found.</p>"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const finalCards = await processMinterCards(validCards)
|
// Additional logic for ARBoard or MinterCards
|
||||||
|
const finalCards = isARBoard
|
||||||
|
? await processARBoardCards(validCards)
|
||||||
|
: await processMinterBoardCards(validCards)
|
||||||
|
|
||||||
// finalCards is already sorted by newest (timestamp) by processMinterCards.
|
// Sort finalCards according to selectedSort
|
||||||
// We'll re-sort if "Name" or "Recent Comments" is selected.
|
|
||||||
|
|
||||||
// Grab the selected sort from the dropdown (if it exists).
|
|
||||||
let selectedSort = 'newest'
|
let selectedSort = 'newest'
|
||||||
const sortSelect = document.getElementById('sort-select')
|
const sortSelect = document.getElementById('sort-select')
|
||||||
if (sortSelect) {
|
if (sortSelect) {
|
||||||
@ -304,232 +431,202 @@ const loadCards = async (cardIdentifierPrefix) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedSort === 'name') {
|
if (selectedSort === 'name') {
|
||||||
// Sort alphabetically by the creator's name
|
|
||||||
finalCards.sort((a, b) => {
|
finalCards.sort((a, b) => {
|
||||||
const nameA = a.name?.toLowerCase() || ''
|
const nameA = a.name?.toLowerCase() || ''
|
||||||
const nameB = b.name?.toLowerCase() || ''
|
const nameB = b.name?.toLowerCase() || ''
|
||||||
return nameA.localeCompare(nameB)
|
return nameA.localeCompare(nameB)
|
||||||
})
|
})
|
||||||
} else if (selectedSort === 'recent-comments') {
|
} else if (selectedSort === 'recent-comments') {
|
||||||
// We need each card's newest comment timestamp for sorting
|
// If you need the newest comment timestamp
|
||||||
for (let card of finalCards) {
|
for (let card of finalCards) {
|
||||||
card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier)
|
card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier)
|
||||||
}
|
}
|
||||||
// Then sort descending by newest comment
|
|
||||||
finalCards.sort((a, b) =>
|
finalCards.sort((a, b) =>
|
||||||
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
||||||
)
|
)
|
||||||
} else if (selectedSort === 'least-votes') {
|
} else if (selectedSort === 'least-votes') {
|
||||||
// For each card, fetch its poll data so we know how many admin + minter votes it has.
|
await applyVoteSortingData(finalCards, /* ascending= */ true)
|
||||||
// Store those values on the card object so we can sort on them.
|
|
||||||
// Sort ascending by admin votes; if there's a tie, sort ascending by minter votes.
|
|
||||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
|
||||||
const minterAdmins = await fetchMinterGroupAdmins()
|
|
||||||
// For each card, fetch the poll data & store counts on the card object.
|
|
||||||
for (const card of finalCards) {
|
|
||||||
try {
|
|
||||||
// We must fetch the card data from QDN to get the `poll` name
|
|
||||||
const cardDataResponse = await qortalRequest({
|
|
||||||
action: "FETCH_QDN_RESOURCE",
|
|
||||||
name: card.name,
|
|
||||||
service: "BLOG_POST",
|
|
||||||
identifier: card.identifier,
|
|
||||||
})
|
|
||||||
// If the card or poll is missing, skip
|
|
||||||
if (!cardDataResponse || !cardDataResponse.poll) {
|
|
||||||
card._adminVotes = 0
|
|
||||||
card._adminYes = 0
|
|
||||||
card._minterVotes = 0
|
|
||||||
card._minterYes = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const pollResults = await fetchPollResults(cardDataResponse.poll)
|
|
||||||
const {
|
|
||||||
adminYes,
|
|
||||||
adminNo,
|
|
||||||
minterYes,
|
|
||||||
minterNo
|
|
||||||
} = await processPollData(
|
|
||||||
pollResults,
|
|
||||||
minterGroupMembers,
|
|
||||||
minterAdmins,
|
|
||||||
cardDataResponse.creator,
|
|
||||||
card.identifier
|
|
||||||
)
|
|
||||||
// Store the totals so we can sort on them
|
|
||||||
card._adminVotes = adminYes + adminNo
|
|
||||||
card._adminYes = adminYes
|
|
||||||
card._minterVotes = minterYes + minterNo
|
|
||||||
card._minterYes = minterYes
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
|
|
||||||
// If something fails, default to zero so it sorts "lowest"
|
|
||||||
card._adminVotes = 0
|
|
||||||
card._adminYes = 0
|
|
||||||
card._minterVotes = 0
|
|
||||||
card._minterYes = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Now that each card has _adminVotes + _minterVotes, do an ascending sort:
|
|
||||||
finalCards.sort((a, b) => {
|
|
||||||
// admin votes ascending
|
|
||||||
const diffAdminTotal = a._adminVotes - b._adminVotes
|
|
||||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
|
||||||
// admin YES ascending
|
|
||||||
const diffAdminYes = a._adminYes - b._adminYes
|
|
||||||
if (diffAdminYes !== 0) return diffAdminYes
|
|
||||||
// minter votes ascending
|
|
||||||
const diffMinterTotal = a._minterVotes - b._minterVotes
|
|
||||||
if (diffMinterTotal !== 0) return diffMinterTotal
|
|
||||||
// minter YES ascending
|
|
||||||
return a._minterYes - b._minterYes
|
|
||||||
})
|
|
||||||
} else if (selectedSort === 'most-votes') {
|
} else if (selectedSort === 'most-votes') {
|
||||||
// Fetch poll data & store admin + minter votes as before.
|
await applyVoteSortingData(finalCards, /* ascending= */ false)
|
||||||
// Sort descending by admin votes; if there's a tie, sort descending by minter votes.
|
|
||||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
|
||||||
const minterAdmins = await fetchMinterGroupAdmins()
|
|
||||||
for (const card of finalCards) {
|
|
||||||
try {
|
|
||||||
const cardDataResponse = await qortalRequest({
|
|
||||||
action: "FETCH_QDN_RESOURCE",
|
|
||||||
name: card.name,
|
|
||||||
service: "BLOG_POST",
|
|
||||||
identifier: card.identifier,
|
|
||||||
})
|
|
||||||
if (!cardDataResponse || !cardDataResponse.poll) {
|
|
||||||
card._adminVotes = 0
|
|
||||||
card._adminYes = 0
|
|
||||||
card._minterVotes = 0
|
|
||||||
card._minterYes = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const pollResults = await fetchPollResults(cardDataResponse.poll)
|
|
||||||
const {
|
|
||||||
adminYes,
|
|
||||||
adminNo,
|
|
||||||
minterYes,
|
|
||||||
minterNo
|
|
||||||
} = await processPollData(
|
|
||||||
pollResults,
|
|
||||||
minterGroupMembers,
|
|
||||||
minterAdmins,
|
|
||||||
cardDataResponse.creator,
|
|
||||||
card.identifier
|
|
||||||
)
|
|
||||||
card._adminVotes = adminYes + adminNo
|
|
||||||
card._adminYes = adminYes
|
|
||||||
card._minterVotes = minterYes + minterNo
|
|
||||||
card._minterYes = minterYes
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
|
|
||||||
card._adminVotes = 0
|
|
||||||
card._adminYes = 0
|
|
||||||
card._minterVotes = 0
|
|
||||||
card._minterYes = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sort descending by admin votes, then minter votes
|
|
||||||
finalCards.sort((a, b) => {
|
|
||||||
// admin votes descending
|
|
||||||
const diffAdminTotal = b._adminVotes - a._adminVotes
|
|
||||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
|
||||||
// admin YES descending
|
|
||||||
const diffAdminYes = b._adminYes - a._adminYes
|
|
||||||
if (diffAdminYes !== 0) return diffAdminYes
|
|
||||||
// minter votes descending
|
|
||||||
const diffMinterTotal = b._minterVotes - a._minterVotes
|
|
||||||
if (diffMinterTotal !== 0) return diffMinterTotal
|
|
||||||
// minter YES descending
|
|
||||||
return b._minterYes - a._minterYes
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// else 'newest' => do nothing, finalCards stays in newest-first order
|
// else 'newest' => do nothing (already sorted newest-first by your process functions).
|
||||||
|
// Create the 'finalCardsArray' that includes the data, etc.
|
||||||
|
let finalCardsArray = []
|
||||||
|
|
||||||
// Display skeleton cards immediately
|
for (const card of finalCards) {
|
||||||
cardsContainer.innerHTML = ""
|
|
||||||
finalCards.forEach(card => {
|
|
||||||
const skeletonHTML = createSkeletonCardHTML(card.identifier)
|
|
||||||
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fetch and update each card
|
|
||||||
finalCards.forEach(async card => {
|
|
||||||
try {
|
try {
|
||||||
const cardDataResponse = await qortalRequest({
|
const cardDataResponse = await qortalRequest({
|
||||||
action: "FETCH_QDN_RESOURCE",
|
action: "FETCH_QDN_RESOURCE",
|
||||||
name: card.name,
|
name: card.name,
|
||||||
service: "BLOG_POST",
|
service: "BLOG_POST",
|
||||||
identifier: card.identifier,
|
identifier: card.identifier
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!cardDataResponse) {
|
|
||||||
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`)
|
|
||||||
removeSkeleton(card.identifier)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cardDataResponse.poll) {
|
|
||||||
console.warn(`Skipping card with no poll: ${card.identifier}`)
|
|
||||||
removeSkeleton(card.identifier)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getting the poll owner address uses the same API call as public key, so takes the same time, but address will be needed later.
|
if (!cardDataResponse || !cardDataResponse.poll) {
|
||||||
|
// skip
|
||||||
|
console.warn(`Skipping card: missing data/poll. identifier=${card.identifier}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Extra validation: check poll ownership matches card publisher
|
||||||
const pollPublisherAddress = await getPollOwnerAddress(cardDataResponse.poll)
|
const pollPublisherAddress = await getPollOwnerAddress(cardDataResponse.poll)
|
||||||
// Getting the card publisher address to compare instead should cause faster loading, since getting the public key by name first gets the address then converts it
|
|
||||||
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(`not displaying card, QuickMythril pollHijack attack found! Discarding card with identifier: ${card.identifier}`)
|
continue
|
||||||
removeSkeleton(card.identifier)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const pollResults = await fetchPollResults(cardDataResponse.poll)
|
// If ARBoard, do a quick address check
|
||||||
const bgColor = generateDarkPastelBackgroundBy(card.name)
|
|
||||||
const commentCount = await countComments(card.identifier)
|
|
||||||
const cardUpdatedTime = card.updated || null
|
|
||||||
|
|
||||||
if (isARBoard) {
|
if (isARBoard) {
|
||||||
const name = await getNameInfo(cardDataResponse.minterName)
|
const ok = await verifyARBoardAddress(cardDataResponse.minterName)
|
||||||
const address = name.owner
|
if (!ok) {
|
||||||
if (minterAdminAddresses && minterGroupAddresses) {
|
console.warn(`Invalid minter address for AR board. identifier=${card.identifier}`)
|
||||||
if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) {
|
continue
|
||||||
console.warn(`Found card from ARBoard that contained a non-minter!`)
|
|
||||||
removeSkeleton(card.identifier)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if (!minterAdminAddresses || !minterGroupAddresses){
|
|
||||||
const minterGroup = await fetchMinterGroupMembers()
|
|
||||||
const adminGroup = await fetchMinterGroupAdmins()
|
|
||||||
minterAdminAddresses = adminGroup.map(m => m.member)
|
|
||||||
minterGroupAddresses = minterGroup.map(m => m.member)
|
|
||||||
if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) {
|
|
||||||
console.warn(`Found card from ARBoard that contained a non-minter!`)
|
|
||||||
removeSkeleton(card.identifier)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// **Push** to finalCardsArray for further processing (duplicates, etc.)
|
||||||
const finalCardHTML = isARBoard ? // If we're calling from the ARBoard, we will create HTML with a different function.
|
finalCardsArray.push({
|
||||||
await createARCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor)
|
...card,
|
||||||
:
|
cardDataResponse,
|
||||||
await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress)
|
pollPublisherAddress,
|
||||||
replaceSkeleton(card.identifier, finalCardHTML)
|
cardPublisherAddress,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error(`Error processing card ${card.identifier}:`, error)
|
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:
|
||||||
|
cardsContainer.innerHTML = ""
|
||||||
|
for (const cardObj of finalCardsArray) {
|
||||||
|
// Insert a skeleton first if you like
|
||||||
|
const skeletonHTML = createSkeletonCardHTML(cardObj.identifier)
|
||||||
|
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
||||||
|
// Build final HTML
|
||||||
|
const pollResults = await fetchPollResults(cardObj.cardDataResponse.poll)
|
||||||
|
const commentCount = await countComments(cardObj.identifier)
|
||||||
|
const cardUpdatedTime = cardObj.updated || null
|
||||||
|
const bgColor = generateDarkPastelBackgroundBy(cardObj.name)
|
||||||
|
// Construct the final HTML for each card
|
||||||
|
const finalCardHTML = isARBoard
|
||||||
|
? await createARCardHTML(
|
||||||
|
cardObj.cardDataResponse,
|
||||||
|
pollResults,
|
||||||
|
cardObj.identifier,
|
||||||
|
commentCount,
|
||||||
|
cardUpdatedTime,
|
||||||
|
bgColor,
|
||||||
|
cardObj.cardPublisherAddress,
|
||||||
|
cardObj.isDuplicate
|
||||||
|
)
|
||||||
|
: await createCardHTML(
|
||||||
|
cardObj.cardDataResponse,
|
||||||
|
pollResults,
|
||||||
|
cardObj.identifier,
|
||||||
|
commentCount,
|
||||||
|
cardUpdatedTime,
|
||||||
|
bgColor,
|
||||||
|
cardObj.cardPublisherAddress
|
||||||
|
)
|
||||||
|
|
||||||
|
replaceSkeleton(cardObj.identifier, finalCardHTML)
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading cards:", error)
|
console.error("Error loading cards:", error)
|
||||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>"
|
cardsContainer.innerHTML = "<p>Failed to load cards.</p>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verifyARBoardAddress = async (minterName) => {
|
||||||
|
try {
|
||||||
|
const nameInfo = await getNameInfo(minterName)
|
||||||
|
|
||||||
|
if (!nameInfo) return false
|
||||||
|
const minterAddress = nameInfo.owner
|
||||||
|
const isValid = await getAddressInfo(minterAddress)
|
||||||
|
|
||||||
|
if (!isValid) return false
|
||||||
|
// Then check if they're in the minter group
|
||||||
|
const minterGroup = await fetchMinterGroupMembers()
|
||||||
|
const adminGroup = await fetchMinterGroupAdmins()
|
||||||
|
const minterGroupAddresses = minterGroup.map(m => m.member)
|
||||||
|
const adminGroupAddresses = adminGroup.map(m => m.member)
|
||||||
|
|
||||||
|
return (minterGroupAddresses.includes(minterAddress) ||
|
||||||
|
adminGroupAddresses.includes(minterAddress))
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("verifyARBoardAddress error:", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyVoteSortingData = async (cards, ascending = true) => {
|
||||||
|
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||||
|
const minterAdmins = await fetchMinterGroupAdmins()
|
||||||
|
|
||||||
|
for (const card of cards) {
|
||||||
|
try {
|
||||||
|
const cardDataResponse = await qortalRequest({
|
||||||
|
action: "FETCH_QDN_RESOURCE",
|
||||||
|
name: card.name,
|
||||||
|
service: "BLOG_POST",
|
||||||
|
identifier: card.identifier,
|
||||||
|
})
|
||||||
|
if (!cardDataResponse || !cardDataResponse.poll) {
|
||||||
|
card._adminVotes = 0
|
||||||
|
card._adminYes = 0
|
||||||
|
card._minterVotes = 0
|
||||||
|
card._minterYes = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const pollResults = await fetchPollResults(cardDataResponse.poll);
|
||||||
|
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
|
||||||
|
pollResults,
|
||||||
|
minterGroupMembers,
|
||||||
|
minterAdmins,
|
||||||
|
cardDataResponse.creator,
|
||||||
|
card.identifier
|
||||||
|
)
|
||||||
|
card._adminVotes = adminYes + adminNo
|
||||||
|
card._adminYes = adminYes
|
||||||
|
card._minterVotes = minterYes + minterNo
|
||||||
|
card._minterYes = minterYes
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
|
||||||
|
card._adminVotes = 0
|
||||||
|
card._adminYes = 0
|
||||||
|
card._minterVotes = 0
|
||||||
|
card._minterYes = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ascending) {
|
||||||
|
// least votes first
|
||||||
|
cards.sort((a, b) => {
|
||||||
|
const diffAdminTotal = a._adminVotes - b._adminVotes
|
||||||
|
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||||
|
const diffAdminYes = a._adminYes - b._adminYes
|
||||||
|
if (diffAdminYes !== 0) return diffAdminYes
|
||||||
|
const diffMinterTotal = a._minterVotes - b._minterVotes
|
||||||
|
if (diffMinterTotal !== 0) return diffMinterTotal
|
||||||
|
return a._minterYes - b._minterYes
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// most votes first
|
||||||
|
cards.sort((a, b) => {
|
||||||
|
const diffAdminTotal = b._adminVotes - a._adminVotes
|
||||||
|
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||||
|
const diffAdminYes = b._adminYes - a._adminYes
|
||||||
|
if (diffAdminYes !== 0) return diffAdminYes
|
||||||
|
const diffMinterTotal = b._minterVotes - a._minterVotes
|
||||||
|
if (diffMinterTotal !== 0) return diffMinterTotal
|
||||||
|
return b._minterYes - a._minterYes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const removeSkeleton = (cardIdentifier) => {
|
const removeSkeleton = (cardIdentifier) => {
|
||||||
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
|
const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
|
||||||
if (skeletonCard) {
|
if (skeletonCard) {
|
||||||
|
@ -802,9 +802,9 @@ const searchAllWithOffset = async (service, query, limit, offset, room) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// NOTE - This function does a search and will return EITHER AN ARRAY OR A SINGLE OBJECT. if you want to guarantee a single object, pass 1 as limit. i.e. await searchSimple(service, identifier, "", 1) will return a single object.
|
// NOTE - This function does a search and will return EITHER AN ARRAY OR A SINGLE OBJECT. if you want to guarantee a single object, pass 1 as limit. i.e. await searchSimple(service, identifier, "", 1) will return a single object.
|
||||||
const searchSimple = async (service, identifier, name, limit=1500, offset=0, room='', reverse=true, prefixOnly=true) => {
|
const searchSimple = async (service, identifier, name, limit=1500, offset=0, room='', reverse=true, prefixOnly=true, after=0) => {
|
||||||
try {
|
try {
|
||||||
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}`
|
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}&fter=${after}`
|
||||||
|
|
||||||
if (name && !identifier && !room) {
|
if (name && !identifier && !room) {
|
||||||
console.log('name only searchSimple', name)
|
console.log('name only searchSimple', name)
|
||||||
|
50
index.html
50
index.html
@ -42,7 +42,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.02b)
|
<a class="navbar-caption display-4" href="index.html">Q-Mintership (v1.04b)
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -61,12 +61,12 @@
|
|||||||
<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.02b<br></a></span>
|
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership v1.04b<br></a></span>
|
||||||
</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>
|
||||||
|
|
||||||
|
|
||||||
<div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn admin-btn btn-secondary display-4" href="TOOLS">ADMIN TOOLS</a><a class="btn admin-btn btn-secondary display-4" href="ADMINBOARD">ADMIN BOARD</a><a class="btn btn-danger display-4" href="MINTERS">MINTER BOARD</a><a class="btn btn-danger display-4" href="ADDREMOVEADMIN">ARA BOARD</a></div>
|
<div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn admin-btn btn-secondary display-4" href="TOOLS">ADMIN TOOLS</a><a class="btn admin-btn btn-secondary display-4" href="ADMINBOARD">ADMIN BOARD</a><a class="btn btn-danger display-4" href="MINTERS">MINTER BOARD</a><a class="btn btn-danger display-4" href="ADDREMOVEADMIN">MAM Board</a></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -93,7 +93,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">Minting Rights? - Start Here - MinterBoard</h2>
|
<h2 class="card-title mbr-fonts-style display-2">MinterBoard</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@ -109,7 +109,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">
|
||||||
Promote / Demote Minter Admins</h2>
|
Minter Admin Management (MAM) Board</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@ -191,6 +191,42 @@
|
|||||||
|
|
||||||
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
|
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-7 card">
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<h2 class="mbr-section-title mbr-fonts-style display-2">
|
||||||
|
v1.04beta 01-27-2025</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-5 card">
|
||||||
|
<div class="text-wrapper">
|
||||||
|
<p class="mbr-text mbr-fonts-style display-7">
|
||||||
|
<b><u>v1.04b Fixes</u></b>- <b>MANY fixes </b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for details, too many to list here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-7 card">
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<h2 class="mbr-section-title mbr-fonts-style display-2">
|
||||||
|
v1.03beta 01-23-2025</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-5 card">
|
||||||
|
<div class="text-wrapper">
|
||||||
|
<p class="mbr-text mbr-fonts-style display-7">
|
||||||
|
<b><u>v1.03b Fixes</u></b>- <b>Filtering issue resolved </b> - Version 1.02 had a filtering logic modification applied to add and remove admin transactions. However, it was not changed on the REMOVE filtering, so REMOVE transactions that were PENDING were showing as COMPLETE and thus the board was displaying cards as already removed when there was only a PENDING tx. This has been resolved in 1.03b.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-7 card">
|
<div class="col-12 col-lg-7 card">
|
||||||
@ -536,12 +572,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.02b)</h2>
|
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership (v1.04b)</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.02beta</p>
|
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v1.04beta</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
|
Loading…
Reference in New Issue
Block a user