Compare commits
23 Commits
main
...
testing-20
Author | SHA1 | Date | |
---|---|---|---|
|
1aa4985375 | ||
|
7cfd0357b5 | ||
|
41e1369d86 | ||
|
9f645f5582 | ||
|
3a083f99f6 | ||
|
1b5e8c38e1 | ||
|
e79e0bf4b1 | ||
|
02868171e3 | ||
|
9971c6d595 | ||
|
0df227d63d | ||
|
057b41af1d | ||
|
eb56e67232 | ||
|
b63b894dcb | ||
|
001f762266 | ||
|
1bfa938caf | ||
|
ef8770c5ca | ||
|
aabb6ab0d4 | ||
|
daf5400aea | ||
|
c5dfd29d94 | ||
|
2a14248e3a | ||
|
dcc9046059 | ||
|
bcab208528 | ||
|
e37246974e |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/.vscode
|
||||
/.sync*
|
BIN
.sync_b48a6eedae0b.db
Normal file
BIN
.sync_b48a6eedae0b.db
Normal file
Binary file not shown.
BIN
.sync_b48a6eedae0b.db-wal
Normal file
BIN
.sync_b48a6eedae0b.db-wal
Normal file
Binary file not shown.
49
README.md
49
README.md
@ -1,49 +0,0 @@
|
||||
### Q-Mintership-Alpha
|
||||
|
||||
Q-Mintership-Alpha is the currently utilized version of the Q-Mintership app published on qortal://APP/Q-Mintership.
|
||||
|
||||
As of Feb 27 2025 Q-Mintership-Alpha is still the published and utilized version of the app.
|
||||
|
||||
#### Q-Mintership's 'MinterBoard'
|
||||
|
||||
The MinterBoard of Q-Mintership, is the primary location for users to publish 'cards' with information about themselves, links to things they have published on QDN, etc... and obtain minting rights from the Minter Admins.
|
||||
|
||||
- Cards are created by any non-minter (either previous minter no longer in the group, or new accounts without minting rights).
|
||||
- Existing community members, existing minters, and Minter Admins, can vote/comment on the cards.
|
||||
- Once a card has obtained the minimum required number of admin votes (40% of the Minter Admin count), the card will then display additional features to those that have the rights to see them. (Minter Admins, and 'Forum Admins', however 'Forum Admins' cannot actually make use of the functionality, they are only able to view it for development purposes.)
|
||||
- The Minter Admins then initiate a PENDING GROUP_INVITE transaction.
|
||||
- The Minter Admins are then able to issue GROUP_APPROVAL transactions to approve the invite.
|
||||
- Once the required number of GROUP_APPROVAL transactions have been created, the GROUP_INVITE is no longer pending, and is active.
|
||||
- The would-be minter that published a card, can then see a new 'JOIN_GROUP' button on their card upon returning to the MinterBoard.
|
||||
- The user will then JOIN_GROUP to the MINTER group, ID 694. Thus allowing the ability to mint.
|
||||
- Upon joining the MINTER group, the user will then have the ability to create a MINTING KEY (the same way as it was created prior to the Mintership concept, however now it no longer requires users to be level 1, only requirement now is to be part of the MINTER group)
|
||||
- User then assigns their key to their node, and starts minting.
|
||||
|
||||
#### Q-Mintership 'AdminBoard'
|
||||
|
||||
- The AdminBoard is a separate board, encrypted to admins only, meant to be utilized for private decision-making between the admins.
|
||||
- The AdminBoard was also adapted to allow REMOVAL of MINTER group members, via GROUP_APPROVAL from the Minter Admins.
|
||||
- The REMOVAL functionality, at the moment, is private. Meaning only the admins that have access to the AdminBoard, can see the data. This will be changed in the future, and a new location where the data will be able to be seen publicly, will be created.
|
||||
|
||||
|
||||
#### Q-Mintership Forum
|
||||
|
||||
- The Forum portion of Q-Mintership is a public (and private) forum, allowing communications to take place in the fashion of long-term forum messages, replies, etc.
|
||||
- Publishing of images with previews, and various 'attachments' with data is also possible on the forum.
|
||||
- The forum has two public rooms by default, and one private room. General and Minter rooms are public, and Admin room is private.
|
||||
- The forum will be getting extensive updates in the future, and the Minter room will be made a publicly VIEWABLE room, but only able to be published to by MINTERS.
|
||||
|
||||
|
||||
#### Q-Mintership MAM Board
|
||||
|
||||
- The MAM Board (or ARBoard in the code) is built to allow the adding and removal of Minter Admins from the MINTER group. Proposals for additions or removals of certain accounts from and to the Minter takes place here.
|
||||
- This board also displays a list of the current Minter Admins, and has the ability to propose a removal of that user with a propose removal button.
|
||||
|
||||
|
||||
#### Additional
|
||||
|
||||
Many additional features and functions are planned for Q-Mintership, and increased performance and more will be added as time goes on.
|
||||
|
||||
Longer-term the plan is to re-write the app into React+TypeScript, which will make it MUCH faster and able to accommodate much more, with a component-based development style similar to that of the other React-based applications on Qortal (Q-Tube, Q-Blog, Q-Mail, etc.)
|
||||
|
||||
A fully featured data viewer and explorer function will be built into Q-Mintership in the future, along with a comprehensive notification system, and more.
|
@ -163,6 +163,36 @@
|
||||
background-color: #19403d;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1vh;
|
||||
background-color: #891616;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 1vh;
|
||||
padding: 0.3vh 0.6vh;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
background-color: #3d1919;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1vh;
|
||||
background-color: #897016;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 1vh;
|
||||
padding: 0.3vh 0.6vh;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-button:hover {
|
||||
background-color: #402d09;
|
||||
}
|
||||
|
||||
/* forum-styles.css additions */
|
||||
|
||||
.message-input-section {
|
||||
@ -584,7 +614,7 @@ body {
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin: 20px auto; /* center horizontally */
|
||||
/* max-width: 600px; */
|
||||
max-width: 600px; /* limit width */
|
||||
color: #ddd; /* text color */
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
@ -596,7 +626,7 @@ body {
|
||||
background-color:#000000;
|
||||
width: 90%;
|
||||
font-size: 1.8rem;
|
||||
color: #fff3f3;
|
||||
color: #4d0000;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
/* you could style the list items or bullet if you like */
|
||||
@ -616,17 +646,7 @@ body {
|
||||
background-color: #14161a;
|
||||
border: 1px solid #8caeb0;
|
||||
border-radius: 4px;
|
||||
color: #f19c9c;
|
||||
}
|
||||
|
||||
.invite-form input.invite-input {
|
||||
padding: 1rem;
|
||||
font-size: 2rem;
|
||||
line-height: 2;
|
||||
background-color: #14161a;
|
||||
border: 1px solid #8caeb0;
|
||||
border-radius: 4px;
|
||||
color: #dddddd;
|
||||
color: #5c0101;
|
||||
}
|
||||
|
||||
.publish-card-button {
|
||||
@ -717,42 +737,30 @@ body {
|
||||
background-color: #281e1e;
|
||||
}
|
||||
|
||||
.approve-invite-list-button {
|
||||
background-color: rgba(32, 88, 34, 0.554);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 1vw;
|
||||
padding: 1vh,2vh;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.publish-card-view {
|
||||
width: 90%;
|
||||
padding: 2vh;
|
||||
}
|
||||
|
||||
.publish-card-button {
|
||||
font-size: 1.8vh;
|
||||
padding: 1.5vh;
|
||||
}
|
||||
|
||||
.publish-card-form button {
|
||||
font-size: 1.8vh;
|
||||
padding: 1.2vh;
|
||||
}
|
||||
}
|
||||
|
||||
.approve-invite-list-button:hover {
|
||||
background-color: rgba(34, 186, 47, 0.84); /* a darker variant */
|
||||
}
|
||||
|
||||
.invite-approvals strong {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.invite-item {
|
||||
margin-bottom: 0.5em;
|
||||
background-color: rgba(31, 31, 31, 0.595);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
color: #ccc;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Top row: use flex for horizontal arrangement */
|
||||
.invite-top-row {
|
||||
display: flex;
|
||||
background-color:#173c52ae;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.refresh-cards-button {
|
||||
border-color: white;
|
||||
border-radius: 1.5vh;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@ -776,14 +784,8 @@ body {
|
||||
.refresh-cards-button {
|
||||
border-color: white;
|
||||
border-radius: 1.5vh;
|
||||
background-color: rgba(0, 0, 0, 0.089);
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.refresh-cards-button:hover {
|
||||
background-color: rgba(35, 129, 136, 0.137);
|
||||
color: rgba(90, 201, 221, 0.793);
|
||||
}
|
||||
/* Two cards per row on medium screens */
|
||||
|
||||
|
@ -59,14 +59,19 @@ const loadAddRemoveAdminPage = async () => {
|
||||
<div id="existing-proposals-section" class="proposals-section" style="margin-top: 3em; display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||
<h3 style="color: #ddd;">Existing Promotion/Demotion Proposals</h3>
|
||||
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Proposal Cards</button>
|
||||
<select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:white; background-color: black;">
|
||||
<option value="newest" selected>Sort by Date</option>
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="recent-comments">Newest Comments</option>
|
||||
<option value="least-votes">Least Votes</option>
|
||||
<option value="most-votes">Most Votes</option>
|
||||
</select>
|
||||
<select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;">
|
||||
<option value="0">All Creation Dates</option>
|
||||
<option value="1">Last 1 Day</option>
|
||||
<option value="7">Last 7 Days</option>
|
||||
<option value="30">...Within 30 Days</option>
|
||||
<option value="45" selected>Published Within Last 45 Days</option>
|
||||
<option value="60">...Within 60 Days</option>
|
||||
<option value="90">...Within 90 Days</option>
|
||||
<option value="0">Show All</option>
|
||||
<option value="1">Last 1 day</option>
|
||||
<option value="7">Last 7 days</option>
|
||||
<option value="30" selected>Last 30 days</option>
|
||||
<option value="90">Last 90 days</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 1rem"">
|
||||
@ -97,7 +102,6 @@ const loadAddRemoveAdminPage = async () => {
|
||||
document.getElementById("refresh-cards-button").addEventListener("click", async () => {
|
||||
const cardsContainer = document.getElementById("cards-container")
|
||||
cardsContainer.innerHTML = "<p>Refreshing cards...</p>"
|
||||
await initializeCachedGroups()
|
||||
await loadCards(addRemoveIdentifierPrefix)
|
||||
})
|
||||
|
||||
@ -120,20 +124,18 @@ const loadAddRemoveAdminPage = async () => {
|
||||
linksContainer.appendChild(newLinkInput)
|
||||
})
|
||||
|
||||
const timeRangeSelectCheckbox = document.getElementById('time-range-select')
|
||||
if (timeRangeSelectCheckbox) {
|
||||
timeRangeSelectCheckbox.addEventListener('change', async (event) => {
|
||||
await loadCards(addRemoveIdentifierPrefix)
|
||||
})
|
||||
}
|
||||
|
||||
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault()
|
||||
await publishARCard(addRemoveIdentifierPrefix)
|
||||
})
|
||||
|
||||
document.getElementById("sort-select").addEventListener("change", async () => {
|
||||
// Only re-load the cards whenever user presses the refresh button.
|
||||
})
|
||||
|
||||
await featureTriggerCheck()
|
||||
await loadCards(addRemoveIdentifierPrefix)
|
||||
await displayExistingMinterAdmins()
|
||||
// Only load the cards whenever user presses the refresh button.
|
||||
await fetchAllARTxData()
|
||||
}
|
||||
|
||||
@ -143,6 +145,19 @@ const toggleProposeButton = () => {
|
||||
proposeButton.style.display === 'flex' ? 'none' : 'flex'
|
||||
}
|
||||
|
||||
const toggleAdminTable = () => {
|
||||
const tableContainer = document.getElementById("adminTableContainer")
|
||||
const toggleBtn = document.getElementById("toggleAdminTableButton")
|
||||
|
||||
if (tableContainer.style.display === "none") {
|
||||
tableContainer.style.display = "block"
|
||||
toggleBtn.textContent = "Hide Minter Admins"
|
||||
} else {
|
||||
tableContainer.style.display = "none"
|
||||
toggleBtn.textContent = "Show Minter Admins"
|
||||
}
|
||||
}
|
||||
|
||||
const fetchAllARTxData = async () => {
|
||||
const addAdmTx = "ADD_GROUP_ADMIN"
|
||||
const remAdmTx = "REMOVE_GROUP_ADMIN"
|
||||
@ -169,63 +184,51 @@ const fetchAllARTxData = async () => {
|
||||
txGroupId: 694,
|
||||
})
|
||||
|
||||
const { finalAddTxs, pendingAddTxs, expiredAddTxs } = partitionAddTransactions(allAddTxs)
|
||||
const { finalRemTxs, pendingRemTxs, expiredRemTxs } = partitionRemoveTransactions(allRemTxs)
|
||||
const { finalAddTxs, pendingAddTxs } = partitionAddTransactions(allAddTxs)
|
||||
const { finalRemTxs, pendingRemTxs } = partitionRemoveTransactions(allRemTxs)
|
||||
|
||||
// We are going to keep all transactions in order to filter more accurately for display purposes.
|
||||
console.log('Final addAdminTxs:', finalAddTxs)
|
||||
console.log('Pending addAdminTxs:', pendingAddTxs)
|
||||
console.log('expired addAdminTxs', expiredAddTxs)
|
||||
console.log('Final remAdminTxs:', finalRemTxs)
|
||||
console.log('Pending remAdminTxs:', pendingRemTxs)
|
||||
console.log('expired remAdminTxs', expiredRemTxs)
|
||||
console.log('Final addAdminTxs:', finalAddTxs);
|
||||
console.log('Pending addAdminTxs:', pendingAddTxs);
|
||||
console.log('Final remAdminTxs:', finalRemTxs);
|
||||
console.log('Pending remAdminTxs:', pendingRemTxs);
|
||||
|
||||
return {
|
||||
finalAddTxs,
|
||||
pendingAddTxs,
|
||||
expiredAddTxs,
|
||||
finalRemTxs,
|
||||
pendingRemTxs,
|
||||
expiredRemTxs
|
||||
}
|
||||
}
|
||||
|
||||
const partitionAddTransactions = (rawTransactions) => {
|
||||
const finalAddTxs = []
|
||||
const pendingAddTxs = []
|
||||
const expiredAddTxs = []
|
||||
|
||||
for (const tx of rawTransactions) {
|
||||
if (tx.approvalStatus === 'PENDING') {
|
||||
pendingAddTxs.push(tx)
|
||||
const finalAddTxs = []
|
||||
const pendingAddTxs = []
|
||||
|
||||
for (const tx of rawTransactions) {
|
||||
if (tx.approvalStatus === 'PENDING') {
|
||||
pendingAddTxs.push(tx)
|
||||
} else {
|
||||
finalAddTxs.push(tx)
|
||||
}
|
||||
}
|
||||
else if (tx.approvalStatus === 'EXPIRED'){
|
||||
expiredAddTxs.push(tx)
|
||||
} else {
|
||||
finalAddTxs.push(tx)
|
||||
}
|
||||
}
|
||||
|
||||
return { finalAddTxs, pendingAddTxs, expiredAddTxs };
|
||||
|
||||
return { finalAddTxs, pendingAddTxs };
|
||||
}
|
||||
|
||||
const partitionRemoveTransactions = (rawTransactions) => {
|
||||
const finalRemTxs = []
|
||||
const pendingRemTxs = []
|
||||
const expiredRemTxs = []
|
||||
const finalRemTxs = []
|
||||
const pendingRemTxs = []
|
||||
|
||||
for (const tx of rawTransactions) {
|
||||
if (tx.approvalStatus === 'PENDING') {
|
||||
for (const tx of rawTransactions) {
|
||||
if (tx.approvalStatus === 'PENDING') {
|
||||
pendingRemTxs.push(tx)
|
||||
}
|
||||
else if (tx.approvalStatus === 'EXPIRED'){
|
||||
expiredRemTxs.push(tx)
|
||||
} else {
|
||||
} else {
|
||||
finalRemTxs.push(tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { finalRemTxs, pendingRemTxs, expiredRemTxs }
|
||||
return { finalRemTxs, pendingRemTxs }
|
||||
}
|
||||
|
||||
|
||||
@ -237,7 +240,29 @@ const displayExistingMinterAdmins = async () => {
|
||||
try {
|
||||
// 1) Fetch addresses
|
||||
const admins = await fetchMinterGroupAdmins()
|
||||
// Get names for each admin first
|
||||
for (const admin of admins) {
|
||||
try {
|
||||
admin.name = await getNameFromAddress(admin.member)
|
||||
} catch (err) {
|
||||
console.warn(`Error fetching name for ${admin.member}:`, err)
|
||||
admin.name = null
|
||||
}
|
||||
}
|
||||
// Sort admin list by name with NULL ACCOUNT at the top
|
||||
admins.sort((a, b) => {
|
||||
if (a.member === nullAddress && b.member !== nullAddress) {
|
||||
return -1;
|
||||
}
|
||||
if (b.member === nullAddress && a.member !== nullAddress) {
|
||||
return 1;
|
||||
}
|
||||
return (a.name || '').localeCompare(b.name || '');
|
||||
})
|
||||
minterAdminAddresses = admins.map(m => m.member)
|
||||
// Compute total admin count and signatures needed (40%, rounded up)
|
||||
const totalAdmins = admins.length;
|
||||
const signaturesNeeded = Math.ceil(totalAdmins * 0.40);
|
||||
let rowsHtml = "";
|
||||
for (const adminAddr of admins) {
|
||||
if (adminAddr.member === nullAddress) {
|
||||
@ -257,16 +282,8 @@ const displayExistingMinterAdmins = async () => {
|
||||
</tr>
|
||||
`
|
||||
continue
|
||||
}
|
||||
// Attempt to get name
|
||||
let adminName
|
||||
try {
|
||||
adminName = await getNameFromAddress(adminAddr.member)
|
||||
} catch (err) {
|
||||
console.warn(`Error fetching name for ${adminAddr.member}:`, err)
|
||||
adminName = null
|
||||
}
|
||||
const displayName = adminName && adminName !== adminAddr.member ? adminName : "(No Name)"
|
||||
const displayName = adminAddr.name && adminAddr.name !== adminAddr.member ? adminAddr.name : "(No Name)"
|
||||
rowsHtml += `
|
||||
<tr>
|
||||
<td style="border: 1px solid rgb(150, 199, 224); font-size: 1.5rem; padding: 4px; color:rgb(70, 156, 196)">${displayName}</td>
|
||||
@ -274,7 +291,7 @@ const displayExistingMinterAdmins = async () => {
|
||||
<td style="border: 1px solid rgb(231, 112, 112); padding: 4px;">
|
||||
<button
|
||||
style="padding: 5px; background: red; color: white; border-radius: 3px; cursor: pointer;"
|
||||
onclick="handleProposeDemotionWrapper('${adminName}', '${adminAddr.member}')"
|
||||
onclick="handleProposeDemotionWrapper('${adminAddr.name}', '${adminAddr.member}')"
|
||||
>
|
||||
Propose Demotion
|
||||
</button>
|
||||
@ -284,6 +301,22 @@ const displayExistingMinterAdmins = async () => {
|
||||
}
|
||||
// 3) Build the table
|
||||
const tableHtml = `
|
||||
<div style="text-align: center; margin-bottom: 1em;">
|
||||
<button
|
||||
id="toggleAdminTableButton"
|
||||
onclick="toggleAdminTable()"
|
||||
style="
|
||||
padding: 10px;
|
||||
background: #444;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
Show Minter Admins
|
||||
</button>
|
||||
</div>
|
||||
<div id="adminTableContainer" style="display: none;">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background:rgb(21, 36, 18); color:rgb(183, 208, 173); font-size: 1.5rem;">
|
||||
@ -296,8 +329,13 @@ const displayExistingMinterAdmins = async () => {
|
||||
${rowsHtml}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
adminListContainer.innerHTML = `
|
||||
<h3 style="color:rgb(212, 212, 212);">Existing Minter Admins: ${totalAdmins}</h3>
|
||||
<h4 style="color:rgb(212, 212, 212);">Signatures for Group Approval (40%): ${signaturesNeeded}</h4>
|
||||
${tableHtml}
|
||||
`
|
||||
adminListContainer.innerHTML = tableHtml
|
||||
} catch (err) {
|
||||
console.error("Error fetching minter admins:", err)
|
||||
adminListContainer.innerHTML =
|
||||
@ -736,6 +774,36 @@ const handleRemoveMinterGroupAdmin = async (name, address) => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteARCard = async (cardIdentifier) => {
|
||||
try {
|
||||
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
|
||||
if (!confirmed) return
|
||||
const blankData = {
|
||||
header: "",
|
||||
content: "",
|
||||
links: [],
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: ""
|
||||
}
|
||||
let base64Data = await objectToBase64(blankData)
|
||||
if (!base64Data) {
|
||||
base64Data = btoa(JSON.stringify(blankData))
|
||||
}
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64Data,
|
||||
})
|
||||
alert("Your card has been effectively deleted.")
|
||||
} catch (error) {
|
||||
console.error("Error deleting AR card:", error)
|
||||
alert("Failed to delete the card. Check console for details.")
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins) => {
|
||||
// Ensure we have addresses
|
||||
if (!minterGroupMembers) {
|
||||
@ -842,9 +910,9 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
|
||||
const actionsHtmlCheck = await checkAndDisplayActions(adminYes, verifiedName, cardIdentifier)
|
||||
actionsHtml = actionsHtmlCheck
|
||||
|
||||
const { finalAddTxs, pendingAddTxs, expiredAddTxs, finalRemTxs, pendingRemTxs, expiredRemTxs } = await fetchAllARTxData()
|
||||
const { finalAddTxs, pendingAddTxs, finalRemTxs, pendingRemTxs } = await fetchAllARTxData()
|
||||
|
||||
const userConfirmedAdd = finalAddTxs.some(
|
||||
const confirmedAdd = finalAddTxs.some(
|
||||
(tx) => tx.groupId === 694 && tx.member === accountAddress
|
||||
)
|
||||
const userPendingAdd = pendingAddTxs.some(
|
||||
@ -856,88 +924,31 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
|
||||
const userPendingRemove = pendingRemTxs.some(
|
||||
(tx) => tx.groupId === 694 && tx.admin === accountAddress
|
||||
)
|
||||
const userExpiredAdd = expiredAddTxs.some(
|
||||
(tx) => tx.groupId === 694 && tx.member === accountAddress
|
||||
)
|
||||
const userExpiredRem = expiredRemTxs.some(
|
||||
(tx) => tx.groupId === 694 && tx.admin === accountAddress
|
||||
)
|
||||
|
||||
const noExpired = (!userExpiredAdd && !userExpiredRem)
|
||||
|
||||
// If user is definitely admin (finalAdd) and not pending removal
|
||||
if (userConfirmedAdd && !userPendingRemove && !userPendingAdd && noExpired && existingAdmin && promotionCard) {
|
||||
if (confirmedAdd && !userPendingRemove && existingAdmin) {
|
||||
console.warn(`account was already admin, final. no add/remove pending.`);
|
||||
cardColorCode = 'rgb(3, 11, 24)'
|
||||
altText = `<h4 style="color:rgb(89, 191, 204); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`;
|
||||
altText = `<h4 style="color:rgb(2, 94, 106); margin-bottom: 0.5em;">PROMOTED to ADMIN</h4>`;
|
||||
actionsHtml = ''
|
||||
}
|
||||
|
||||
if (userConfirmedAdd && !userPendingRemove && userExpiredRem && existingAdmin && promotionCard) {
|
||||
console.warn(`Account has previously had a removal attempt expire`);
|
||||
cardColorCode = 'rgb(33, 40, 11)'
|
||||
altText = `<h4 style="color:rgb(136, 114, 146); margin-bottom: 0.5em;">PROMOTED, (+Previous Failed Demotion).</h4>`;
|
||||
actionsHtml = ''
|
||||
}
|
||||
|
||||
if (userConfirmedAdd && !userPendingRemove && userExpiredAdd && existingAdmin && promotionCard) {
|
||||
console.warn(`Account has previously had a removal attempt expire`);
|
||||
cardColorCode = 'rgb(14, 3, 24)'
|
||||
altText = `<h4 style="color:rgb(114, 117, 146); margin-bottom: 0.5em;">PROMOTED, (+Previous Failed Promotion).</h4>`;
|
||||
actionsHtml = ''
|
||||
}
|
||||
|
||||
if (userConfirmedAdd && userPendingRemove && existingAdmin && noExpired && !promotionCard) {
|
||||
if (confirmedAdd && userPendingRemove && existingAdmin) {
|
||||
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
|
||||
altText = `<h4 style="color:rgb(85, 34, 34); margin-bottom: 0.5em;">Pending REMOVAL in progress...</h4>`;
|
||||
}
|
||||
|
||||
if (userConfirmedAdd && userPendingRemove && existingAdmin && userExpiredAdd && !promotionCard) {
|
||||
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
|
||||
altText = `<h4 style="color:rgb(85, 74, 34); margin-bottom: 0.5em;">Pending REMOVAL in progress... (+Previous Failed Promotion)</h4>`;
|
||||
}
|
||||
|
||||
if (userConfirmedAdd && userPendingRemove && existingAdmin && userExpiredRem && !promotionCard) {
|
||||
console.warn(`user is a previously approved an admin, but now has pending removals. Keeping html`)
|
||||
altText = `<h4 style="color:rgb(198, 26, 13); margin-bottom: 0.5em;">Pending REMOVAL in progress... (+Previous Failed Demotion)</h4>`;
|
||||
}
|
||||
|
||||
// If user has a final "remove" and no pending additions or removals and no expired transactions
|
||||
if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && noExpired && !promotionCard) {
|
||||
console.warn(`account was demoted, final. no add pending, existingMinter, no expired add/remove.`);
|
||||
// If user has a final "remove" and no pending additions or removals
|
||||
if (confirmedRemove && !userPendingAdd && existingMinter) {
|
||||
console.warn(`account was demoted, final. no add pending, existingMinter.`);
|
||||
cardColorCode = 'rgb(29, 4, 6)'
|
||||
altText = `<h4 style="color:rgb(73, 24, 24); margin-bottom: 0.5em;">DEMOTED from ADMIN</h4>`
|
||||
actionsHtml = ''
|
||||
}
|
||||
|
||||
if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && userExpiredRem && !promotionCard) {
|
||||
console.warn(`account was demoted, final. no add pending, existingMinter, no expired add/remove.`);
|
||||
cardColorCode = 'rgb(29, 4, 6)'
|
||||
altText = `<h4 style="color:rgb(170, 32, 48); margin-bottom: 0.5em;">DEMOTED (+Previous Failed Demotion)</h4>`
|
||||
actionsHtml = ''
|
||||
}
|
||||
|
||||
if (confirmedRemove && !userPendingAdd && existingMinter && !existingAdmin && userExpiredAdd && !promotionCard) {
|
||||
console.warn(`account was demoted, final. no add pending, existingMinter, no expired add/remove.`);
|
||||
cardColorCode = 'rgb(29, 4, 6)'
|
||||
altText = `<h4 style="color:rgb(119, 170, 32); margin-bottom: 0.5em;">DEMOTED (+Previous Failed Promotion)</h4>`
|
||||
actionsHtml = ''
|
||||
}
|
||||
|
||||
// If user has both final remove and pending add, do something else
|
||||
if (confirmedRemove && userPendingAdd && existingMinter && noExpired && promotionCard) {
|
||||
if (confirmedRemove && userPendingAdd && existingMinter) {
|
||||
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
|
||||
altText = `<h4 style="color:rgb(73, 68, 24); margin-bottom: 0.5em;">Previously DEMOTED from ADMIN, attempted re-add in progress...</h4>`
|
||||
}
|
||||
|
||||
if (confirmedRemove && userPendingAdd && existingMinter && userExpiredAdd && promotionCard) {
|
||||
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
|
||||
altText = `<h4 style="color:rgb(73, 68, 24); margin-bottom: 0.5em;">Previously DEMOTED from ADMIN, attempted re-add in progress...(+Previous Failed Promotion)</h4>`
|
||||
}
|
||||
|
||||
if (confirmedRemove && userPendingAdd && existingMinter && userExpiredRem && promotionCard) {
|
||||
console.warn(`account was previously demoted, but also a pending re-add, allowing actions to show...`)
|
||||
altText = `<h4 style="color:rgb(73, 68, 24); margin-bottom: 0.5em;">Previously DEMOTED from ADMIN, attempted re-add in progress...(+Previous Failed Demotion)</h4>`
|
||||
// Possibly show "DEMOTED but re-add in progress" or something
|
||||
}
|
||||
|
||||
} else if ( verifiedName && illegalDuplicate) {
|
||||
@ -999,6 +1010,16 @@ const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCo
|
||||
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||
</div>
|
||||
</div>
|
||||
${creator === userState.accountName ? `
|
||||
<div style="margin-top: 0.8em;">
|
||||
<button
|
||||
style="padding: 10px; background: darkred; color: white; border-radius: 4px; cursor: pointer;"
|
||||
onclick="deleteARCard('${cardIdentifier}')"
|
||||
>
|
||||
DELETE CARD
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
|
@ -72,7 +72,8 @@ const loadAdminBoardPage = async () => {
|
||||
mainContent.innerHTML = `
|
||||
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
||||
<h1 style="color: lightblue;">AdminBoard</h1>
|
||||
<p style="font-size: 0.95rem; color:rgba(255, 255, 255, 0.53)"> The Admin Board was meant to be utilized for DECISIONS regarding Minters or would-be Minters, and is encrypted to the Admins so that the data for the DECISIONS remains private. However, it later became the location to REMOVE minters as well. This, not being the original intended purpose has become problematic, as the removal data SHOULD be public. In the future, this data WILL be made public. The Admin Board will continue to be utilized for decision-making, but will NOT be a place for hidden removal data only. </p>
|
||||
<p style="font-size: 1.25em;"> The Admin Board is an encrypted card publishing board to keep track of minter data for the Minter Admins. Any Admin may publish a card, and related data, make comments on existing cards, and vote on existing card data in support or not of the name on the card. It is essentially a 'project management' tool to assist the Minter Admins in keeping track of the data related to minters they are adding/removing from the minter group. </p>
|
||||
<p> More functionality will be added over time. One of the first features will be the ability to output the existing card data 'decisions', to a json formatted list in order to allow crowetic to run his script easily until the final Mintership proposal changes are completed, and the MINTER group is transferred to 'null'.</p>
|
||||
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Encrypted Card</button>
|
||||
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button>
|
||||
<select id="sort-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color:rgb(70, 106, 105); background-color: black;">
|
||||
@ -83,14 +84,12 @@ const loadAdminBoardPage = async () => {
|
||||
<option value="most-votes">Most Votes</option>
|
||||
</select>
|
||||
<select id="time-range-select" style="margin-left: 10px; padding: 5px; font-size: 1.25rem; color: white; background-color: black;">
|
||||
<option value="0">All Creation Dates</option>
|
||||
<option value="1">Last 1 Day</option>
|
||||
<option value="7">Last 7 Days</option>
|
||||
<option value="30">...Within 30 Days</option>
|
||||
<option value="45" selected>Published Within Last 45 Days</option>
|
||||
<option value="60">...Within 60 Days</option>
|
||||
<option value="90">...Within 90 Days</option>
|
||||
</select>
|
||||
<option value="0">Show All</option>
|
||||
<option value="1">Last 1 day</option>
|
||||
<option value="7">Last 7 days</option>
|
||||
<option value="30" selected>Last 30 days</option>
|
||||
<option value="90">Last 90 days</option>
|
||||
</select>
|
||||
<div class="show-card-checkbox" style="margin-top: 1em;">
|
||||
<input type="checkbox" id="admin-show-hidden-checkbox" name="adminHidden" />
|
||||
<label for="admin-show-hidden-checkbox">Show User-Hidden Cards?</label>
|
||||
@ -381,123 +380,11 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
|
||||
selectedSort = sortSelect.value
|
||||
}
|
||||
|
||||
if (selectedSort === 'name') {
|
||||
// Sort alphabetically by the minter's name
|
||||
finalCards.sort((a, b) => {
|
||||
const nameA = a.decryptedCardData.minterName?.toLowerCase() || ''
|
||||
const nameB = b.decryptedCardData.minterName?.toLowerCase() || ''
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
} else if (selectedSort === 'recent-comments') {
|
||||
// We need each card's newest comment timestamp for sorting
|
||||
for (let card of finalCards) {
|
||||
card.newestCommentTimestamp = await getNewestAdminCommentTimestamp(card.card.identifier)
|
||||
}
|
||||
// Then sort descending by newest comment
|
||||
finalCards.sort((a, b) =>
|
||||
(b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
||||
)
|
||||
} else if (selectedSort === 'least-votes') {
|
||||
// TODO: Add the logic to sort by LEAST total ADMIN votes, then totalYesWeight
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
for (const finalCard of finalCards) {
|
||||
try {
|
||||
const pollName = finalCard.decryptedCardData.poll
|
||||
// If card or poll is missing, default to zero
|
||||
if (!pollName) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
const pollResults = await fetchPollResults(pollName)
|
||||
if (!pollResults || pollResults.error) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
// Pull only the adminYes/adminNo/totalYesWeight from processPollData
|
||||
const {
|
||||
adminYes,
|
||||
adminNo,
|
||||
totalYesWeight
|
||||
} = await processPollData(
|
||||
pollResults,
|
||||
minterGroupMembers,
|
||||
minterAdmins,
|
||||
finalCard.decryptedCardData.creator,
|
||||
finalCard.card.identifier
|
||||
)
|
||||
finalCard._adminTotalVotes = adminYes + adminNo
|
||||
finalCard._yesWeight = totalYesWeight
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
}
|
||||
}
|
||||
// Sort ascending by (adminYes + adminNo), then descending by totalYesWeight
|
||||
finalCards.sort((a, b) => {
|
||||
const diffAdminTotal = a._adminTotalVotes - b._adminTotalVotes
|
||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||
// If there's a tie, show the card with higher yesWeight first
|
||||
return b._yesWeight - a._yesWeight
|
||||
})
|
||||
} else if (selectedSort === 'most-votes') {
|
||||
// TODO: Add the logic to sort by MOST total ADMIN votes, then totalYesWeight
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
for (const finalCard of finalCards) {
|
||||
try {
|
||||
const pollName = finalCard.decryptedCardData.poll
|
||||
if (!pollName) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
const pollResults = await fetchPollResults(pollName)
|
||||
if (!pollResults || pollResults.error) {
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
continue
|
||||
}
|
||||
const {
|
||||
adminYes,
|
||||
adminNo,
|
||||
totalYesWeight
|
||||
} = await processPollData(
|
||||
pollResults,
|
||||
minterGroupMembers,
|
||||
minterAdmins,
|
||||
finalCard.decryptedCardData.creator,
|
||||
finalCard.card.identifier
|
||||
)
|
||||
finalCard._adminTotalVotes = adminYes + adminNo
|
||||
finalCard._yesWeight = totalYesWeight
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching or processing poll for card ${finalCard.card.identifier}:`, error)
|
||||
finalCard._adminTotalVotes = 0
|
||||
finalCard._yesWeight = 0
|
||||
}
|
||||
}
|
||||
// Sort descending by (adminYes + adminNo), then descending by totalYesWeight
|
||||
finalCards.sort((a, b) => {
|
||||
const diffAdminTotal = b._adminTotalVotes - a._adminTotalVotes
|
||||
if (diffAdminTotal !== 0) return diffAdminTotal
|
||||
return b._yesWeight - a._yesWeight
|
||||
})
|
||||
} else {
|
||||
// Sort cards by timestamp (most recent first)
|
||||
finalCards.sort((a, b) => {
|
||||
const timestampA = a.card.updated || a.card.created || 0
|
||||
const timestampB = b.card.updated || b.card.created || 0
|
||||
return timestampB - timestampA;
|
||||
})
|
||||
}
|
||||
const sortedFinalCards = await sortCards(finalCards, selectedSort, "admin")
|
||||
|
||||
encryptedCardsContainer.innerHTML = ""
|
||||
|
||||
const finalVisualFilterCards = finalCards.filter(({card}) => {
|
||||
const finalVisualFilterCards = sortedFinalCards.filter(({card}) => {
|
||||
const showKickedBanned = document.getElementById('admin-show-kicked-banned-checkbox')?.checked ?? false
|
||||
const showHiddenAdminCards = document.getElementById('admin-show-hidden-checkbox')?.checked ?? false
|
||||
|
||||
@ -1079,7 +966,7 @@ const createRemoveButtonHtml = (name, cardIdentifier) => {
|
||||
|
||||
const handleKickMinter = async (minterName) => {
|
||||
try {
|
||||
let isAddress = await getAddressInfo(minterName)
|
||||
isAddress = await getAddressInfo(minterName)
|
||||
|
||||
// Optional block check
|
||||
let txGroupId = 0
|
||||
@ -1092,7 +979,7 @@ const handleKickMinter = async (minterName) => {
|
||||
|
||||
// Get the minter address from name info
|
||||
let minterAddress
|
||||
if (!isAddress.address || !isAddress.address != minterName){
|
||||
if (!isAddress){
|
||||
const minterNameInfo = await getNameInfo(minterName)
|
||||
minterAddress = minterNameInfo?.owner
|
||||
} else {
|
||||
@ -1108,7 +995,7 @@ const handleKickMinter = async (minterName) => {
|
||||
const reason = 'Kicked by Minter Admins'
|
||||
const fee = 0.01
|
||||
|
||||
const rawKickTransaction = await createGroupKickTransaction(adminPublicKey, 694, minterAddress, reason, txGroupId, fee)
|
||||
const rawKickTransaction = await createGroupKickTransaction(minterAddress, adminPublicKey, 694, minterAddress, reason, txGroupId, fee)
|
||||
|
||||
const signedKickTransaction = await qortalRequest({
|
||||
action: "SIGN_TRANSACTION",
|
||||
@ -1139,7 +1026,7 @@ const handleKickMinter = async (minterName) => {
|
||||
}
|
||||
|
||||
const handleBanMinter = async (minterName) => {
|
||||
let isAddress = await getAddressInfo(minterName)
|
||||
isAddress = await getAddressInfo(minterName)
|
||||
try {
|
||||
let txGroupId = 0
|
||||
// const { height: currentHeight } = await getLatestBlockInfo()
|
||||
@ -1152,9 +1039,9 @@ const handleBanMinter = async (minterName) => {
|
||||
txGroupId = 694
|
||||
}
|
||||
let minterAddress
|
||||
if (!isAddress.address || !isAddress.address != minterName){
|
||||
if (!isAddress) {
|
||||
const minterNameInfo = await getNameInfo(minterName)
|
||||
minterAddress = minterNameInfo?.owner
|
||||
const minterAddress = minterNameInfo?.owner
|
||||
} else {
|
||||
minterAddress = minterName
|
||||
}
|
||||
@ -1197,20 +1084,36 @@ const handleBanMinter = async (minterName) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getNewestAdminCommentTimestamp = async (cardIdentifier) => {
|
||||
const deleteAdminCard = async (cardIdentifier) => {
|
||||
try {
|
||||
const comments = await fetchEncryptedComments(cardIdentifier)
|
||||
if (!comments || comments.length === 0) {
|
||||
return 0
|
||||
const confirmed = confirm("Are you sure you want to delete this card? This action cannot be undone.")
|
||||
if (!confirmed) return
|
||||
const blankData = {
|
||||
header: "",
|
||||
content: "",
|
||||
links: [],
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: ""
|
||||
}
|
||||
const newestTimestamp = comments.reduce((acc, comment) => {
|
||||
const cTime = comment.updated || comment.created || 0
|
||||
return cTime > acc ? cTime : acc
|
||||
}, 0)
|
||||
return newestTimestamp
|
||||
} catch (err) {
|
||||
console.error('Failed to get newest comment timestamp:', err)
|
||||
return 0
|
||||
let base64Data = await objectToBase64(blankData)
|
||||
if (!base64Data) {
|
||||
base64Data = btoa(JSON.stringify(blankData))
|
||||
}
|
||||
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "MAIL_PRIVATE",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64Data,
|
||||
encrypt: true,
|
||||
publicKeys: verifiedAdminPublicKeys
|
||||
})
|
||||
alert("Your card has been effectively deleted.")
|
||||
} catch (error) {
|
||||
console.error("Error deleting Admin card:", error)
|
||||
alert("Failed to delete the card. Check console for details.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1304,9 +1207,9 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
showRemoveHtml = removeActionsHtml
|
||||
|
||||
if (userVote === 0) {
|
||||
cardColorCode = "rgba(1, 65, 39, 0.41)"; // or any green you want
|
||||
cardColorCode = "rgba(1, 128, 20, 0.35)"; // or any green you want
|
||||
} else if (userVote === 1) {
|
||||
cardColorCode = "rgba(55, 12, 12, 0.61)"; // or any red you want
|
||||
cardColorCode = "rgba(124, 6, 6, 0.45)"; // or any red you want
|
||||
}
|
||||
|
||||
const confirmedKick = finalKickTxs.some(
|
||||
@ -1399,6 +1302,16 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
|
||||
</div>
|
||||
</div>
|
||||
${creator === userState.accountName ? `
|
||||
<div style="margin-top: 0.8em;">
|
||||
<button
|
||||
style="padding: 10px; background: darkred; color: white; border-radius: 4px; cursor: pointer;"
|
||||
onclick="deleteAdminCard('${cardIdentifier}')"
|
||||
>
|
||||
DELETE CARD
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
|
@ -18,23 +18,22 @@ const loadMinterAdminToolsPage = async () => {
|
||||
mainContent.innerHTML = `
|
||||
<div class="tools-main mbr-parallax-background cid-ttRnlSkg2R">
|
||||
<div class="tools-header" style="color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 10px;">
|
||||
|
||||
<div><h1 style="font-size: 50px; margin: 0;">Admin Tools</h1></div>
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
<span>${userState.accountName || 'Guest'}'s Admin Tools</span>
|
||||
<span>${userState.accountName || 'Guest'}</span>
|
||||
</div>
|
||||
|
||||
<div><h2>Welcome to Admin Tools</h2></div>
|
||||
<div>
|
||||
<p style="color:rgba(80, 9, 9, 0.63)"></p>
|
||||
<p style="color:rgb(82, 114, 145)"> The approve feature allows invite by name, and shows ALL existing approvals ongoing, whether initiated on this page manually or not. Allowing for easy displaying and approving without loading the MinterBoard and scrolling through cards. </p>
|
||||
<p style="font-size: 0.85rem"> This is NOT a substitute for the AdminBoard, as obviously no data regarding the account is published here. However, if you have already read the data there, and wish to see it easily in one place, here, that is fine. It can also obviously be utilized to manually invite users that require such actions to be taken, however, this action as well should be extremely limited in usage, and not leveraged without extensive provided rationale.</p>
|
||||
<p>On this page you will find admin functionality for the Q-Mintership App. Including the 'blockList' for blocking comments from certain names, and manual creation of invite transactions.</p>
|
||||
<p>More features will be added as time goes on. This is the start of the functionality here.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tools-submenu" class="tools-submenu">
|
||||
<div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;">
|
||||
<button id="toggle-blocklist-button" style="background-color:rgba(80, 9, 9, 0.63)" class="publish-card-button">Add/Remove blockedUsers</button>
|
||||
<button id="create-group-invite" class="publish-card-button" style="background-color:rgb(82, 114, 145)">Create and Display Pending Group Invites</button>
|
||||
<button id="toggle-blocklist-button" class="publish-card-button">Show/Hide blockedUsers</button>
|
||||
<button id="create-group-invite" class="publish-card-button">Create Pending Group Invite</button>
|
||||
</div>
|
||||
|
||||
<div id="tools-window" class="tools-window" style="margin-top: 2em;">
|
||||
@ -56,30 +55,6 @@ const loadMinterAdminToolsPage = async () => {
|
||||
<button id="blocklist-remove-button" class="publish-card-button">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="invite-container" class="invite-form" style="display: none; flex-direction: column; padding: 0.75em; align-items: center; justify-content: center;">
|
||||
|
||||
<!-- Existing pending invites display -->
|
||||
<div id="pending-invites-display" class="pending-invites-display" style="margin-bottom: 1em;">
|
||||
<!-- We will fill this dynamically with a list/table of pending invites -->
|
||||
</div>
|
||||
|
||||
<!-- Input for name/address -->
|
||||
<h3 style="margin-top: 0;">Manual Group Invite</h3>
|
||||
<input
|
||||
type="text"
|
||||
id="invite-input"
|
||||
class="invite-input"
|
||||
placeholder="Enter name or address to invite"
|
||||
style="margin-bottom: 1em;"
|
||||
/>
|
||||
|
||||
<!-- Button to create the invite transaction -->
|
||||
<div class="invite-button-container publish-card-form">
|
||||
<button id="invite-user-button" class="publish-card-button">Invite User</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -88,10 +63,10 @@ const loadMinterAdminToolsPage = async () => {
|
||||
|
||||
document.body.appendChild(mainContent)
|
||||
|
||||
await addToolsPageEventListeners()
|
||||
addToolsPageEventListeners()
|
||||
}
|
||||
|
||||
const addToolsPageEventListeners= async () => {
|
||||
function addToolsPageEventListeners() {
|
||||
document.getElementById("toggle-blocklist-button").addEventListener("click", async () => {
|
||||
const container = document.getElementById("blocklist-container")
|
||||
// toggle show/hide
|
||||
@ -141,32 +116,6 @@ const addToolsPageEventListeners= async () => {
|
||||
alert(`"${nameToRemove}" removed from the block list (if it was present).`)
|
||||
})
|
||||
|
||||
document.getElementById("invite-user-button").addEventListener("click", async () => {
|
||||
const inviteInput = document.getElementById("invite-input")
|
||||
const nameOrAddress = inviteInput.value.trim()
|
||||
if (!nameOrAddress) return
|
||||
|
||||
try {
|
||||
// We'll call some function handleManualInvite(nameOrAddress)
|
||||
await handleManualInvite(nameOrAddress)
|
||||
inviteInput.value = ""
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error inviting user:", err)
|
||||
alert("Failed to invite user.")
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById("create-group-invite").addEventListener("click", async () => {
|
||||
const inviteContainer = document.getElementById("invite-container")
|
||||
// Toggle display
|
||||
inviteContainer.style.display = (inviteContainer.style.display === "none" ? "flex" : "none")
|
||||
// If showing, load the pending invites
|
||||
if (inviteContainer.style.display === "flex") {
|
||||
const pendingInvites = await fetchPendingInvites()
|
||||
await displayPendingInviteDetails(pendingInvites)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const displayBlockList = (blockedNames) => {
|
||||
@ -182,139 +131,4 @@ const displayBlockList = (blockedNames) => {
|
||||
`
|
||||
}
|
||||
|
||||
const fetchPendingInvites = async () => {
|
||||
try {
|
||||
const { finalInviteTxs, pendingInviteTxs } = await fetchAllInviteTransactions()
|
||||
return pendingInviteTxs
|
||||
} catch (err) {
|
||||
console.error("Error fetching pending invites:", err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const handleManualInvite = async (nameOrAddress) => {
|
||||
const addressInfo = await getAddressInfo(nameOrAddress)
|
||||
let address = addressInfo.address
|
||||
if (addressInfo && address) {
|
||||
console.log(`address is ${address}`)
|
||||
} else {
|
||||
// it might be a Qortal name => getNameInfo
|
||||
const nameData = await getNameInfo(nameOrAddress)
|
||||
if (!nameData || !nameData.owner) {
|
||||
throw new Error(`Cannot find valid address for ${nameOrAddress}`)
|
||||
}
|
||||
address = nameData.owner
|
||||
}
|
||||
|
||||
const adminPublicKey = await getPublicKeyByName(userState.accountName)
|
||||
const timeToLive = 864000 // e.g. 10 days in seconds
|
||||
const fee = 0.01
|
||||
let txGroupId = 694
|
||||
|
||||
// build the raw invite transaction
|
||||
const rawInviteTransaction = await createGroupInviteTransaction(
|
||||
address,
|
||||
adminPublicKey,
|
||||
694,
|
||||
address,
|
||||
timeToLive,
|
||||
txGroupId,
|
||||
fee
|
||||
)
|
||||
|
||||
// sign
|
||||
const signedTransaction = await qortalRequest({
|
||||
action: "SIGN_TRANSACTION",
|
||||
unsignedBytes: rawInviteTransaction
|
||||
})
|
||||
if (!signedTransaction) {
|
||||
throw new Error("SIGN_TRANSACTION returned null. Possibly user canceled or an older UI?")
|
||||
}
|
||||
|
||||
// process
|
||||
const processResponse = await processTransaction(signedTransaction)
|
||||
if (!processResponse) {
|
||||
throw new Error("Failed to process transaction. Possibly canceled or error from Qortal Core.")
|
||||
}
|
||||
|
||||
alert(`Invite transaction submitted for ${nameOrAddress}. Wait for confirmation.`)
|
||||
}
|
||||
|
||||
|
||||
const displayPendingInviteDetails = async (pendingInvites) => {
|
||||
const invitesContainer = document.getElementById('pending-invites-display')
|
||||
if (!pendingInvites || pendingInvites.length === 0) {
|
||||
invitesContainer.innerHTML = "<p>No pending invites found.</p>"
|
||||
return
|
||||
}
|
||||
|
||||
let html = `<h4>Current Pending Invites:</h4><div class="pending-invites-list">`
|
||||
|
||||
for (const inviteTx of pendingInvites) {
|
||||
const inviteeAddress = inviteTx.invitee
|
||||
const dateStr = new Date(inviteTx.timestamp).toLocaleString()
|
||||
let inviteeName = ""
|
||||
const txSig = inviteTx.signature
|
||||
const creatorName = await getNameFromAddress(inviteTx.creatorAddress)
|
||||
if (!creatorName) {
|
||||
creatorName = inviteTx.creatorAddress
|
||||
}
|
||||
|
||||
try {
|
||||
// fetch the name from address, if it fails we keep it blank or fallback to the address
|
||||
inviteeName = await getNameFromAddress(inviteeAddress)
|
||||
if (!inviteeName || inviteeName === inviteeAddress) {
|
||||
inviteeName = inviteeAddress // fallback
|
||||
}
|
||||
} catch (err) {
|
||||
inviteeName = inviteeAddress // fallback if getName fails
|
||||
}
|
||||
|
||||
const approvalSearchResults = await searchTransactions({
|
||||
txTypes: ['GROUP_APPROVAL'],
|
||||
confirmationStatus: 'CONFIRMED',
|
||||
limit: 0,
|
||||
reverse: false,
|
||||
offset: 0,
|
||||
startBlock: 1990000,
|
||||
blockLimit: 0,
|
||||
txGroupId: 0
|
||||
})
|
||||
|
||||
const approvals = approvalSearchResults.filter(
|
||||
(approvalTx) => approvalTx.pendingSignature === txSig
|
||||
)
|
||||
|
||||
const { tableHtml, approvalCount = approvals.length } = await buildApprovalTableHtml(approvals, getNameFromAddress)
|
||||
const finalTable = approvals.length > 0 ? tableHtml : "<p>No Approvals Found</p>"
|
||||
|
||||
html += `
|
||||
<div class="invite-item">
|
||||
<div class="invite-top-row">
|
||||
<span><strong>Invite Tx</strong>:<p style="color:lightblue"> ${inviteTx.signature.slice(0, 8)}...</p></span>
|
||||
<span> <strong>Invitee</strong>:<p style="color:lightblue"> ${inviteeName}</p></span>
|
||||
<span> <strong>Date</strong>:<p style="color:lightblue"> ${dateStr}</p></span>
|
||||
<span> <strong>CreatorName</strong>:<p style="color:lightblue"> ${creatorName}</p></span>
|
||||
<span> <strong>Total Approvals</strong>:<p style="color:lightblue"> ${approvalCount}</p></span>
|
||||
|
||||
</div>
|
||||
<!-- Next line for approvals -->
|
||||
<div class="invite-approvals">
|
||||
<strong>Existing Approvals:</strong>
|
||||
${finalTable}
|
||||
</div>
|
||||
<button
|
||||
class="approve-invite-list-button"
|
||||
onclick="handleGroupApproval('${inviteTx.signature}')"
|
||||
>
|
||||
Approve Invite
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
html += "</div>"
|
||||
invitesContainer.innerHTML = html
|
||||
}
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
const Q_MINTERSHIP_VERSION = "1.22"
|
||||
const Q_MINTERSHIP_VERSION = "1.06.1"
|
||||
|
||||
const messageIdentifierPrefix = `mintership-forum-message`
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`
|
||||
@ -78,28 +78,28 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault()
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
await loadForumPage();
|
||||
await loadForumPage()
|
||||
loadRoomContent("general")
|
||||
startPollingForNewMessages()
|
||||
createScrollToTopButton()
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]')
|
||||
minterBoardLinks.forEach(link => {
|
||||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
event.preventDefault()
|
||||
if (typeof loadMinterBoardPage === "undefined") {
|
||||
console.log("loadMinterBoardPage not found, loading script dynamically...")
|
||||
await loadScript("./assets/js/MinterBoard.js")
|
||||
}
|
||||
await loadMinterBoardPage()
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -107,15 +107,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
addRemoveAdminLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault()
|
||||
// Possibly require user to login if not logged
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
if (typeof loadMinterBoardPage === "undefined") {
|
||||
console.log("loadMinterBoardPage not found, loading script dynamically...")
|
||||
await loadScript("./assets/js/MinterBoard.js")
|
||||
}
|
||||
await loadAddRemoveAdminPage()
|
||||
if (!userState.isLoggedIn) {
|
||||
await login()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -240,7 +239,10 @@ const loadForumPage = async () => {
|
||||
<div class="forum-main mbr-parallax-background cid-ttRnlSkg2R">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: white; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
${userState.isLoggedIn ? `
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
` : ''
|
||||
}
|
||||
<span>${userState.accountName || 'Guest'}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -468,6 +470,7 @@ let selectedImages = []
|
||||
let selectedFiles = []
|
||||
let multiResource = []
|
||||
let attachmentIdentifiers = []
|
||||
let editMessageIdentifier = null
|
||||
|
||||
// Set up file input handling
|
||||
const setupFileInputs = (room) => {
|
||||
@ -558,9 +561,14 @@ const processSelectedImages = async (selectedImages, multiResource, room) => {
|
||||
|
||||
// Handle send message
|
||||
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
|
||||
const messageIdentifier = room === "admins"
|
||||
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
|
||||
: `${messageIdentifierPrefix}-${room}-${randomID()}`
|
||||
let messageIdentifier
|
||||
if (editMessageIdentifier) {
|
||||
messageIdentifier = editMessageIdentifier
|
||||
} else {
|
||||
messageIdentifier = room === "admins"
|
||||
? `${messageIdentifierPrefix}-${room}-e-${randomID()}`
|
||||
: `${messageIdentifierPrefix}-${room}-${randomID()}`
|
||||
}
|
||||
|
||||
try {
|
||||
// Process selected images
|
||||
@ -644,6 +652,61 @@ const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImage
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteMessage = async (room, existingMessageIdentifier) => {
|
||||
try {
|
||||
const blankMessageObject = {
|
||||
messageHtml: "<em>This post has been deleted.</em>",
|
||||
hasAttachment: false,
|
||||
attachments: [],
|
||||
replyTo: null
|
||||
}
|
||||
const base64Message = btoa(JSON.stringify(blankMessageObject))
|
||||
const service = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST"
|
||||
const request = {
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: userState.accountName,
|
||||
service: service,
|
||||
identifier: existingMessageIdentifier,
|
||||
data64: base64Message
|
||||
}
|
||||
if (room === "admins") {
|
||||
request.encrypt = true
|
||||
request.publicKeys = adminPublicKeys
|
||||
}
|
||||
console.log("Deleting forum message...")
|
||||
await qortalRequest(request)
|
||||
} catch (err) {
|
||||
console.error("Error deleting message:", err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditMessage = async (room, existingMessageIdentifier) => {
|
||||
try {
|
||||
const editedMessageObject = {
|
||||
messageHtml: "", // TODO: Add Quill editor content here
|
||||
hasAttachment: false,
|
||||
attachments: [],
|
||||
replyTo: null
|
||||
}
|
||||
const base64Message = btoa(JSON.stringify(editedMessageObject))
|
||||
const service = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST"
|
||||
const request = {
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: userState.accountName,
|
||||
service: service,
|
||||
identifier: existingMessageIdentifier,
|
||||
data64: base64Message
|
||||
}
|
||||
if (room === "admins") {
|
||||
request.encrypt = true
|
||||
request.publicKeys = adminPublicKeys
|
||||
}
|
||||
console.log("Editing forum message...")
|
||||
await qortalRequest(request)
|
||||
} catch (err) {
|
||||
console.error("Error editing message:", err)
|
||||
}
|
||||
}
|
||||
|
||||
function clearInputs() {
|
||||
// Clear the file input elements and preview container
|
||||
@ -662,6 +725,7 @@ function clearInputs() {
|
||||
attachmentIdentifiers = []
|
||||
selectedImages = []
|
||||
selectedFiles = []
|
||||
editMessageIdentifier = null
|
||||
|
||||
// Remove the reply container
|
||||
const replyContainer = document.querySelector('.reply-container')
|
||||
@ -760,6 +824,8 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
}
|
||||
|
||||
handleReplyLogic(fetchMessages)
|
||||
handleDeleteLogic(fetchMessages, room)
|
||||
handleEditLogic(fetchMessages, room)
|
||||
|
||||
await updatePaginationControls(room, limit)
|
||||
} catch (error) {
|
||||
@ -979,6 +1045,24 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
|
||||
const replyHtml = await buildReplyHtml(message, room)
|
||||
const attachmentHtml = await buildAttachmentHtml(message, room)
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`
|
||||
let deleteButtonHtml = ''
|
||||
let editButtonHtml = ''
|
||||
if (message.name === userState.accountName) {
|
||||
deleteButtonHtml = `
|
||||
<button class="delete-button"
|
||||
data-message-identifier="${message.identifier}"
|
||||
data-room="${room}">
|
||||
Delete
|
||||
</button>
|
||||
`
|
||||
editButtonHtml = `
|
||||
<button class="edit-button"
|
||||
data-message-identifier="${message.identifier}"
|
||||
data-room="${room}">
|
||||
Edit
|
||||
</button>
|
||||
`
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
@ -995,7 +1079,10 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
|
||||
<div class="attachments-gallery">
|
||||
${attachmentHtml}
|
||||
</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
<div class="message-actions">
|
||||
${deleteButtonHtml}${editButtonHtml}
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@ -1147,6 +1234,41 @@ const handleReplyLogic = (fetchMessages) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteLogic = (fetchMessages, room) => {
|
||||
// Only select buttons that do NOT already have a listener
|
||||
const deleteButtons = document.querySelectorAll('.delete-button:not(.bound-delete)')
|
||||
deleteButtons.forEach(button => {
|
||||
button.classList.add('bound-delete')
|
||||
button.addEventListener('click', async () => {
|
||||
const messageId = button.dataset.messageIdentifier
|
||||
const postRoom = button.dataset.room
|
||||
const msg = fetchMessages.find(m => m && m.identifier === messageId)
|
||||
if (msg) {
|
||||
const confirmed = confirm("Are you sure you want to delete this post?")
|
||||
if (!confirmed) return
|
||||
await handleDeleteMessage(postRoom, messageId)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleEditLogic = (fetchMessages, room) => {
|
||||
// Only select buttons that do NOT already have a listener
|
||||
const editButtons = document.querySelectorAll('.edit-button:not(.bound-edit)')
|
||||
editButtons.forEach(button => {
|
||||
button.classList.add('bound-edit')
|
||||
button.addEventListener('click', async () => {
|
||||
const existingMessageIdentifier = button.dataset.messageIdentifier
|
||||
const originalMessage = messagesById[existingMessageIdentifier]
|
||||
if (!originalMessage) return
|
||||
editMessageIdentifier = existingMessageIdentifier
|
||||
const quill = new Quill('#editor')
|
||||
quill.clipboard.dangerouslyPasteHTML(originalMessage.content)
|
||||
// Optionally show a small notice: "Editing message..."
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const showReplyPreview = (repliedMessage) => {
|
||||
replyToMessageIdentifier = repliedMessage.identifier
|
||||
|
||||
|
@ -6,11 +6,6 @@ let baseUrl = ''
|
||||
let isOutsideOfUiDevelopment = false
|
||||
let nullAddress = 'QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG'
|
||||
|
||||
// Caching to improve performance
|
||||
const nameInfoCache = new Map(); // name -> nameInfo
|
||||
const addressInfoCache = new Map(); // address -> addressInfo
|
||||
const pollResultsCache = new Map(); // pollName -> pollResults
|
||||
|
||||
if (typeof qortalRequest === 'function') {
|
||||
console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.')
|
||||
isOutsideOfUiDevelopment = false
|
||||
@ -58,7 +53,7 @@ const timestampToHumanReadableDate = async(timestamp) => {
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
const formattedDate = `${day}.${month}.${year}..@${hours}:${minutes}:${seconds}`
|
||||
const formattedDate = `${year}.${month}.${day} @ ${hours}:${minutes}:${seconds}`
|
||||
console.log('Formatted date:', formattedDate)
|
||||
return formattedDate
|
||||
}
|
||||
@ -228,20 +223,7 @@ const getUserAddress = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getAddressInfoCached = async (address) => {
|
||||
if (addressInfoCache.has(address)) return addressInfoCache.get(address)
|
||||
const result = await getAddressInfo(address)
|
||||
addressInfoCache.set(address, result)
|
||||
return result
|
||||
}
|
||||
|
||||
const getAddressInfo = async (address) => {
|
||||
const qortalAddressPattern = /^Q[A-Za-z0-9]{33}$/ // Q + 33 almum = 34 total length
|
||||
|
||||
if (!qortalAddressPattern.test(address)) {
|
||||
console.warn(`Not a valid Qortal address format, returning same thing that was passed to not break other functions: ${address}`)
|
||||
return address
|
||||
}
|
||||
try {
|
||||
const response = await fetch (`${baseUrl}/addresses/${address}`, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
@ -266,19 +248,6 @@ const getAddressInfo = async (address) => {
|
||||
}
|
||||
}
|
||||
|
||||
const nameToAddressCache = new Map()
|
||||
const fetchOwnerAddressFromNameCached = async (name) => {
|
||||
if (nameToAddressCache.has(name)) {
|
||||
return nameToAddressCache.get(name)
|
||||
}
|
||||
|
||||
const address = await fetchOwnerAddressFromName(name)
|
||||
|
||||
nameToAddressCache.set(name, address)
|
||||
return address
|
||||
}
|
||||
|
||||
|
||||
const fetchOwnerAddressFromName = async (name) => {
|
||||
console.log('fetchOwnerAddressFromName called')
|
||||
console.log('name:', name)
|
||||
@ -357,15 +326,6 @@ const verifyAddressIsAdmin = async (address) => {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const getNameInfoCached = async (name) => {
|
||||
if (nameInfoCache.has(name)) {
|
||||
return nameInfoCache.get(name)
|
||||
}
|
||||
const result = await getNameInfo(name)
|
||||
nameInfoCache.set(name, result)
|
||||
return result
|
||||
}
|
||||
|
||||
const getNameInfo = async (name) => {
|
||||
console.log('getNameInfo called')
|
||||
@ -826,20 +786,18 @@ const searchSimple = async (service, identifier, name, limit=1500, offset=0, roo
|
||||
|
||||
if (name && !identifier && !room) {
|
||||
console.log('name only searchSimple', name)
|
||||
urlSuffix = `service=${service}&name=${name}&limit=${limit}&prefix=true&reverse=${reverse}&after=${after}`
|
||||
console.log(`urlSuffix used: ${urlSuffix}`)
|
||||
urlSuffix = `service=${service}&name=${name}&limit=${limit}&prefix=true&reverse=${reverse}`
|
||||
|
||||
} else if (!name && identifier && !room) {
|
||||
console.log('identifier only searchSimple', identifier)
|
||||
urlSuffix = `service=${service}&identifier=${identifier}&limit=${limit}&prefix=true&reverse=${reverse}&after=${after}`
|
||||
console.log(`urlSuffix used: ${urlSuffix}`)
|
||||
urlSuffix = `service=${service}&identifier=${identifier}&limit=${limit}&prefix=true&reverse=${reverse}`
|
||||
|
||||
} else if (!name && !identifier && !room) {
|
||||
console.error(`name: ${name} AND identifier: ${identifier} not passed. Must include at least one...`)
|
||||
return null
|
||||
|
||||
} else {
|
||||
console.log(`final searchSimple params = service: '${service}', identifier: '${identifier}', name: '${name}', limit: '${limit}', offset: '${offset}', room: '${room}', reverse: '${reverse}', after: ${after}`)
|
||||
console.log(`final searchSimple params = service: '${service}', identifier: '${identifier}', name: '${name}', limit: '${limit}', offset: '${offset}', room: '${room}', reverse: '${reverse}'`)
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/arbitrary/resources/searchsimple?${urlSuffix}`, {
|
||||
@ -1273,20 +1231,6 @@ const getProductDetails = async (service, name, identifier) => {
|
||||
|
||||
// Qortal poll-related calls ----------------------------------------------------------------------
|
||||
|
||||
const pollOwnerAddrCache = new Map()
|
||||
|
||||
const getPollOwnerAddressCached = async (pollName) => {
|
||||
if (pollOwnerAddrCache.has(pollName)) {
|
||||
return pollOwnerAddrCache.get(pollName)
|
||||
}
|
||||
|
||||
const ownerAddress = await getPollOwnerAddress(pollName)
|
||||
|
||||
// Store in cache
|
||||
pollOwnerAddrCache.set(pollName, ownerAddress)
|
||||
return ownerAddress
|
||||
}
|
||||
|
||||
const getPollOwnerAddress = async (pollName) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/polls/${pollName}`, {
|
||||
@ -1315,15 +1259,6 @@ const getPollPublisherPublicKey = async (pollName) => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPollResultsCached = async (pollName) => {
|
||||
if (pollResultsCache.has(pollName)) {
|
||||
return pollResultsCache.get(pollName)
|
||||
}
|
||||
const result = await fetchPollResults(pollName)
|
||||
pollResultsCache.set(pollName, result)
|
||||
return result
|
||||
}
|
||||
|
||||
const fetchPollResults = async (pollName) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {
|
||||
@ -1411,7 +1346,7 @@ const processTransaction = async (signedTransaction) => {
|
||||
|
||||
// Create a group invite transaction. This will utilize a default timeToLive (which is how long the tx will be alive, not the time until it IS live...) of 10 days in seconds, as the legacy UI has a bug that doesn't display invites older than 10 days.
|
||||
// We will also default to the MINTER group for groupId, AFTER the GROUP_APPROVAL changes, the txGroupId will need to be set for tx that require approval.
|
||||
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive=0, txGroupId, fee) => {
|
||||
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive, txGroupId, fee) => {
|
||||
|
||||
try {
|
||||
// Fetch account reference correctly
|
||||
@ -1460,16 +1395,16 @@ const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, gr
|
||||
}
|
||||
}
|
||||
|
||||
const createGroupKickTransaction = async (adminPublicKey, groupId=694, member, reason='Kicked by admins', txGroupId=694, fee=0.01) => {
|
||||
const createGroupKickTransaction = async (recipientAddress, adminPublicKey, groupId=694, member, reason='Kicked by admins', txGroupId, fee) => {
|
||||
|
||||
try {
|
||||
// Fetch account reference correctly
|
||||
const accountInfo = await getAddressInfo(member)
|
||||
const accountInfo = await getAddressInfo(recipientAddress)
|
||||
const accountReference = accountInfo.reference
|
||||
|
||||
// Validate inputs before making the request
|
||||
if (!adminPublicKey || !accountReference || !member) {
|
||||
throw new Error("Missing required parameters for group kick transaction.")
|
||||
if (!adminPublicKey || !accountReference || !recipientAddress) {
|
||||
throw new Error("Missing required parameters for group invite transaction.")
|
||||
}
|
||||
|
||||
const payload = {
|
||||
@ -1477,10 +1412,11 @@ const createGroupKickTransaction = async (adminPublicKey, groupId=694, member, r
|
||||
reference: accountReference,
|
||||
fee,
|
||||
txGroupId,
|
||||
recipient: null,
|
||||
adminPublicKey,
|
||||
groupId,
|
||||
member,
|
||||
reason
|
||||
groupId: groupId,
|
||||
member: member || recipientAddress,
|
||||
reason: reason
|
||||
}
|
||||
|
||||
console.log("Sending GROUP_KICK transaction payload:", payload)
|
||||
|
@ -196,25 +196,426 @@ const fetchAllInviteTransactions = async () => {
|
||||
|
||||
const { finalTx: finalInviteTxs, pendingTx: pendingInviteTxs } = partitionTransactions(allInviteTx)
|
||||
|
||||
console.log('Final InviteTxs:', finalInviteTxs)
|
||||
console.log('Pending InviteTxs:', pendingInviteTxs)
|
||||
console.log('Final kickTxs:', finalInviteTxs)
|
||||
console.log('Pending kickTxs:', pendingInviteTxs)
|
||||
|
||||
return {
|
||||
finalInviteTxs,
|
||||
pendingInviteTxs,
|
||||
}
|
||||
}
|
||||
|
||||
const findPendingApprovalsForTxSignature = async (txSignature, txType='GROUP_APPROVAL', limit=0, offset=0) => {
|
||||
const pendingTxs = await searchPendingTransactions(limit, offset)
|
||||
|
||||
// Filter only the relevant GROUP_APPROVAL TX referencing txSignature
|
||||
const approvals = pendingTxs.filter(tx =>
|
||||
tx.type === txType && tx.pendingSignature === txSignature
|
||||
)
|
||||
console.log(`approvals found:`,approvals)
|
||||
return approvals
|
||||
const sortCards = async (cardsArray, selectedSort, board) => {
|
||||
// Default sort is by newest if none provided
|
||||
if (!selectedSort) selectedSort = 'newest'
|
||||
switch (selectedSort) {
|
||||
case 'name':
|
||||
// Sort by name
|
||||
cardsArray.sort((a, b) => {
|
||||
const nameA = (board === "admin")
|
||||
? (a.decryptedCardData?.minterName || '').toLowerCase()
|
||||
: ((board === "ar")
|
||||
? (a.minterName?.toLowerCase() || '')
|
||||
: (a.name?.toLowerCase() || '')
|
||||
)
|
||||
const nameB = (board === "admin")
|
||||
? (b.decryptedCardData?.minterName || '').toLowerCase()
|
||||
: ((board === "ar")
|
||||
? (b.minterName?.toLowerCase() || '')
|
||||
: (b.name?.toLowerCase() || '')
|
||||
)
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
break
|
||||
case 'recent-comments':
|
||||
// Sort by newest comment timestamp
|
||||
for (let card of cardsArray) {
|
||||
const cardIdentifier = (board === "admin")
|
||||
? card.card.identifier
|
||||
: card.identifier
|
||||
card.newestCommentTimestamp = await getNewestCommentTimestamp(cardIdentifier, board)
|
||||
}
|
||||
cardsArray.sort((a, b) => {
|
||||
return (b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0)
|
||||
})
|
||||
break
|
||||
case 'least-votes':
|
||||
await applyVoteSortingData(cardsArray, /* ascending= */ true, board)
|
||||
break
|
||||
case 'most-votes':
|
||||
await applyVoteSortingData(cardsArray, /* ascending= */ false, board)
|
||||
break
|
||||
default:
|
||||
// Sort by date
|
||||
cardsArray.sort((a, b) => {
|
||||
const timestampA = (board === "admin")
|
||||
? a.card.updated || a.card.created || 0
|
||||
: a.updated || a.created || 0
|
||||
const timestampB = (board === "admin")
|
||||
? b.card.updated || b.card.created || 0
|
||||
: b.updated || b.created || 0
|
||||
return timestampB - timestampA;
|
||||
})
|
||||
break
|
||||
}
|
||||
return cardsArray
|
||||
}
|
||||
|
||||
|
||||
const getNewestCommentTimestamp = async (cardIdentifier, board) => {
|
||||
try {
|
||||
const comments = (board === "admin") ? await fetchEncryptedComments(cardIdentifier) : await fetchCommentsForCard(cardIdentifier)
|
||||
if (!comments || comments.length === 0) {
|
||||
return 0
|
||||
}
|
||||
const newestTimestamp = comments.reduce((acc, c) => {
|
||||
const cTime = c.updated || c.created || 0
|
||||
return (cTime > acc) ? cTime : acc
|
||||
}, 0)
|
||||
return newestTimestamp
|
||||
} catch (err) {
|
||||
console.error('Failed to get newest comment timestamp:', err)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
const applyVoteSortingData = async (cards, ascending = true, boardType = 'minter') => {
|
||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
||||
const minterAdmins = await fetchMinterGroupAdmins()
|
||||
for (const card of cards) {
|
||||
try {
|
||||
if (boardType === 'admin') {
|
||||
// For the Admin board, we already have the poll name in `card.decryptedCardData.poll`
|
||||
// No need to fetch the resource from BLOG_POST
|
||||
const pollName = card.decryptedCardData?.poll
|
||||
if (!pollName) {
|
||||
// No poll => no votes
|
||||
card._adminYes = 0
|
||||
card._adminNo = 0
|
||||
card._minterYes = 0
|
||||
card._minterNo = 0
|
||||
continue
|
||||
}
|
||||
// Fetch poll results
|
||||
const pollResults = await fetchPollResults(pollName)
|
||||
if (!pollResults) {
|
||||
card._adminYes = 0
|
||||
card._adminNo = 0
|
||||
card._minterYes = 0
|
||||
card._minterNo = 0
|
||||
continue
|
||||
}
|
||||
// Process them
|
||||
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
|
||||
pollResults,
|
||||
minterGroupMembers,
|
||||
minterAdmins,
|
||||
card.decryptedCardData.creator,
|
||||
card.card.identifier
|
||||
)
|
||||
card._adminYes = adminYes
|
||||
card._adminNo = adminNo
|
||||
card._minterYes = minterYes
|
||||
card._minterNo = minterNo
|
||||
} else {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
})
|
||||
if (!cardDataResponse || !cardDataResponse.poll) {
|
||||
card._adminYes = 0
|
||||
card._adminNo = 0
|
||||
card._minterYes = 0
|
||||
card._minterNo = 0
|
||||
continue
|
||||
}
|
||||
const pollResults = await fetchPollResults(cardDataResponse.poll);
|
||||
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
|
||||
pollResults,
|
||||
minterGroupMembers,
|
||||
minterAdmins,
|
||||
cardDataResponse.creator,
|
||||
card.identifier
|
||||
)
|
||||
card._adminYes = adminYes
|
||||
card._adminNo = adminNo
|
||||
card._minterYes = minterYes
|
||||
card._minterNo = minterNo
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching or processing poll for card ${card.identifier}:`, error)
|
||||
card._adminYes = 0
|
||||
card._adminNo = 0
|
||||
card._minterYes = 0
|
||||
card._minterNo = 0
|
||||
}
|
||||
}
|
||||
if (ascending) {
|
||||
// least votes first
|
||||
cards.sort((a, b) => {
|
||||
const diffAdminYes = a._adminYes - b._adminYes
|
||||
if (diffAdminYes !== 0) return diffAdminYes
|
||||
const diffAdminNo = b._adminNo - a._adminNo
|
||||
if (diffAdminNo !== 0) return diffAdminNo
|
||||
const diffMinterYes = a._minterYes - b._minterYes
|
||||
if (diffMinterYes !== 0) return diffMinterYes
|
||||
return b._minterNo - a._minterNo
|
||||
})
|
||||
} else {
|
||||
// most votes first
|
||||
cards.sort((a, b) => {
|
||||
const diffAdminYes = b._adminYes - a._adminYes
|
||||
if (diffAdminYes !== 0) return diffAdminYes
|
||||
const diffAdminNo = a._adminNo - b._adminNo
|
||||
if (diffAdminNo !== 0) return diffAdminNo
|
||||
const diffMinterYes = b._minterYes - a._minterYes
|
||||
if (diffMinterYes !== 0) return diffMinterYes
|
||||
return a._minterNo - b._minterNo
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let globalVoterMap = new Map()
|
||||
|
||||
const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator, cardIdentifier) => {
|
||||
if (!pollData || !Array.isArray(pollData.voteWeights) || !Array.isArray(pollData.votes)) {
|
||||
console.warn("Poll data is missing or invalid. pollData:", pollData)
|
||||
return {
|
||||
adminYes: 0,
|
||||
adminNo: 0,
|
||||
minterYes: 0,
|
||||
minterNo: 0,
|
||||
totalYes: 0,
|
||||
totalNo: 0,
|
||||
totalYesWeight: 0,
|
||||
totalNoWeight: 0,
|
||||
detailsHtml: `<p>Poll data is invalid or missing.</p>`,
|
||||
userVote: null
|
||||
}
|
||||
}
|
||||
|
||||
const memberAddresses = minterGroupMembers.map(m => m.member)
|
||||
const minterAdminAddresses = minterAdmins.map(m => m.member)
|
||||
const adminGroupsMembers = await fetchAllAdminGroupsMembers()
|
||||
const featureTriggerPassed = await featureTriggerCheck()
|
||||
const groupAdminAddresses = adminGroupsMembers.map(m => m.member)
|
||||
let adminAddresses = [...minterAdminAddresses]
|
||||
|
||||
if (!featureTriggerPassed) {
|
||||
console.log(`featureTrigger is NOT passed, only showing admin results from Minter Admins and Group Admins`)
|
||||
adminAddresses = [...minterAdminAddresses, ...groupAdminAddresses]
|
||||
}
|
||||
|
||||
let adminYes = 0, adminNo = 0
|
||||
let minterYes = 0, minterNo = 0
|
||||
let yesWeight = 0, noWeight = 0
|
||||
let userVote = null
|
||||
|
||||
for (const w of pollData.voteWeights) {
|
||||
if (w.optionName.toLowerCase() === 'yes') {
|
||||
yesWeight = w.voteWeight
|
||||
} else if (w.optionName.toLowerCase() === 'no') {
|
||||
noWeight = w.voteWeight
|
||||
}
|
||||
}
|
||||
|
||||
const voterPromises = pollData.votes.map(async (vote) => {
|
||||
const optionIndex = vote.optionIndex; // 0 => yes, 1 => no
|
||||
const voterPublicKey = vote.voterPublicKey
|
||||
const voterAddress = await getAddressFromPublicKey(voterPublicKey)
|
||||
|
||||
if (voterAddress === userState.accountAddress) {
|
||||
userVote = optionIndex
|
||||
}
|
||||
|
||||
if (optionIndex === 0) {
|
||||
if (adminAddresses.includes(voterAddress)) {
|
||||
adminYes++
|
||||
} else if (memberAddresses.includes(voterAddress)) {
|
||||
minterYes++
|
||||
} else {
|
||||
console.log(`voter ${voterAddress} is not a minter nor an admin... Not included in aggregates.`)
|
||||
}
|
||||
} else if (optionIndex === 1) {
|
||||
if (adminAddresses.includes(voterAddress)) {
|
||||
adminNo++
|
||||
} else if (memberAddresses.includes(voterAddress)) {
|
||||
minterNo++
|
||||
} else {
|
||||
console.log(`voter ${voterAddress} is not a minter nor an admin... Not included in aggregates.`)
|
||||
}
|
||||
}
|
||||
|
||||
let voterName = ''
|
||||
try {
|
||||
const nameInfo = await getNameFromAddress(voterAddress)
|
||||
if (nameInfo) {
|
||||
voterName = nameInfo
|
||||
if (nameInfo === voterAddress) voterName = ''
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`No name for address ${voterAddress}`, err)
|
||||
}
|
||||
|
||||
let blocksMinted = 0
|
||||
try {
|
||||
const addressInfo = await getAddressInfo(voterAddress)
|
||||
blocksMinted = addressInfo?.blocksMinted || 0
|
||||
} catch (e) {
|
||||
console.warn(`Failed to get addressInfo for ${voterAddress}`, e)
|
||||
}
|
||||
const isAdmin = adminAddresses.includes(voterAddress)
|
||||
const isMinter = memberAddresses.includes(voterAddress)
|
||||
|
||||
return {
|
||||
optionIndex,
|
||||
voterPublicKey,
|
||||
voterAddress,
|
||||
voterName,
|
||||
isAdmin,
|
||||
isMinter,
|
||||
blocksMinted
|
||||
}
|
||||
})
|
||||
|
||||
const allVoters = await Promise.all(voterPromises)
|
||||
const yesVoters = []
|
||||
const noVoters = []
|
||||
let totalMinterAndAdminYesWeight = 0
|
||||
let totalMinterAndAdminNoWeight = 0
|
||||
|
||||
for (const v of allVoters) {
|
||||
if (v.optionIndex === 0) {
|
||||
yesVoters.push(v)
|
||||
totalMinterAndAdminYesWeight+=v.blocksMinted
|
||||
} else if (v.optionIndex === 1) {
|
||||
noVoters.push(v)
|
||||
totalMinterAndAdminNoWeight+=v.blocksMinted
|
||||
}
|
||||
}
|
||||
|
||||
yesVoters.sort((a,b) => b.blocksMinted - a.blocksMinted)
|
||||
noVoters.sort((a,b) => b.blocksMinted - a.blocksMinted)
|
||||
const sortedAllVoters = allVoters.sort((a,b) => b.blocksMinted - a.blocksMinted)
|
||||
await createVoterMap(sortedAllVoters, cardIdentifier)
|
||||
|
||||
const yesTableHtml = buildVotersTableHtml(yesVoters, /* tableColor= */ "green")
|
||||
const noTableHtml = buildVotersTableHtml(noVoters, /* tableColor= */ "red")
|
||||
const detailsHtml = `
|
||||
<div class="poll-details-container" id'"${creator}-poll-details">
|
||||
<h1 style ="color:rgb(123, 123, 85); text-align: center; font-size: 2.0rem">${creator}'s</h1><h3 style="color: white; text-align: center; font-size: 1.8rem"> Support Poll Result Details</h3>
|
||||
<h4 style="color: green; text-align: center;">Yes Vote Details</h4>
|
||||
${yesTableHtml}
|
||||
<h4 style="color: red; text-align: center; margin-top: 2em;">No Vote Details</h4>
|
||||
${noTableHtml}
|
||||
</div>
|
||||
`
|
||||
const totalYes = adminYes + minterYes
|
||||
const totalNo = adminNo + minterNo
|
||||
|
||||
return {
|
||||
adminYes,
|
||||
adminNo,
|
||||
minterYes,
|
||||
minterNo,
|
||||
totalYes,
|
||||
totalNo,
|
||||
totalYesWeight: totalMinterAndAdminYesWeight,
|
||||
totalNoWeight: totalMinterAndAdminNoWeight,
|
||||
detailsHtml,
|
||||
userVote
|
||||
}
|
||||
}
|
||||
|
||||
const createVoterMap = async (voters, cardIdentifier) => {
|
||||
const voterMap = new Map()
|
||||
voters.forEach((voter) => {
|
||||
const voterNameOrAddress = voter.voterName || voter.voterAddress
|
||||
voterMap.set(voterNameOrAddress, {
|
||||
vote: voter.optionIndex === 0 ? "yes" : "no", // Use optionIndex directly
|
||||
voterType: voter.isAdmin ? "Admin" : voter.isMinter ? "Minter" : "User",
|
||||
blocksMinted: voter.blocksMinted,
|
||||
})
|
||||
})
|
||||
globalVoterMap.set(cardIdentifier, voterMap)
|
||||
}
|
||||
|
||||
const buildVotersTableHtml = (voters, tableColor) => {
|
||||
if (!voters.length) {
|
||||
return `<p>No voters here.</p>`
|
||||
}
|
||||
|
||||
// Decide extremely dark background for the <tbody>
|
||||
let bodyBackground
|
||||
if (tableColor === "green") {
|
||||
bodyBackground = "rgba(0, 18, 0, 0.8)" // near-black green
|
||||
} else if (tableColor === "red") {
|
||||
bodyBackground = "rgba(30, 0, 0, 0.8)" // near-black red
|
||||
} else {
|
||||
// fallback color if needed
|
||||
bodyBackground = "rgba(40, 20, 10, 0.8)"
|
||||
}
|
||||
|
||||
// tableColor is used for the <thead>, bodyBackground for the <tbody>
|
||||
const minterColor = 'rgb(98, 122, 167)'
|
||||
const adminColor = 'rgb(44, 209, 151)'
|
||||
const userColor = 'rgb(102, 102, 102)'
|
||||
return `
|
||||
<table style="
|
||||
width: 100%;
|
||||
border-style: dotted;
|
||||
border-width: 0.15rem;
|
||||
border-color: #576b6f;
|
||||
margin-bottom: 1em;
|
||||
border-collapse: collapse;
|
||||
">
|
||||
<thead style="background: ${tableColor}; color:rgb(238, 238, 238) ;">
|
||||
<tr style="font-size: 1.5rem;">
|
||||
<th style="padding: 0.1rem; text-align: center;">Voter Name/Address</th>
|
||||
<th style="padding: 0.1rem; text-align: center;">Voter Type</th>
|
||||
<th style="padding: 0.1rem; text-align: center;">Voter Weight(=BlocksMinted)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<!-- Tbody with extremely dark green or red -->
|
||||
<tbody style="background-color: ${bodyBackground}; color: #c6c6c6;">
|
||||
${voters
|
||||
.map(v => {
|
||||
const userType = v.isAdmin ? "Admin" : v.isMinter ? "Minter" : "User"
|
||||
const pollName = v.pollName
|
||||
const displayName =
|
||||
v.voterName
|
||||
? v.voterName
|
||||
: v.voterAddress
|
||||
return `
|
||||
<tr style="font-size: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; font-weight: bold;">
|
||||
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
|
||||
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${displayName}</td>
|
||||
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
|
||||
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${userType}</td>
|
||||
<td style="padding: 1.2rem; border-width: 0.1rem; border-style: dotted; border-color: lightgrey; text-align: center;
|
||||
color:${userType === 'Admin' ? adminColor : v.isMinter? minterColor : userColor };">${v.blocksMinted}</td>
|
||||
</tr>
|
||||
`
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
const featureTriggerCheck = async () => {
|
||||
const latestBlockInfo = await getLatestBlockInfo()
|
||||
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
|
||||
if (isBlockPassed) {
|
||||
console.warn(`featureTrigger check (verifyFeatureTrigger) determined block has PASSED:`, isBlockPassed)
|
||||
featureTriggerPassed = true
|
||||
return true
|
||||
} else {
|
||||
console.warn(`featureTrigger check (verifyFeatureTrigger) determined block has NOT PASSED:`, isBlockPassed)
|
||||
featureTriggerPassed = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
13
index.html
13
index.html
@ -32,11 +32,6 @@
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// Variable-based versioning (credit: QuickMythril)
|
||||
; // Update here in the future
|
||||
</script>
|
||||
|
||||
<section data-bs-version="5.1" class="menu menu1 boldm5 cid-ttRnktJ11Q" once="menu" id="menu1-0">
|
||||
|
||||
<nav class="navbar navbar-dropdown navbar-expand-lg">
|
||||
@ -141,7 +136,7 @@
|
||||
<div class="row">
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper" style="justify-content:center;">
|
||||
<div class="card-wrapper" style="justify-content:center; align: center; align-text: center;">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-file" style="color:aliceblue;"></span>
|
||||
</div>
|
||||
@ -200,14 +195,14 @@
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-7 card">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-2 version">
|
||||
</h2>
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-2">
|
||||
v1.06.1beta Feb 1 2025</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-5 card">
|
||||
<div class="text-wrapper">
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
<!-- <b class="version"><u>v1.06.4b</u></b>- <b>various improvements</b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for RELEASE NOTES. -->
|
||||
<b><u>v1.06.1b Counter</u></b>- <b>NEW COUNTER </b> - See post in the <a href="MINTERSHIP-FORUM">FORUM</a> for RELEASE NOTES, This is a simple update focused on adding a 'counter' to the MinterBoard. The same functionality will be added to the other boards in the future. But this should assist with people realizing how many cards they are 'missing'. Also added 'automatic refresh' when selecting time/date range on ARBoard(MAM) and MinterBoard when selecting ShowExistingMinters.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user