main #5
BIN
assets/images/background1.jpg
Normal file
BIN
assets/images/background1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 262 KiB |
BIN
assets/images/mbr-1-1818x1212.jpg
Normal file
BIN
assets/images/mbr-1-1818x1212.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 133 KiB |
BIN
assets/images/mbr-1818x1212.jpg
Normal file
BIN
assets/images/mbr-1818x1212.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 241 KiB |
890
assets/js/ARBoard.js
Normal file
890
assets/js/ARBoard.js
Normal file
@ -0,0 +1,890 @@
|
||||
|
||||
let minterGroupAddresses
|
||||
let minterAdminAddresses
|
||||
let isTest = false
|
||||
let isAddRemoveBoard = true
|
||||
let otherPublisher = false
|
||||
const addRemoveIdentifierPrefix = "QM-AR-card"
|
||||
const loadAddRemoveAdminPage = async () => {
|
||||
console.log("Loading Add/Remove Admin page...")
|
||||
const bodyChildren = document.body.children
|
||||
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i]
|
||||
|
||||
if (!child.classList.contains("menu")) {
|
||||
child.remove()
|
||||
}
|
||||
}
|
||||
|
||||
const mainContainer = document.createElement("div")
|
||||
mainContainer.className = "add-remove-admin-main"
|
||||
mainContainer.style = "padding: 20px; text-align: center;"
|
||||
mainContainer.innerHTML = `
|
||||
<h1 style="color: lightblue;">Minter Admin Management</h1>
|
||||
<p style="font-size:0.95rem; color: white;">
|
||||
This page allows proposing the promotion of an existing minter to admin,
|
||||
or demotion of an existing admin back to a normal minter.
|
||||
</p>
|
||||
|
||||
<div id="admin-table-section" class="admin-table-section" style="margin-top: 2em;">
|
||||
<h3 style="color:rgb(212, 212, 212);">Existing Minter Admins</h3>
|
||||
<div id="admin-list-container" style="margin: 1em auto; max-width: 600px;"></div>
|
||||
</div>
|
||||
|
||||
<div id="promotion-section" class="promotion-section" style="margin-top: 3em;">
|
||||
<button id="propose-promotion-button" style="padding: 10px; color: white; background:rgb(7, 73, 71) ; cursor: pointer; border-radius: 5px;">
|
||||
Propose a Minter for Admin Position
|
||||
</button>
|
||||
<div id="promotion-form-container" class="publish-card-view" style="display: none; margin-top: 1em;">
|
||||
<form id="publish-card-form">
|
||||
<h3>Create or Update Promotion/Demotion Proposal Card</h3>
|
||||
<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>
|
||||
<label for="card-header">Header:</label>
|
||||
<input type="text" id="card-header" maxlength="100" placeholder="Header / Headline info" required>
|
||||
<label for="card-content">Content:</label>
|
||||
<textarea id="card-content" placeholder="Enter detailed information about why you are making this proposal for promotion/demotion. You may utilize links to additional data as well." required></textarea>
|
||||
<label for="card-links">Links (qortal://...):</label>
|
||||
<div id="links-container">
|
||||
<input type="text" class="card-link" placeholder="Enter QDN link">
|
||||
</div>
|
||||
<button type="button" id="add-link-button">Add Another Link</button>
|
||||
<button type="submit" id="submit-publish-button">Publish Card</button>
|
||||
<button type="button" id="cancel-publish-button">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||
<h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3>
|
||||
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button>
|
||||
</div>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 1rem"">
|
||||
<!-- We'll fill this with existing proposal cards -->
|
||||
</div>
|
||||
|
||||
`
|
||||
|
||||
document.body.appendChild(mainContainer)
|
||||
|
||||
document.getElementById("propose-promotion-button").addEventListener("click", async () => {
|
||||
try {
|
||||
// Show the form
|
||||
const publishCardView = document.getElementById("promotion-form-container")
|
||||
publishCardView.style.display = 'flex'
|
||||
// publishCardView.style.display === "none" ? "flex" : "none"
|
||||
// document.getElementById("existing-proposals-section").style.display = "none"
|
||||
const proposeButton = document.getElementById('propose-promotion-button')
|
||||
proposeButton.style.display = 'none'
|
||||
// proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error checking for existing card:", error)
|
||||
alert("Failed to check for existing card. Please try again.")
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById("refresh-cards-button").addEventListener("click", async () => {
|
||||
const cardsContainer = document.getElementById("cards-container")
|
||||
cardsContainer.innerHTML = "<p>Refreshing cards...</p>"
|
||||
await loadCards(addRemoveIdentifierPrefix)
|
||||
})
|
||||
|
||||
document.getElementById("cancel-publish-button").addEventListener("click", async () => {
|
||||
// const cardsContainer = document.getElementById("existing-proposals-section")
|
||||
// cardsContainer.style.display = "flex" // Restore visibility
|
||||
const publishCardView = document.getElementById("promotion-form-container")
|
||||
publishCardView.style.display = "none" // Hide the publish form
|
||||
const proposeButton = document.getElementById('propose-promotion-button')
|
||||
proposeButton.style.display = 'flex'
|
||||
// proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
||||
})
|
||||
|
||||
document.getElementById("add-link-button").addEventListener("click", async () => {
|
||||
const linksContainer = document.getElementById("links-container")
|
||||
const newLinkInput = document.createElement("input")
|
||||
newLinkInput.type = "text"
|
||||
newLinkInput.className = "card-link"
|
||||
newLinkInput.placeholder = "Enter QDN link"
|
||||
linksContainer.appendChild(newLinkInput)
|
||||
})
|
||||
|
||||
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault()
|
||||
await publishARCard(addRemoveIdentifierPrefix)
|
||||
})
|
||||
await featureTriggerCheck()
|
||||
await loadCards(addRemoveIdentifierPrefix)
|
||||
await displayExistingMinterAdmins()
|
||||
await fetchAllARTxData()
|
||||
}
|
||||
|
||||
const toggleProposeButton = () => {
|
||||
const proposeButton = document.getElementById('propose-promotion-button')
|
||||
proposeButton.style.display =
|
||||
proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
||||
}
|
||||
|
||||
let addAdminTxs
|
||||
let remAdminTxs
|
||||
|
||||
const fetchAllARTxData = async () => {
|
||||
const addAdmTx = "ADD_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({
|
||||
txTypes: [addAdmTx],
|
||||
confirmationStatus: 'CONFIRMED',
|
||||
limit: 0,
|
||||
reverse: true,
|
||||
offset: 0,
|
||||
startBlock: 1990000,
|
||||
blockLimit: 0,
|
||||
txGroupId: 694,
|
||||
})
|
||||
// Filter out 'PENDING'
|
||||
addAdminTxs = filterAddTransactions(allAddTxs)
|
||||
console.warn('addAdminTxData (no PENDING nor past+PENDING):', addAdminTxs)
|
||||
|
||||
// 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 adminListContainer = document.getElementById("admin-list-container")
|
||||
adminListContainer.innerHTML =
|
||||
"<p style='color: #999; font-size: 1.1rem;'>Loading existing admins...</p>"
|
||||
|
||||
try {
|
||||
// 1) Fetch addresses
|
||||
const admins = await fetchMinterGroupAdmins()
|
||||
minterAdminAddresses = admins.map(m => m.member)
|
||||
let rowsHtml = "";
|
||||
for (const adminAddr of admins) {
|
||||
if (adminAddr.member === nullAddress) {
|
||||
// Display a "NULL ACCOUNT" row
|
||||
rowsHtml += `
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 4px; color: #aaa;">
|
||||
NULL ACCOUNT
|
||||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 4px; color: #aaa;">
|
||||
${nullAddress}
|
||||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 4px; color: #aaa;">
|
||||
<!-- No button, or a dash. -->
|
||||
—
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
continue
|
||||
}
|
||||
// Attempt to get name
|
||||
let adminName
|
||||
try {
|
||||
adminName = await getNameFromAddress(adminAddr.member)
|
||||
} catch (err) {
|
||||
console.warn(`Error fetching name for ${adminAddr.member}:`, err)
|
||||
adminName = null
|
||||
}
|
||||
const displayName = adminName && adminName !== adminAddr.member ? adminName : "(No Name)"
|
||||
rowsHtml += `
|
||||
<tr>
|
||||
<td style="border: 1px solid rgb(150, 199, 224); font-size: 1.5rem; padding: 4px; color:rgb(70, 156, 196)">${displayName}</td>
|
||||
<td style="border: 1px solid rgb(106, 203, 179); font-size: 1rem; padding: 4px; color:rgb(120, 150, 163);">${adminAddr.member}</td>
|
||||
<td style="border: 1px solid rgb(231, 112, 112); padding: 4px;">
|
||||
<button
|
||||
style="padding: 5px; background: red; color: white; border-radius: 3px; cursor: pointer;"
|
||||
onclick="handleProposeDemotionWrapper('${adminName}', '${adminAddr.member}')"
|
||||
>
|
||||
Propose Demotion
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
// 3) Build the table
|
||||
const tableHtml = `
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background:rgb(21, 36, 18); color:rgb(183, 208, 173); font-size: 1.5rem;">
|
||||
<th style="border: 1px solid rgb(34, 118, 129); padding: 4px;">Admin Name</th>
|
||||
<th style="border: 1px solid rgb(90, 122, 122); padding: 4px;">Admin Address</th>
|
||||
<th style="border: 1px solid rgb(138, 49, 49); padding: 4px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rowsHtml}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
adminListContainer.innerHTML = tableHtml
|
||||
} catch (err) {
|
||||
console.error("Error fetching minter admins:", err)
|
||||
adminListContainer.innerHTML =
|
||||
"<p style='color: red;'>Failed to load admins.</p>"
|
||||
}
|
||||
}
|
||||
|
||||
const handleProposeDemotionWrapper = (adminName, adminAddress) => {
|
||||
// Call the async function and handle any unhandled rejections
|
||||
handleProposeDemotion(adminName, adminAddress).catch(error => {
|
||||
console.error(`Error in handleProposeDemotionWrapper:`, error)
|
||||
alert("An unexpected error occurred. Please try again.")
|
||||
})
|
||||
}
|
||||
|
||||
const handleProposeDemotion = async (adminName, adminAddress) => {
|
||||
console.log(`Proposing demotion for: ${adminName} (${adminAddress})`)
|
||||
const proposeButton = document.getElementById('propose-promotion-button')
|
||||
proposeButton.style.display = 'none'
|
||||
const fetchedCard = await fetchExistingARCard(addRemoveIdentifierPrefix, adminName)
|
||||
|
||||
if (fetchedCard) {
|
||||
alert("A card already exists. Publishing of multiple cards is not allowed. Please update your card.")
|
||||
isExistingCard = true
|
||||
await loadCardIntoForm(fetchedCard)
|
||||
}
|
||||
// Populate the form with the admin's name
|
||||
const nameInput = document.getElementById("minter-name-input")
|
||||
nameInput.value = adminName
|
||||
|
||||
// Display the form if it's hidden
|
||||
const formContainer = document.getElementById("promotion-form-container")
|
||||
formContainer.style.display = "flex"
|
||||
|
||||
// Optionally hide other sections (e.g., the existing proposals section)
|
||||
// const proposalsSection = document.getElementById("existing-proposals-section")
|
||||
// proposalsSection.style.display = "none"
|
||||
|
||||
// Notify the user to fill out the rest
|
||||
alert(`Admin "${adminName}" has been selected for demotion. Please fill out the rest of the form.`)
|
||||
}
|
||||
|
||||
const fetchExistingARCard = async (cardIdentifierPrefix, minterName) => {
|
||||
try {
|
||||
const response = await searchSimple(
|
||||
'BLOG_POST',
|
||||
`${cardIdentifierPrefix}`,
|
||||
'',
|
||||
0,
|
||||
0,
|
||||
'',
|
||||
false,
|
||||
true
|
||||
)
|
||||
|
||||
console.log(`fetchExistingCard searchSimple response: ${JSON.stringify(response, null, 2)}`)
|
||||
|
||||
if (!response || !Array.isArray(response) || response.length === 0) {
|
||||
console.log("No cards found.")
|
||||
return null
|
||||
}
|
||||
|
||||
const validatedCards = await Promise.all(
|
||||
response.map(async (card) => {
|
||||
const isValid = await validateCardStructure(card)
|
||||
|
||||
if (!isValid) return null
|
||||
// Fetch full card data for validation
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
})
|
||||
|
||||
if (cardDataResponse.minterName === minterName) {
|
||||
console.log(`Card with the same minterName found: ${minterName}`)
|
||||
if (cardDataResponse.creator === userState.accountName) {
|
||||
console.log(`The user is the publisher, adding card...`)
|
||||
return {
|
||||
card,
|
||||
cardData: cardDataResponse,
|
||||
}
|
||||
} else {
|
||||
console.warn(`Card found, but user is not the creator!`)
|
||||
otherPublisher = true
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
)
|
||||
// Filter out null results and check for duplicates
|
||||
const matchingCards = validatedCards.filter((result) => result !== null)
|
||||
|
||||
if (matchingCards.length > 0) {
|
||||
const { card, cardData } = matchingCards[0] // Use the first matching card, which should be the first published for the minterName
|
||||
existingCardIdentifier = card.identifier
|
||||
existingCardData = cardData
|
||||
isExistingCard = true
|
||||
|
||||
return {
|
||||
cardData
|
||||
}
|
||||
}
|
||||
|
||||
console.log("No valid cards found or no matching minterName.")
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error("Error fetching existing AR card:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const publishARCard = async (cardIdentifierPrefix) => {
|
||||
const minterNameInput = document.getElementById("minter-name-input").value.trim()
|
||||
const potentialNameInfo = await getNameInfo(minterNameInput)
|
||||
let minterName
|
||||
let address
|
||||
let isPromotionCard
|
||||
|
||||
if (potentialNameInfo.owner) {
|
||||
console.log(`MINTER NAME FOUND:`, minterNameInput)
|
||||
minterName = minterNameInput
|
||||
address = potentialNameInfo.owner
|
||||
|
||||
} else {
|
||||
console.warn(`user input an address?...`, minterNameInput)
|
||||
if (!address){
|
||||
const validAddress = await getAddressInfo(minterNameInput)
|
||||
if (validAddress){
|
||||
address = minterNameInput
|
||||
} else {
|
||||
console.error(`input address by user INVALID`, minterNameInput)
|
||||
alert(`You have input an invalid address! Please try again...`)
|
||||
return
|
||||
}
|
||||
}
|
||||
const checkForName = await getNameFromAddress(minterNameInput)
|
||||
|
||||
if (checkForName) {
|
||||
minterName = checkForName
|
||||
} else if (!checkForName && address){
|
||||
console.warn(`user input an address that has no name...`)
|
||||
alert(`you have input an address that has no name, the address will need to register a name prior to being able to be promoted`)
|
||||
return
|
||||
} else {
|
||||
console.warn(`Input was either an invalid name, or incorrect address?`, minterNameInput)
|
||||
alert(`Your input could not be validated, check the name/address and try again!`)
|
||||
return
|
||||
}
|
||||
}
|
||||
const exists = await fetchExistingARCard(cardIdentifierPrefix, minterName)
|
||||
|
||||
if (exists) {
|
||||
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)
|
||||
minterName = exists.minterName
|
||||
const nameInfo = await getNameInfo(exists.minterName)
|
||||
address = nameInfo.owner
|
||||
isExistingCard = true
|
||||
} else if (otherPublisher){
|
||||
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}`)
|
||||
return
|
||||
}
|
||||
|
||||
const minterGroupData = await fetchMinterGroupMembers()
|
||||
minterGroupAddresses = minterGroupData.map(m => m.member)
|
||||
|
||||
const minterAdminGroupData = await fetchMinterGroupAdmins()
|
||||
minterAdminAddresses = minterAdminGroupData.map(m => m.member)
|
||||
|
||||
if (minterAdminAddresses.includes(address)){
|
||||
isPromotionCard = false
|
||||
console.warn(`this is a DEMOTION`, address)
|
||||
}else if (minterGroupAddresses.includes(address)) {
|
||||
isPromotionCard = true
|
||||
console.warn(`address is a MINTER, this is a promotion card...`)
|
||||
}
|
||||
|
||||
if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) {
|
||||
console.error(`you cannot publish a card here unless the user is a MINTER or an ADMIN!`)
|
||||
alert(`Card cannot be published for an account that is neither a minter nor an admin! This board is for Promotions and Demotions of Admins ONLY!`)
|
||||
return
|
||||
}
|
||||
|
||||
const header = document.getElementById("card-header").value.trim()
|
||||
const content = document.getElementById("card-content").value.trim()
|
||||
const links = Array.from(document.querySelectorAll(".card-link"))
|
||||
.map(input => input.value.trim())
|
||||
.filter(link => link.startsWith("qortal://"))
|
||||
|
||||
if (!header || !content) {
|
||||
alert("Header and content are required!")
|
||||
return
|
||||
}
|
||||
|
||||
const cardIdentifier = isExistingCard ? existingCardIdentifier : `${cardIdentifierPrefix}-${await uid()}`
|
||||
const pollName = `${cardIdentifier}-poll`
|
||||
const pollDescription = `AR Board Card Proposed By: ${userState.accountName}`
|
||||
|
||||
const cardData = {
|
||||
minterName,
|
||||
header,
|
||||
content,
|
||||
links,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: pollName,
|
||||
promotionCard: isPromotionCard
|
||||
}
|
||||
|
||||
try {
|
||||
let base64CardData = await objectToBase64(cardData)
|
||||
if (!base64CardData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`)
|
||||
base64CardData = btoa(JSON.stringify(cardData))
|
||||
}
|
||||
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64CardData,
|
||||
})
|
||||
|
||||
if (!isExistingCard){
|
||||
await qortalRequest({
|
||||
action: "CREATE_POLL",
|
||||
pollName,
|
||||
pollDescription,
|
||||
pollOptions: ['Yes, No'],
|
||||
pollOwnerAddress: userState.accountAddress,
|
||||
})
|
||||
alert("Card and poll published successfully!")
|
||||
}
|
||||
|
||||
if (isExistingCard){
|
||||
alert("Card Updated Successfully! (No poll updates are possible at this time...)")
|
||||
isExistingCard = false
|
||||
}
|
||||
|
||||
if (isPromotionCard){
|
||||
isPromotionCard = false
|
||||
}
|
||||
|
||||
document.getElementById("publish-card-form").reset()
|
||||
document.getElementById("promotion-form-container").style.display = "none"
|
||||
// document.getElementById("cards-container").style.display = "flex"
|
||||
|
||||
await loadCards(addRemoveIdentifierPrefix)
|
||||
|
||||
} catch (error) {
|
||||
|
||||
console.error("Error publishing card or poll:", error)
|
||||
alert("Failed to publish card and poll.")
|
||||
}
|
||||
}
|
||||
|
||||
const checkAndDisplayActions = async (adminYes, name, cardIdentifier) => {
|
||||
const latestBlockInfo = await getLatestBlockInfo()
|
||||
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
|
||||
let minAdminCount
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
|
||||
if ((minterAdmins) && (minterAdmins.length === 1)){
|
||||
console.warn(`simply a double-check that there is only one MINTER group admin, in which case the group hasn't been transferred to null...keeping default minAdminCount of: ${minAdminCount}`)
|
||||
minAdminCount = 9
|
||||
} else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){
|
||||
const totalAdmins = minterAdmins.length
|
||||
const fortyPercent = totalAdmins * 0.40
|
||||
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}`)
|
||||
}
|
||||
const addressInfo = await getNameInfo(name)
|
||||
const address = addressInfo.owner
|
||||
|
||||
if (isBlockPassed) {
|
||||
console.warn(`feature trigger has passed, checking for approval requirements`)
|
||||
const addAdminApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "ADD_GROUP_ADMIN")
|
||||
const removeAdminApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "REMOVE_GROUP_ADMIN")
|
||||
|
||||
if (addAdminApprovalHtml) {
|
||||
return addAdminApprovalHtml
|
||||
}
|
||||
|
||||
if (removeAdminApprovalHtml) {
|
||||
return removeAdminApprovalHtml
|
||||
}
|
||||
}
|
||||
|
||||
if (!minterGroupAddresses) {
|
||||
const minterGroupData = await fetchMinterGroupMembers()
|
||||
minterGroupAddresses = minterGroupData.map(m => m.member)
|
||||
}
|
||||
|
||||
if (!minterAdminAddresses) {
|
||||
const adminAddressData = await fetchMinterGroupAdmins()
|
||||
minterAdminAddresses = adminAddressData.map(m => m.member)
|
||||
}
|
||||
|
||||
if (!minterGroupAddresses.includes(userState.accountAddress)){
|
||||
console.warn(`User is not in the MINTER group, no need for buttons`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (adminYes >= minAdminCount && (minterAdminAddresses.includes(address))){
|
||||
const removeAdminHtml = createRemoveAdminButton(name, cardIdentifier, address)
|
||||
return removeAdminHtml
|
||||
} else if (adminYes >= minAdminCount && (minterGroupAddresses.includes(address))){
|
||||
const addAdminHtml = createAddAdminButton(name, cardIdentifier, address)
|
||||
return addAdminHtml
|
||||
}
|
||||
}
|
||||
|
||||
const createAddAdminButton = (name, cardIdentifier, address) => {
|
||||
return `
|
||||
<div id="add-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||
<button onclick="handleAddMinterGroupAdmin('${name}','${address}')"
|
||||
style="padding: 10px; background: rgb(4, 119, 134); color: white; border: none; cursor: pointer; border-radius: 5px;"
|
||||
onmouseover="this.style.backgroundColor='rgb(11, 47, 24) '"
|
||||
onmouseout="this.style.backgroundColor='rgb(4, 123, 134) '">
|
||||
Create ADD_GROUP_ADMIN Tx
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const createRemoveAdminButton = (name, cardIdentifier, address) => {
|
||||
return `
|
||||
<div id="add-button-container-${cardIdentifier}" style="margin-top: 1em;">
|
||||
<button onclick="handleRemoveMinterGroupAdmin('${name}','${address}')"
|
||||
style="padding: 10px; background: rgb(134, 4, 4); color: white; border: none; cursor: pointer; border-radius: 5px;"
|
||||
onmouseover="this.style.backgroundColor='rgb(0, 0, 0) '"
|
||||
onmouseout="this.style.backgroundColor='rgb(134, 4, 4) '">
|
||||
Create REMOVE_GROUP_ADMIN Tx
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const handleAddMinterGroupAdmin = async (name, address) => {
|
||||
try {
|
||||
// Optional block check
|
||||
let txGroupId = 0
|
||||
let member = address
|
||||
// const { height: currentHeight } = await getLatestBlockInfo()
|
||||
const isBlockPassed = await featureTriggerCheck()
|
||||
if (isBlockPassed) {
|
||||
console.log(`block height above featureTrigger Height, using group approval method...txGroupId 694`)
|
||||
txGroupId = 694
|
||||
}
|
||||
|
||||
const ownerPublicKey = await getPublicKeyFromAddress(userState.accountAddress)
|
||||
const fee = 0.01
|
||||
|
||||
const rawTx = await createAddGroupAdminTransaction(ownerPublicKey, 694, member, txGroupId, fee)
|
||||
|
||||
const signedTx = await qortalRequest({
|
||||
action: "SIGN_TRANSACTION",
|
||||
unsignedBytes: rawTx
|
||||
})
|
||||
|
||||
if (!signedTx) {
|
||||
console.warn(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added?`)
|
||||
alert(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added? Please talk to developers.`)
|
||||
return
|
||||
}
|
||||
|
||||
let txToProcess = signedTx
|
||||
|
||||
const processTx = await processTransaction(txToProcess)
|
||||
|
||||
if (typeof processTx === 'object') {
|
||||
console.log("transaction success object:", processTx)
|
||||
alert(`${name} kick successfully issued! Wait for confirmation...Transaction Response: ${JSON.stringify(processTx)}`)
|
||||
} else {
|
||||
console.log("transaction raw text response:", processTx)
|
||||
alert(`TxResponse: ${JSON.stringify(processTx)}`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error removing minter:", error)
|
||||
alert(`Error:${error}. Please try again.`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveMinterGroupAdmin = async (name, address) => {
|
||||
try {
|
||||
// Optional block check
|
||||
let txGroupId = 0
|
||||
const admin = address
|
||||
// const { height: currentHeight } = await getLatestBlockInfo()
|
||||
const isBlockPassed = await featureTriggerCheck()
|
||||
if (isBlockPassed) {
|
||||
console.log(`block height above featureTrigger Height, using group approval method...txGroupId 694`)
|
||||
txGroupId = 694
|
||||
}
|
||||
|
||||
const ownerPublicKey = await getPublicKeyFromAddress(userState.accountAddress)
|
||||
const fee = 0.01
|
||||
|
||||
const rawTx = await createRemoveGroupAdminTransaction(ownerPublicKey, 694, admin, txGroupId, fee)
|
||||
|
||||
const signedTx = await qortalRequest({
|
||||
action: "SIGN_TRANSACTION",
|
||||
unsignedBytes: rawTx
|
||||
})
|
||||
if (!signedTx) {
|
||||
console.warn(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added?`)
|
||||
alert(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added? Please talk to developers.`)
|
||||
return
|
||||
}
|
||||
|
||||
let txToProcess = signedTx
|
||||
|
||||
const processTx = await processTransaction(txToProcess)
|
||||
|
||||
if (typeof processTx === 'object') {
|
||||
console.log("transaction success object:", processTx)
|
||||
alert(`${name} kick successfully issued! Wait for confirmation...Transaction Response: ${JSON.stringify(processTx)}`)
|
||||
} else {
|
||||
console.log("transaction raw text response:", processTx)
|
||||
alert(`TxResponse: ${JSON.stringify(processTx)}`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error removing minter:", error)
|
||||
alert(`Error:${error}. Please try again.`)
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins) => {
|
||||
// Ensure we have addresses
|
||||
if (!minterGroupMembers) {
|
||||
console.warn("No minterGroupMembers array was passed in fallback check!")
|
||||
return false
|
||||
}
|
||||
const minterGroupAddresses = minterGroupMembers.map(m => m.member)
|
||||
const adminAddresses = minterAdmins.map(m => m.member)
|
||||
const minterAcctInfo = await getNameInfo(minterName)
|
||||
if (!minterAcctInfo || !minterAcctInfo.owner) {
|
||||
console.warn(`Name info not found or missing 'owner' for ${minterName}`)
|
||||
return false
|
||||
}
|
||||
// If user is already in the group => we call it a "promotion card"
|
||||
if (adminAddresses.includes(minterAcctInfo.owner)) {
|
||||
console.warn(`display check found minterAdminCard - NOT a promotion card...`)
|
||||
return false
|
||||
} else {
|
||||
return minterGroupAddresses.includes(minterAcctInfo.owner)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
|
||||
const { minterName, header, content, links, creator, timestamp, poll, promotionCard } = cardData
|
||||
const formattedDate = new Date(timestamp).toLocaleString()
|
||||
const minterAvatar = await getMinterAvatar(minterName)
|
||||
const creatorAvatar = await getMinterAvatar(creator)
|
||||
const linksHTML = links.map((link, index) => `
|
||||
<button onclick="openLinkDisplayModal('${link}')">
|
||||
${`Link ${index + 1} - ${link}`}
|
||||
</button>
|
||||
`).join("")
|
||||
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
|
||||
let showPromotionCard = false
|
||||
// showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers, minterAdmins)
|
||||
|
||||
if (typeof promotionCard === 'boolean') {
|
||||
showPromotionCard = promotionCard;
|
||||
} else if (typeof promotionCard === 'string') {
|
||||
// Could be "true" or "false" or something else
|
||||
const lower = promotionCard.trim().toLowerCase()
|
||||
if (lower === "true") {
|
||||
showPromotionCard = true
|
||||
} else if (lower === "false") {
|
||||
showPromotionCard = false
|
||||
} else {
|
||||
// Unexpected string => fallback
|
||||
console.warn(`Unexpected string in promotionCard="${promotionCard}"`)
|
||||
showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers)
|
||||
}
|
||||
} else if (promotionCard == null) {
|
||||
// null or undefined => fallback check
|
||||
console.warn(`No promotionCard field in card data, doing manual check...`)
|
||||
showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers)
|
||||
} else {
|
||||
// If it’s an object or something else weird => fallback
|
||||
console.warn(`promotionCard has unexpected type, fallback...`)
|
||||
showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers)
|
||||
}
|
||||
|
||||
let cardColorCode = (showPromotionCard) ? 'rgb(17, 44, 46)' : 'rgb(57, 11, 13)'
|
||||
|
||||
const promotionDemotionHtml = (showPromotionCard) ? `
|
||||
<div class="support-header"><h5> REGARDING (Promotion): </h5></div>
|
||||
${minterAvatar}
|
||||
<h3>${minterName}</h3>` :
|
||||
`
|
||||
<div class="support-header"><h5> REGARDING (Demotion): </h5></div>
|
||||
${minterAvatar}
|
||||
<h3>${minterName}</h3>`
|
||||
|
||||
if (!promotionDemotionHtml){
|
||||
console.warn(`promotionDemotionHtml missing!`)
|
||||
}
|
||||
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier)
|
||||
|
||||
createModal('links')
|
||||
createModal('poll-details')
|
||||
|
||||
let actionsHtml = ''
|
||||
let altText = ''
|
||||
const verifiedName = await validateMinterName(minterName)
|
||||
|
||||
if (verifiedName) {
|
||||
const accountInfo = await getNameInfo(verifiedName)
|
||||
const accountAddress = accountInfo.owner
|
||||
console.log(`name is validated, utilizing for removal features...${verifiedName}`)
|
||||
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
|
||||
actionsHtml = actionsHtmlCheck
|
||||
|
||||
if (!addAdminTxs || !remAdminTxs) {
|
||||
await fetchAllARTxData()
|
||||
}
|
||||
|
||||
if (addAdminTxs.some((addTx) => addTx.groupId === 694 && addTx.member === accountAddress)){
|
||||
console.warn(`account was already adminified(PROMOTED), displaying as such...`)
|
||||
cardColorCode = 'rgb(3, 11, 24)'
|
||||
altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`
|
||||
actionsHtml = ''
|
||||
// =============================================================== if 'showPromotedDemoted' is wanted to be added.
|
||||
// if (!showPromotedDemoted){
|
||||
// console.warn(`promoted/demoted show checkbox is unchecked, and card is promoted, not displaying...`)
|
||||
// return ''
|
||||
// }
|
||||
}
|
||||
|
||||
if (remAdminTxs.some((remTx) => remTx.groupId === 694 && remTx.admin === accountAddress)){
|
||||
console.warn(`account was already UNadminified(DEMOTED), displaying as such...`)
|
||||
cardColorCode = 'rgb(29, 4, 6)'
|
||||
altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
|
||||
actionsHtml = ''
|
||||
|
||||
// if (!showPromotedDemoted) {
|
||||
// console.warn(`promoted/demoted show checkbox is unchecked, card is demoted, not displaying...`)
|
||||
// return ''
|
||||
// }
|
||||
}
|
||||
|
||||
} else {
|
||||
console.warn(`name could not be validated, not setting actionsHtml`)
|
||||
actionsHtml = ''
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="admin-card" style="background-color: ${cardColorCode}">
|
||||
<div class="minter-card-header">
|
||||
<h2 class="support-header"> Created By: </h2>
|
||||
${creatorAvatar}
|
||||
<h2>${creator}</h2>
|
||||
${promotionDemotionHtml}
|
||||
<p>${header}</p>
|
||||
${altText}
|
||||
</div>
|
||||
<div class="info">
|
||||
${content}
|
||||
</div>
|
||||
<div class="support-header"><h5>LINKS</h5></div>
|
||||
<div class="info-links">
|
||||
${linksHTML}
|
||||
</div>
|
||||
<div class="results-header support-header"><h5>CURRENT RESULTS</h5></div>
|
||||
<div class="minter-card-results">
|
||||
<button onclick="togglePollDetails('${cardIdentifier}')">Display Poll Details</button>
|
||||
<div id="poll-details-${cardIdentifier}" style="display: none;">
|
||||
${detailsHtml}
|
||||
</div>
|
||||
${actionsHtml}
|
||||
<div class="admin-results">
|
||||
<span class="admin-yes">Admin Support: ${adminYes}</span>
|
||||
<span class="admin-no">Admin Against: ${adminNo}</span>
|
||||
</div>
|
||||
<div class="minter-results">
|
||||
<span class="minter-yes">Minter Yes: ${minterYes}</span>
|
||||
<span class="minter-no">Minter No: ${minterNo}</span>
|
||||
</div>
|
||||
<div class="total-results">
|
||||
<span class="total-yes">Total Yes: ${totalYes}</span>
|
||||
<span class="total-yes">Weight: ${totalYesWeight}</span>
|
||||
<span class="total-no">Total No: ${totalNo}</span>
|
||||
<span class="total-no">Weight: ${totalNoWeight}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="support-header"><h5>ACTIONS FOR</h5><h5 style="color: #ffae42;">${minterName}</h5>
|
||||
<p style="color: #c7c7c7; font-size: .65rem; margin-top: 1vh">(click COMMENTS button to open/close card comments)</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="actions-buttons">
|
||||
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
|
||||
<button id="comment-button-${cardIdentifier}" data-comment-count="${commentCount}" class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
|
||||
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
<button onclick="postComment('${cardIdentifier}')">Post Comment</button>
|
||||
</div>
|
||||
<p style="font-size: 0.75rem; margin-top: 1vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
@ -76,6 +76,13 @@ 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>
|
||||
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Encrypted Card</button>
|
||||
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button>
|
||||
<select id="sort-select" style="margin-left: 10px; padding: 5px;">
|
||||
<option value="newest" selected>Sort by Date</option>
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="recent-comments">Newest Comments</option>
|
||||
<option value="least-votes">Least Votes</option>
|
||||
<option value="most-votes">Most Votes</option>
|
||||
</select>
|
||||
<div class="show-card-checkbox" style="margin-top: 1em;">
|
||||
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
|
||||
<label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label>
|
||||
@ -173,6 +180,11 @@ const loadAdminBoardPage = async () => {
|
||||
await publishEncryptedCard(isTopicChecked)
|
||||
})
|
||||
|
||||
document.getElementById("sort-select").addEventListener("change", async () => {
|
||||
// Re-load the cards whenever user chooses a new sort option.
|
||||
await fetchAllEncryptedCards()
|
||||
})
|
||||
|
||||
createScrollToTopButton()
|
||||
// await fetchAndValidateAllAdminCards()
|
||||
await updateOrSaveAdminGroupsDataLocally()
|
||||
@ -404,12 +416,125 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
||||
// Convert the map into an array of final cards
|
||||
const finalCards = Array.from(mostRecentCardsMap.values());
|
||||
|
||||
let selectedSort = 'newest'
|
||||
const sortSelect = document.getElementById('sort-select')
|
||||
if (sortSelect) {
|
||||
selectedSort = sortSelect.value
|
||||
}
|
||||
|
||||
if (selectedSort === 'name') {
|
||||
// Sort alphabetically by the minter's name
|
||||
finalCards.sort((a, b) => {
|
||||
const nameA = a.decryptedCardData.minterName?.toLowerCase() || ''
|
||||
const nameB = b.decryptedCardData.minterName?.toLowerCase() || ''
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
} else if (selectedSort === 'recent-comments') {
|
||||
// We need each card's newest comment timestamp for sorting
|
||||
for (let card of finalCards) {
|
||||
card.newestCommentTimestamp = await getNewestAdminCommentTimestamp(card.card.identifier)
|
||||
}
|
||||
// Then sort descending by newest comment
|
||||
finalCards.sort((a, b) =>
|
||||
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
||||
)
|
||||
} else if (selectedSort === 'least-votes') {
|
||||
// TODO: Add the logic to sort by LEAST total ADMIN votes, then totalYesWeight
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
for (const finalCard of finalCards) {
|
||||
try {
|
||||
const pollName = finalCard.decryptedCardData.poll
|
||||
// If card or poll is missing, default to zero
|
||||
if (!pollName) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
const pollResults = await fetchPollResults(pollName)
|
||||
if (!pollResults || pollResults.error) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
// Pull only the adminYes/adminNo/totalYesWeight from processPollData
|
||||
const {
|
||||
adminYes,
|
||||
adminNo,
|
||||
totalYesWeight
|
||||
} = await processPollData(
|
||||
pollResults,
|
||||
minterGroupMembers,
|
||||
minterAdmins,
|
||||
finalCard.decryptedCardData.creator,
|
||||
finalCard.card.identifier
|
||||
)
|
||||
finalCard._adminTotalVotes = adminYes + adminNo
|
||||
finalCard._yesWeight = totalYesWeight
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
}
|
||||
}
|
||||
// Sort ascending by (adminYes + adminNo), then descending by totalYesWeight
|
||||
finalCards.sort((a, b) => {
|
||||
const diffAdminTotal = a._adminTotalVotes - b._adminTotalVotes
|
||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||
// If there's a tie, show the card with higher yesWeight first
|
||||
return b._yesWeight - a._yesWeight
|
||||
})
|
||||
} else if (selectedSort === 'most-votes') {
|
||||
// TODO: Add the logic to sort by MOST total ADMIN votes, then totalYesWeight
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
for (const finalCard of finalCards) {
|
||||
try {
|
||||
const pollName = finalCard.decryptedCardData.poll
|
||||
if (!pollName) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
const pollResults = await fetchPollResults(pollName)
|
||||
if (!pollResults || pollResults.error) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
const {
|
||||
adminYes,
|
||||
adminNo,
|
||||
totalYesWeight
|
||||
} = await processPollData(
|
||||
pollResults,
|
||||
minterGroupMembers,
|
||||
minterAdmins,
|
||||
finalCard.decryptedCardData.creator,
|
||||
finalCard.card.identifier
|
||||
)
|
||||
finalCard._adminTotalVotes = adminYes + adminNo
|
||||
finalCard._yesWeight = totalYesWeight
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
}
|
||||
}
|
||||
// Sort descending by (adminYes + adminNo), then descending by totalYesWeight
|
||||
finalCards.sort((a, b) => {
|
||||
const diffAdminTotal = b._adminTotalVotes - a._adminTotalVotes
|
||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||
return b._yesWeight - a._yesWeight
|
||||
})
|
||||
} else {
|
||||
// Sort cards by timestamp (most recent first)
|
||||
finalCards.sort((a, b) => {
|
||||
const timestampA = a.card.updated || a.card.created || 0
|
||||
const timestampB = b.card.updated || b.card.created || 0
|
||||
return timestampB - timestampA;
|
||||
})
|
||||
}
|
||||
|
||||
encryptedCardsContainer.innerHTML = ""
|
||||
|
||||
@ -675,7 +800,7 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
|
||||
publicKeys: verifiedAdminPublicKeys
|
||||
})
|
||||
|
||||
// Possibly create a poll if it’s a brand new card
|
||||
// Possibly create a poll if it's a brand new card
|
||||
if (!isUpdateCard) {
|
||||
await qortalRequest({
|
||||
action: "CREATE_POLL",
|
||||
@ -1080,6 +1205,23 @@ const handleBanMinter = async (minterName) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getNewestAdminCommentTimestamp = async (cardIdentifier) => {
|
||||
try {
|
||||
const comments = await fetchEncryptedComments(cardIdentifier)
|
||||
if (!comments || comments.length === 0) {
|
||||
return 0
|
||||
}
|
||||
const newestTimestamp = comments.reduce((acc, comment) => {
|
||||
const cTime = comment.updated || comment.created || 0
|
||||
return cTime > acc ? cTime : acc
|
||||
}, 0)
|
||||
return newestTimestamp
|
||||
} catch (err) {
|
||||
console.error('Failed to get newest comment timestamp:', err)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Create the overall Minter Card HTML -----------------------------------------------
|
||||
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
|
||||
const { minterName, header, content, links, creator, timestamp, poll, topicMode } = cardData
|
||||
@ -1113,29 +1255,41 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
|
||||
const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
|
||||
<div class="support-header"><h5> REGARDING (Topic): </h5></div>
|
||||
<h3>${minterName}</h3>` :
|
||||
<h3>${minterName}` :
|
||||
`
|
||||
<div class="support-header"><h5> REGARDING (Name): </h5></div>
|
||||
${minterAvatar}
|
||||
<h3>${minterName}</h3>`
|
||||
<h3>${minterName}`
|
||||
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier)
|
||||
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml, userVote = null } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier)
|
||||
|
||||
createModal('links')
|
||||
createModal('poll-details')
|
||||
|
||||
let showRemoveHtml
|
||||
let altText
|
||||
let altText = ''
|
||||
let penaltyText = ''
|
||||
let adjustmentText = ''
|
||||
const verifiedName = await validateMinterName(minterName)
|
||||
let levelText = '</h3>'
|
||||
|
||||
if (verifiedName) {
|
||||
const accountInfo = await getNameInfo(verifiedName)
|
||||
const accountAddress = accountInfo.owner
|
||||
const addressInfo = await getAddressInfo(accountAddress)
|
||||
levelText = ` - Level ${addressInfo.level}</h3>`
|
||||
console.log(`name is validated, utilizing for removal features...${verifiedName}`)
|
||||
penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<p>'
|
||||
adjustmentText = addressInfo.blocksMintedAdjustment == 0 ? '' : '<p>(has Blocks Adjustment)<p>'
|
||||
const removeActionsHtml = await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
|
||||
showRemoveHtml = removeActionsHtml
|
||||
if (userVote === 0) {
|
||||
cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want
|
||||
} else if (userVote === 1) {
|
||||
cardColorCode = "rgba(55, 12, 12, 0.61)"; // or any red you want
|
||||
}
|
||||
|
||||
if (banTransactions.some((banTx) => banTx.groupId === 694 && banTx.offender === accountAddress)){
|
||||
console.warn(`account was already banned, displaying as such...`)
|
||||
@ -1176,9 +1330,9 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
<h2 class="support-header"> Created By: </h2>
|
||||
${creatorAvatar}
|
||||
<h2>${creator}</h2>
|
||||
${minterOrTopicHtml}
|
||||
${minterOrTopicHtml}${levelText}
|
||||
<p>${header}</p>
|
||||
${altText}
|
||||
${penaltyText}${adjustmentText}${altText}
|
||||
</div>
|
||||
<div class="info">
|
||||
${content}
|
||||
|
@ -30,6 +30,13 @@ 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>
|
||||
<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>
|
||||
<select id="sort-select" style="margin-left: 10px; padding: 5px;">
|
||||
<option value="newest" selected>Sort by Date</option>
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="recent-comments">Newest Comments</option>
|
||||
<option value="least-votes">Least Votes</option>
|
||||
<option value="most-votes">Most Votes</option>
|
||||
</select>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
||||
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||
<form id="publish-card-form">
|
||||
@ -119,6 +126,12 @@ const loadMinterBoardPage = async () => {
|
||||
event.preventDefault()
|
||||
await publishCard(minterCardIdentifierPrefix)
|
||||
})
|
||||
|
||||
document.getElementById("sort-select").addEventListener("change", async () => {
|
||||
// Re-load the cards whenever user chooses a new sort option.
|
||||
await loadCards(minterCardIdentifierPrefix)
|
||||
})
|
||||
|
||||
await featureTriggerCheck()
|
||||
await loadCards(minterCardIdentifierPrefix)
|
||||
}
|
||||
@ -280,6 +293,159 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
}
|
||||
const finalCards = await processMinterCards(validCards)
|
||||
|
||||
// finalCards is already sorted by newest (timestamp) by processMinterCards.
|
||||
// We'll re-sort if "Name" or "Recent Comments" is selected.
|
||||
|
||||
// Grab the selected sort from the dropdown (if it exists).
|
||||
let selectedSort = 'newest'
|
||||
const sortSelect = document.getElementById('sort-select')
|
||||
if (sortSelect) {
|
||||
selectedSort = sortSelect.value
|
||||
}
|
||||
|
||||
if (selectedSort === 'name') {
|
||||
// Sort alphabetically by the creator's name
|
||||
finalCards.sort((a, b) => {
|
||||
const nameA = a.name?.toLowerCase() || ''
|
||||
const nameB = b.name?.toLowerCase() || ''
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
} else if (selectedSort === 'recent-comments') {
|
||||
// We need each card's newest comment timestamp for sorting
|
||||
for (let card of finalCards) {
|
||||
card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier)
|
||||
}
|
||||
// Then sort descending by newest comment
|
||||
finalCards.sort((a, b) =>
|
||||
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
||||
)
|
||||
} else if (selectedSort === 'least-votes') {
|
||||
// For each card, fetch its poll data so we know how many admin + minter votes it has.
|
||||
// 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') {
|
||||
// Fetch poll data & store admin + minter votes as before.
|
||||
// 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
|
||||
|
||||
// Display skeleton cards immediately
|
||||
cardsContainer.innerHTML = ""
|
||||
finalCards.forEach(card => {
|
||||
@ -308,10 +474,13 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
removeSkeleton(card.identifier)
|
||||
return
|
||||
}
|
||||
const pollPublisherPublicKey = await getPollPublisherPublicKey(cardDataResponse.poll)
|
||||
const cardPublisherPublicKey = await getPublicKeyByName(card.name)
|
||||
|
||||
if (pollPublisherPublicKey != cardPublisherPublicKey) {
|
||||
// Getting the poll owner address uses the same API call as public key, so takes the same time, but address will be needed later.
|
||||
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)
|
||||
|
||||
if (pollPublisherAddress != cardPublisherAddress) {
|
||||
console.warn(`not displaying card, QuickMythril pollHijack attack found! Discarding card with identifier: ${card.identifier}`)
|
||||
removeSkeleton(card.identifier)
|
||||
return
|
||||
@ -346,7 +515,7 @@ const loadCards = async (cardIdentifierPrefix) => {
|
||||
const finalCardHTML = isARBoard ? // If we're calling from the ARBoard, we will create HTML with a different function.
|
||||
await createARCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor)
|
||||
:
|
||||
await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor)
|
||||
await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor, cardPublisherAddress)
|
||||
replaceSkeleton(card.identifier, finalCardHTML)
|
||||
|
||||
} catch (error) {
|
||||
@ -489,8 +658,8 @@ const publishCard = async (cardIdentifierPrefix) => {
|
||||
|
||||
const minterGroupData = await fetchMinterGroupMembers()
|
||||
const minterGroupAddresses = minterGroupData.map(m => m.member)
|
||||
const userAddress = userState.accountAddress
|
||||
|
||||
const userAddress = userState.accountAddress;
|
||||
if (minterGroupAddresses.includes(userAddress)) {
|
||||
alert("You are already a Minter and cannot publish a new card!")
|
||||
return
|
||||
@ -576,7 +745,8 @@ const processPollData= async (pollData, minterGroupMembers, minterAdmins, creato
|
||||
totalNo: 0,
|
||||
totalYesWeight: 0,
|
||||
totalNoWeight: 0,
|
||||
detailsHtml: `<p>Poll data is invalid or missing.</p>`
|
||||
detailsHtml: `<p>Poll data is invalid or missing.</p>`,
|
||||
userVote: null
|
||||
}
|
||||
}
|
||||
|
||||
@ -595,6 +765,7 @@ const processPollData= async (pollData, minterGroupMembers, minterAdmins, creato
|
||||
let adminYes = 0, adminNo = 0
|
||||
let minterYes = 0, minterNo = 0
|
||||
let yesWeight = 0, noWeight = 0
|
||||
let userVote = null
|
||||
|
||||
for (const w of pollData.voteWeights) {
|
||||
if (w.optionName.toLowerCase() === 'yes') {
|
||||
@ -609,6 +780,10 @@ const processPollData= async (pollData, minterGroupMembers, minterAdmins, creato
|
||||
const voterPublicKey = vote.voterPublicKey
|
||||
const voterAddress = await getAddressFromPublicKey(voterPublicKey)
|
||||
|
||||
if (voterAddress === userState.accountAddress) {
|
||||
userVote = optionIndex
|
||||
}
|
||||
|
||||
if (optionIndex === 0) {
|
||||
if (adminAddresses.includes(voterAddress)) {
|
||||
adminYes++
|
||||
@ -703,7 +878,8 @@ const processPollData= async (pollData, minterGroupMembers, minterAdmins, creato
|
||||
totalNo,
|
||||
totalYesWeight: totalMinterAndAdminYesWeight,
|
||||
totalNoWeight: totalMinterAndAdminNoWeight,
|
||||
detailsHtml
|
||||
detailsHtml,
|
||||
userVote
|
||||
}
|
||||
}
|
||||
|
||||
@ -815,7 +991,6 @@ const postComment = async (cardIdentifier) => {
|
||||
data64: base64CommentData,
|
||||
})
|
||||
|
||||
alert('Comment posted successfully!')
|
||||
commentInput.value = ''
|
||||
} catch (error) {
|
||||
console.error('Error posting comment:', error)
|
||||
@ -1593,9 +1768,28 @@ const getMinterAvatar = async (minterName) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getNewestCommentTimestamp = async (cardIdentifier) => {
|
||||
try {
|
||||
// fetchCommentsForCard returns resources each with at least 'created' or 'updated'
|
||||
const comments = await fetchCommentsForCard(cardIdentifier)
|
||||
if (!comments || comments.length === 0) {
|
||||
// No comments => fallback to 0 (or card's own date, if you like)
|
||||
return 0
|
||||
}
|
||||
// The newest can be determined by comparing 'updated' or 'created'
|
||||
const newestTimestamp = comments.reduce((acc, c) => {
|
||||
const cTime = c.updated || c.created || 0
|
||||
return (cTime > acc) ? cTime : acc
|
||||
}, 0)
|
||||
return newestTimestamp
|
||||
} catch (err) {
|
||||
console.error('Failed to get newest comment timestamp:', err)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Create the overall Minter Card HTML -----------------------------------------------
|
||||
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor) => {
|
||||
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor, address) => {
|
||||
const { header, content, links, creator, timestamp, poll } = cardData
|
||||
const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
|
||||
const avatarHtml = await getMinterAvatar(creator)
|
||||
@ -1607,7 +1801,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
|
||||
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier)
|
||||
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml, userVote } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier)
|
||||
createModal('links')
|
||||
createModal('poll-details')
|
||||
|
||||
@ -1616,12 +1810,18 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
|
||||
|
||||
let finalBgColor = bgColor
|
||||
let invitedText = "" // for "INVITED" label if found
|
||||
const addressInfo = await getAddressInfo(address)
|
||||
const penaltyText = addressInfo.blocksMintedPenalty == 0 ? '' : '<p>(has Blocks Penalty)<p>'
|
||||
const adjustmentText = addressInfo.blocksMintedAdjustment == 0 ? '' : '<p>(has Blocks Adjustment)<p>'
|
||||
|
||||
try {
|
||||
const minterAddress = await fetchOwnerAddressFromName(creator)
|
||||
const invites = await fetchGroupInvitesByAddress(minterAddress)
|
||||
const invites = await fetchGroupInvitesByAddress(address)
|
||||
const hasMinterInvite = invites.some((invite) => invite.groupId === 694)
|
||||
if (hasMinterInvite) {
|
||||
if (userVote === 0) {
|
||||
finalBgColor = "rgba(0, 192, 0, 0.3)"; // or any green you want
|
||||
} else if (userVote === 1) {
|
||||
finalBgColor = "rgba(192, 0, 0, 0.3)"; // or any red you want
|
||||
} else if (hasMinterInvite) {
|
||||
// If so, override background color & add an "INVITED" label
|
||||
finalBgColor = "black";
|
||||
invitedText = `<h4 style="color: gold; margin-bottom: 0.5em;">INVITED</h4>`
|
||||
@ -1651,9 +1851,9 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
|
||||
<div class="minter-card" style="background-color: ${finalBgColor}">
|
||||
<div class="minter-card-header">
|
||||
${avatarHtml}
|
||||
<h3>${creator}</h3>
|
||||
<h3>${creator} - Level ${addressInfo.level}</h3>
|
||||
<p>${header}</p>
|
||||
${invitedText}
|
||||
${penaltyText}${adjustmentText}${invitedText}
|
||||
</div>
|
||||
<div class="support-header"><h5>USER'S POST</h5></div>
|
||||
<div class="info">
|
||||
|
@ -48,12 +48,12 @@ const timestampToHumanReadableDate = async(timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
const day = date.getDate()
|
||||
const month = date.getMonth() + 1
|
||||
const year = date.getFullYear() - 2000
|
||||
const year = date.getFullYear()
|
||||
const hours = date.getHours()
|
||||
const minutes = date.getMinutes()
|
||||
const seconds = date.getSeconds()
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
const formattedDate = `${month}.${day}.${year}@${hours}:${minutes}:${seconds}`
|
||||
const formattedDate = `${year}.${month}.${day}@${hours}:${minutes}:${seconds}`
|
||||
console.log('Formatted date:', formattedDate)
|
||||
return formattedDate
|
||||
}
|
||||
@ -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.
|
||||
const searchSimple = async (service, identifier, name, limit = 1500, offset = 0, room='', reverse='true') => {
|
||||
const searchSimple = async (service, identifier, name, limit=1500, offset=0, room='', reverse=true, prefixOnly=true) => {
|
||||
try {
|
||||
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}`
|
||||
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&prefix=true&limit=${limit}&offset=${offset}&reverse=${reverse}&prefix=${prefixOnly}`
|
||||
|
||||
if (name && !identifier && !room) {
|
||||
console.log('name only searchSimple', name)
|
||||
@ -1226,7 +1226,7 @@ const renderData = async (service, name, identifier) => {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error rendering data:', error)
|
||||
// Return the custom message when there’s an error or invalid data
|
||||
// Return the custom message when there's an error or invalid data
|
||||
return 'Requested data is either missing or still being obtained from QDN... please try again in a short time.'
|
||||
}
|
||||
}
|
||||
@ -1347,7 +1347,7 @@ const processTransaction = async (signedTransaction) => {
|
||||
const rawText = await response.text();
|
||||
console.log("Raw text from server (version 2 means JSON string in text):", rawText)
|
||||
|
||||
// Attempt to parse if it’s indeed JSON
|
||||
// Attempt to parse if it's indeed JSON
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(rawText);
|
||||
|
33
index.html
33
index.html
@ -42,7 +42,7 @@
|
||||
</a>
|
||||
</span>
|
||||
<span class="navbar-caption-wrap">
|
||||
<a class="navbar-caption display-4" href="index.html">Q-Mintership (v1.0b)
|
||||
<a class="navbar-caption display-4" href="index.html">Q-Mintership (v1.02b)
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@ -61,7 +61,7 @@
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
</a>
|
||||
</span>
|
||||
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership v1.0b<br></a></span>
|
||||
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership v1.02b<br></a></span>
|
||||
</div>
|
||||
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
<div class="item-wrapper">
|
||||
<img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
|
||||
<div class="item-content">
|
||||
<h2 class="card-title mbr-fonts-style display-2">New Minters - Start Here</h2>
|
||||
<h2 class="card-title mbr-fonts-style display-2">Minting Rights? - Start Here - MinterBoard</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@ -196,13 +196,32 @@
|
||||
<div class="col-12 col-lg-7 card">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-2">
|
||||
v1.0beta 01-21-2025</h2>
|
||||
v1.02beta 01-22-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.0b information coming soon.</u></b>- <b>All Features related to new featureTriggers is completed and tested.</b>
|
||||
<b><u>v1.02b Fixes</u></b>- <b>There were publish issues on ARA Board</b> - These publish issues have now been resolved. DUPLICATES ARE NOT ALLOWED, and if you did not publish a card for a name, you cannot publish an update. Also removed the 'alert' for comment publish, as it was unnecessary. Also resolved an issue preventing a user from publishing more than a single card (regardless of duplicates).
|
||||
</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.01beta 01-21-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.01b - Improving Major changes in 1.0.</u></b>- <b>Every feature required for the new featureTriggers have been added</b>, and a minor bug on the ARA Board has been resolved. Pull Request from QuickMythril has also been added. Allowing cards to be arranged based on various parameters.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -517,12 +536,12 @@
|
||||
<div class="title-wrapper">
|
||||
<div class="title-wrap">
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership-Alpha</h2>
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership (v1.02b)</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="link-wrap" href="#">
|
||||
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership-Alpha v0.84beta</p>
|
||||
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v1.02beta</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
Loading…
Reference in New Issue
Block a user