Added AdminBoard with all bugFixes
This commit is contained in:
parent
6e7c2c497a
commit
c36193633d
756
assets/js/AdminBoard.js
Normal file
756
assets/js/AdminBoard.js
Normal file
@ -0,0 +1,756 @@
|
|||||||
|
// NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
|
||||||
|
const isEncryptedTestMode = true;
|
||||||
|
const encryptedCardIdentifierPrefix = "test-MDC";
|
||||||
|
let isExistingEncryptedCard = false;
|
||||||
|
let existingDecryptedCardData = {};
|
||||||
|
let existingEncryptedCardIdentifier = {};
|
||||||
|
let cardMinterName = {}
|
||||||
|
let existingCardMinterNames = []
|
||||||
|
|
||||||
|
console.log("Attempting to load AdminBoard.js");
|
||||||
|
|
||||||
|
const loadAdminBoardPage = async () => {
|
||||||
|
// Clear existing content on the 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the "Minter Board" content
|
||||||
|
const mainContent = document.createElement("div");
|
||||||
|
mainContent.innerHTML = `
|
||||||
|
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
||||||
|
<h1 style="color: lightblue;">AdminBoard</h1>
|
||||||
|
<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>
|
||||||
|
<div id="encrypted-cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
||||||
|
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||||
|
<form id="publish-card-form">
|
||||||
|
<h3>Create or Update Your Minter Card</h3>
|
||||||
|
<label for="minter-name-input">Minter Name:</label>
|
||||||
|
<input type="text" id="minter-name-input" maxlength="100" placeholder="Enter Minter's Name" required>
|
||||||
|
<label for="card-header">Header:</label>
|
||||||
|
<input type="text" id="card-header" maxlength="100" placeholder="Explain main point/issue" required>
|
||||||
|
<label for="card-content">Content:</label>
|
||||||
|
<textarea id="card-content" placeholder="Enter any information you like...You may also attach links to more in-depth information, etc." 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>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(mainContent);
|
||||||
|
const publishCardButton = document.getElementById("publish-card-button")
|
||||||
|
if (publishCardButton) {
|
||||||
|
publishCardButton.addEventListener("click", async () => {
|
||||||
|
const publishCardView = document.getElementById("publish-card-view")
|
||||||
|
publishCardView.style.display = "flex"
|
||||||
|
document.getElementById("encrypted-cards-container").style.display = "none"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const refreshCardsButton = document.getElementById("refresh-cards-button")
|
||||||
|
if (refreshCardsButton) {
|
||||||
|
refreshCardsButton.addEventListener("click", async () => {
|
||||||
|
const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
|
||||||
|
encryptedCardsContainer.innerHTML = "<p>Refreshing cards...</p>"
|
||||||
|
await fetchAllEncryptedCards()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelPublishButton = document.getElementById("cancel-publish-button")
|
||||||
|
if (cancelPublishButton) {
|
||||||
|
cancelPublishButton.addEventListener("click", async () => {
|
||||||
|
const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
|
||||||
|
encryptedCardsContainer.style.display = "flex"; // Restore visibility
|
||||||
|
const publishCardView = document.getElementById("publish-card-view")
|
||||||
|
publishCardView.style.display = "none"; // Hide the publish form
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const addLinkButton = document.getElementById("add-link-button")
|
||||||
|
if (addLinkButton) {
|
||||||
|
addLinkButton.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 publishEncryptedCard();
|
||||||
|
});
|
||||||
|
|
||||||
|
await createCardMinterNameList();
|
||||||
|
await fetchAllEncryptedCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractCardsMinterName = (cardIdentifier) => {
|
||||||
|
// Ensure the identifier starts with the prefix
|
||||||
|
if (!cardIdentifier.startsWith(`${encryptedCardIdentifierPrefix}-`)) {
|
||||||
|
throw new Error('Invalid identifier format or prefix mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the identifier into parts
|
||||||
|
const parts = cardIdentifier.split('-');
|
||||||
|
|
||||||
|
// Ensure the format has at least 3 parts
|
||||||
|
if (parts.length < 3) {
|
||||||
|
throw new Error('Invalid identifier format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract minterName (everything from the second part to the second-to-last part)
|
||||||
|
const minterName = parts.slice(2, -1).join('-');
|
||||||
|
|
||||||
|
// Return the extracted minterName
|
||||||
|
return minterName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const createCardMinterNameList = async () => {
|
||||||
|
|
||||||
|
const response = await qortalRequest({
|
||||||
|
action: "SEARCH_QDN_RESOURCES",
|
||||||
|
service: "MAIL_PRIVATE",
|
||||||
|
query: `${encryptedCardIdentifierPrefix}`,
|
||||||
|
mode: "ALL",
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedEncryptedCards = await Promise.all(
|
||||||
|
response.map(async card => {
|
||||||
|
const isValid = await validateEncryptedCardIdentifier(card);
|
||||||
|
return isValid ? card : null;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null);
|
||||||
|
|
||||||
|
if (validEncryptedCards.length === 0) {
|
||||||
|
console.log(`no matches found, not adding any names to name list.`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const result of validEncryptedCards) {
|
||||||
|
const minterName = await extractCardsMinterName(result.identifier)
|
||||||
|
|
||||||
|
if (!existingCardMinterNames.includes(minterName)) {
|
||||||
|
existingCardMinterNames.push(minterName)
|
||||||
|
console.log(`cardsMinterName: ${minterName} - added to list`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Main function to load the Minter Cards ----------------------------------------
|
||||||
|
const fetchAllEncryptedCards = async () => {
|
||||||
|
const encryptedCardsContainer = document.getElementById("encrypted-cards-container");
|
||||||
|
encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await qortalRequest({
|
||||||
|
action: "SEARCH_QDN_RESOURCES",
|
||||||
|
service: "MAIL_PRIVATE",
|
||||||
|
query: encryptedCardIdentifierPrefix,
|
||||||
|
mode: "ALL"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !Array.isArray(response) || response.length === 0) {
|
||||||
|
encryptedCardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate cards and filter
|
||||||
|
const validatedEncryptedCards = await Promise.all(
|
||||||
|
response.map(async card => {
|
||||||
|
const isValid = await validateEncryptedCardIdentifier(card);
|
||||||
|
return isValid ? card : null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null);
|
||||||
|
|
||||||
|
if (validEncryptedCards.length === 0) {
|
||||||
|
encryptedCardsContainer.innerHTML = "<p>No valid cards found.</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by identifier and keep only the newest card for each identifier
|
||||||
|
const latestCardsMap = new Map();
|
||||||
|
|
||||||
|
validEncryptedCards.forEach(card => {
|
||||||
|
const timestamp = card.updated || card.created || 0;
|
||||||
|
const existingCard = latestCardsMap.get(card.identifier);
|
||||||
|
|
||||||
|
if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
|
||||||
|
latestCardsMap.set(card.identifier, card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract unique cards and sort by timestamp descending
|
||||||
|
const uniqueValidCards = Array.from(latestCardsMap.values()).sort((a, b) => {
|
||||||
|
const timestampA = a.updated || a.created || 0;
|
||||||
|
const timestampB = b.updated || b.created || 0;
|
||||||
|
return timestampB - timestampA;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Display skeleton cards immediately
|
||||||
|
encryptedCardsContainer.innerHTML = "";
|
||||||
|
uniqueValidCards.forEach(card => {
|
||||||
|
const skeletonHTML = createSkeletonCardHTML(card.identifier);
|
||||||
|
encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch and update each card
|
||||||
|
uniqueValidCards.forEach(async card => {
|
||||||
|
try {
|
||||||
|
const cardDataResponse = await qortalRequest({
|
||||||
|
action: "FETCH_QDN_RESOURCE",
|
||||||
|
name: card.name,
|
||||||
|
service: "MAIL_PRIVATE",
|
||||||
|
identifier: card.identifier,
|
||||||
|
encoding: "base64"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cardDataResponse) {
|
||||||
|
console.warn(`Skipping invalid card: ${JSON.stringify(card)}`);
|
||||||
|
removeSkeleton(card.identifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedCardData = await decryptAndParseObject(cardDataResponse);
|
||||||
|
|
||||||
|
// Skip cards without polls
|
||||||
|
if (!decryptedCardData.poll) {
|
||||||
|
console.warn(`Skipping card with no poll: ${card.identifier}`);
|
||||||
|
removeSkeleton(card.identifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch poll results
|
||||||
|
const pollResults = await fetchPollResults(decryptedCardData.poll);
|
||||||
|
const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
|
||||||
|
|
||||||
|
// Generate final card HTML
|
||||||
|
const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier);
|
||||||
|
replaceEncryptedSkeleton(card.identifier, finalCardHTML);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing card ${card.identifier}:`, error);
|
||||||
|
removeEncryptedSkeleton(card.identifier); // Silently remove skeleton on error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading cards:", error);
|
||||||
|
encryptedCardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const removeEncryptedSkeleton = (cardIdentifier) => {
|
||||||
|
const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
|
||||||
|
if (encryptedSkeletonCard) {
|
||||||
|
encryptedSkeletonCard.remove(); // Remove the skeleton silently
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceEncryptedSkeleton = (cardIdentifier, htmlContent) => {
|
||||||
|
const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
|
||||||
|
if (encryptedSkeletonCard) {
|
||||||
|
encryptedSkeletonCard.outerHTML = htmlContent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to create a skeleton card
|
||||||
|
const createEncryptedSkeletonCardHTML = (cardIdentifier) => {
|
||||||
|
return `
|
||||||
|
<div id="skeleton-${cardIdentifier}" class="skeleton-card" style="padding: 10px; border: 1px solid gray; margin: 10px 0;">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<div style="width: 50px; height: 50px; background-color: #ccc; border-radius: 50%;"></div>
|
||||||
|
<div style="margin-left: 10px;">
|
||||||
|
<div style="width: 120px; height: 20px; background-color: #ccc; margin-bottom: 5px;"></div>
|
||||||
|
<div style="width: 80px; height: 15px; background-color: #ddd;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<div style="width: 100%; height: 40px; background-color: #eee;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
|
||||||
|
const fetchExistingEncryptedCard = async (minterName) => {
|
||||||
|
try {
|
||||||
|
// Step 1: Perform the search
|
||||||
|
const response = await qortalRequest({
|
||||||
|
action: "SEARCH_QDN_RESOURCES",
|
||||||
|
service: "MAIL_PRIVATE",
|
||||||
|
identifier: encryptedCardIdentifierPrefix,
|
||||||
|
query: minterName,
|
||||||
|
mode: "ALL",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`);
|
||||||
|
|
||||||
|
// Step 2: Check if the response is an array and not empty
|
||||||
|
if (!response || !Array.isArray(response) || response.length === 0) {
|
||||||
|
console.log("No cards found for the current user.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Validate cards asynchronously
|
||||||
|
const validatedCards = await Promise.all(
|
||||||
|
response.map(async card => {
|
||||||
|
const isValid = await validateEncryptedCardIdentifier(card)
|
||||||
|
return isValid ? card : null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 4: Filter out invalid cards
|
||||||
|
const validCards = validatedCards.filter(card => card !== null);
|
||||||
|
|
||||||
|
if (validCards.length > 0) {
|
||||||
|
// Step 5: Sort by most recent timestamp
|
||||||
|
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0];
|
||||||
|
|
||||||
|
// Step 6: Fetch full card data
|
||||||
|
const cardDataResponse = await qortalRequest({
|
||||||
|
action: "FETCH_QDN_RESOURCE",
|
||||||
|
name: mostRecentCard.name,
|
||||||
|
service: mostRecentCard.service,
|
||||||
|
identifier: mostRecentCard.identifier,
|
||||||
|
encoding: "base64"
|
||||||
|
});
|
||||||
|
|
||||||
|
existingEncryptedCardIdentifier = mostRecentCard.identifier;
|
||||||
|
|
||||||
|
existingDecryptedCardData = await decryptAndParseObject(cardDataResponse)
|
||||||
|
console.log("Full card data fetched successfully:", existingDecryptedCardData);
|
||||||
|
|
||||||
|
return existingDecryptedCardData;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("No valid cards found.");
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching existing card:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate that a card is indeed a card and not a comment. -------------------------------------
|
||||||
|
const validateEncryptedCardIdentifier = async (card) => {
|
||||||
|
return (
|
||||||
|
typeof card === "object" &&
|
||||||
|
card.name &&
|
||||||
|
card.service === "MAIL_PRIVATE" &&
|
||||||
|
card.identifier && !card.identifier.includes("comment") &&
|
||||||
|
card.created
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing card data passed, into the form for editing -------------------------------------
|
||||||
|
const loadEncryptedCardIntoForm = async () => {
|
||||||
|
if (existingDecryptedCardData) {
|
||||||
|
console.log("Loading existing card data:", existingDecryptedCardData);
|
||||||
|
document.getElementById("minter-name-input").value = existingDecryptedCardData.minterName
|
||||||
|
document.getElementById("card-header").value = existingDecryptedCardData.header
|
||||||
|
document.getElementById("card-content").value = existingDecryptedCardData.content
|
||||||
|
|
||||||
|
const linksContainer = document.getElementById("links-container");
|
||||||
|
linksContainer.innerHTML = ""; // Clear previous links
|
||||||
|
existingDecryptedCardData.links.forEach(link => {
|
||||||
|
const linkInput = document.createElement("input");
|
||||||
|
linkInput.type = "text";
|
||||||
|
linkInput.className = "card-link";
|
||||||
|
linkInput.value = link;
|
||||||
|
linksContainer.appendChild(linkInput);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateMinterName = async(minterName) => {
|
||||||
|
try {
|
||||||
|
const nameInfo = await getNameInfo(minterName)
|
||||||
|
if (!nameInfo) {
|
||||||
|
return error (`No NameInfo able to be obtained? Did you pass name?`)
|
||||||
|
}
|
||||||
|
const name = nameInfo.name
|
||||||
|
return name
|
||||||
|
} catch (error){
|
||||||
|
console.error(`extracting name from name info: ${minterName} failed.`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function to publish a new Minter Card -----------------------------------------------
|
||||||
|
const publishEncryptedCard = async () => {
|
||||||
|
const minterNameInput = document.getElementById("minter-name-input").value.trim();
|
||||||
|
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://"));
|
||||||
|
const publishedMinterName = await validateMinterName(minterNameInput)
|
||||||
|
|
||||||
|
if (!header || !content) {
|
||||||
|
alert("Header and Content are required!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!publishedMinterName) {
|
||||||
|
alert(`Minter name invalid! Name input: ${minterNameInput} - please check the name and try again!`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isExistingEncryptedCard) {
|
||||||
|
if (existingCardMinterNames.includes(publishedMinterName)) {
|
||||||
|
const updateCard = confirm(`Minter Name: ${publishedMinterName} - CARD ALREADY EXISTS, you can update it (overwriting existing publish) or cancel... `)
|
||||||
|
if (updateCard) {
|
||||||
|
await fetchExistingEncryptedCard(publishedMinterName)
|
||||||
|
await loadEncryptedCardIntoForm()
|
||||||
|
isExistingEncryptedCard = true
|
||||||
|
return
|
||||||
|
}else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardIdentifier = isExistingEncryptedCard ? existingEncryptedCardIdentifier : `${encryptedCardIdentifierPrefix}-${publishedMinterName}-${await uid()}`;
|
||||||
|
const pollName = `${cardIdentifier}-poll`;
|
||||||
|
const pollDescription = `Admin Board Poll Published By ${userState.accountName}`;
|
||||||
|
|
||||||
|
const cardData = {
|
||||||
|
minterName: `${publishedMinterName}`,
|
||||||
|
header,
|
||||||
|
content,
|
||||||
|
links,
|
||||||
|
creator: userState.accountName,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
poll: pollName,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
let base64CardData = await objectToBase64(cardData);
|
||||||
|
if (!base64CardData) {
|
||||||
|
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||||
|
base64CardData = btoa(JSON.stringify(cardData));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
||||||
|
adminPublicKeys = verifiedAdminPublicKeys
|
||||||
|
|
||||||
|
await qortalRequest({
|
||||||
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
|
name: userState.accountName,
|
||||||
|
service: "MAIL_PRIVATE",
|
||||||
|
identifier: cardIdentifier,
|
||||||
|
data64: base64CardData,
|
||||||
|
encrypt: true,
|
||||||
|
publicKeys: verifiedAdminPublicKeys
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isExistingEncryptedCard){
|
||||||
|
await qortalRequest({
|
||||||
|
action: "CREATE_POLL",
|
||||||
|
pollName,
|
||||||
|
pollDescription,
|
||||||
|
pollOptions: ['Yes, No'],
|
||||||
|
pollOwnerAddress: userState.accountAddress,
|
||||||
|
});
|
||||||
|
|
||||||
|
alert("Card and poll published successfully!");
|
||||||
|
existingCardMinterNames.push(`${publishedMinterName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExistingEncryptedCard){
|
||||||
|
alert("Card Updated Successfully! (No poll updates are possible at this time...)")
|
||||||
|
}
|
||||||
|
document.getElementById("publish-card-form").reset();
|
||||||
|
document.getElementById("publish-card-view").style.display = "none";
|
||||||
|
document.getElementById("encrypted-cards-container").style.display = "flex";
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error publishing card or poll:", error);
|
||||||
|
alert("Failed to publish card and poll.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post a comment on a card. ---------------------------------
|
||||||
|
const postEncryptedComment = async (cardIdentifier) => {
|
||||||
|
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
|
||||||
|
const commentText = commentInput.value.trim();
|
||||||
|
if (!commentText) {
|
||||||
|
alert('Comment cannot be empty!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postTimestamp = `${Date.now()}`
|
||||||
|
console.log(`timestmp to be posted: ${postTimestamp}`)
|
||||||
|
|
||||||
|
const commentData = {
|
||||||
|
content: commentText,
|
||||||
|
creator: userState.accountName,
|
||||||
|
timestamp: postTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
|
||||||
|
|
||||||
|
if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0)) {
|
||||||
|
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
||||||
|
adminPublicKeys = verifiedAdminPublicKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base64CommentData = await objectToBase64(commentData);
|
||||||
|
if (!base64CommentData) {
|
||||||
|
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||||
|
base64CommentData = btoa(JSON.stringify(commentData));
|
||||||
|
}
|
||||||
|
|
||||||
|
await qortalRequest({
|
||||||
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
|
name: userState.accountName,
|
||||||
|
service: "MAIL_PRIVATE",
|
||||||
|
identifier: commentIdentifier,
|
||||||
|
data64: base64CommentData,
|
||||||
|
encrypt: true,
|
||||||
|
publicKeys: adminPublicKeys
|
||||||
|
});
|
||||||
|
|
||||||
|
alert('Comment posted successfully!');
|
||||||
|
commentInput.value = ''; // Clear input
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error posting comment:', error);
|
||||||
|
alert('Failed to post comment.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Fetch the comments for a card with passed card identifier ----------------------------
|
||||||
|
const fetchEncryptedComments = async (cardIdentifier) => {
|
||||||
|
try {
|
||||||
|
const response = await qortalRequest({
|
||||||
|
action: 'SEARCH_QDN_RESOURCES',
|
||||||
|
service: 'MAIL_PRIVATE',
|
||||||
|
query: `comment-${cardIdentifier}`,
|
||||||
|
mode: "ALL"
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// display the comments on the card, with passed cardIdentifier to identify the card --------------
|
||||||
|
const displayEncryptedComments = async (cardIdentifier) => {
|
||||||
|
try {
|
||||||
|
const comments = await fetchEncryptedComments(cardIdentifier);
|
||||||
|
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
|
||||||
|
|
||||||
|
// Fetch and display each comment
|
||||||
|
for (const comment of comments) {
|
||||||
|
const commentDataResponse = await qortalRequest({
|
||||||
|
action: "FETCH_QDN_RESOURCE",
|
||||||
|
name: comment.name,
|
||||||
|
service: "MAIL_PRIVATE",
|
||||||
|
identifier: comment.identifier,
|
||||||
|
encoding: "base64"
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCommentData = await decryptAndParseObject(commentDataResponse)
|
||||||
|
|
||||||
|
const timestamp = await timestampToHumanReadableDate(decryptedCommentData.timestamp);
|
||||||
|
|
||||||
|
//TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section.
|
||||||
|
const commentHTML = `
|
||||||
|
<div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;">
|
||||||
|
<p><strong><u>${decryptedCommentData.creator}</strong>:</p></u>
|
||||||
|
<p>${decryptedCommentData.content}</p>
|
||||||
|
<p><i>${timestamp}</p></i>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
commentsContainer.insertAdjacentHTML('beforeend', commentHTML);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error displaying comments for ${cardIdentifier}:`, error);
|
||||||
|
alert("Failed to load comments. Please try again.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateAdminBoardPollResults = async (pollData, minterGroupMembers, minterAdmins) => {
|
||||||
|
const memberAddresses = minterGroupMembers.map(member => member.member)
|
||||||
|
const minterAdminAddresses = minterAdmins.map(member => member.member)
|
||||||
|
const adminGroupsMembers = await fetchAllAdminGroupsMembers()
|
||||||
|
const groupAdminAddresses = adminGroupsMembers.map(member => member.member)
|
||||||
|
const adminAddresses = [];
|
||||||
|
adminAddresses.push(...minterAdminAddresses,...groupAdminAddresses);
|
||||||
|
|
||||||
|
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, yesWeight = 0 , noWeight = 0
|
||||||
|
|
||||||
|
pollData.voteWeights.forEach(weightData => {
|
||||||
|
if (weightData.optionName === 'Yes') {
|
||||||
|
yesWeight = weightData.voteWeight
|
||||||
|
} else if (weightData.optionName === 'No') {
|
||||||
|
noWeight = weightData.voteWeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const vote of pollData.votes) {
|
||||||
|
const voterAddress = await getAddressFromPublicKey(vote.voterPublicKey)
|
||||||
|
console.log(`voter address: ${voterAddress}`)
|
||||||
|
|
||||||
|
if (vote.optionIndex === 0) {
|
||||||
|
adminAddresses.includes(voterAddress) ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`)
|
||||||
|
} else if (vote.optionIndex === 1) {
|
||||||
|
adminAddresses.includes(voterAddress) ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - create a new function to calculate the weights of each voting MINTER only.
|
||||||
|
// This will give ALL weight whether voter is in minter group or not...
|
||||||
|
// until that is changed on the core we must calculate manually.
|
||||||
|
const totalYesWeight = yesWeight
|
||||||
|
const totalNoWeight = noWeight
|
||||||
|
|
||||||
|
const totalYes = adminYes + minterYes
|
||||||
|
const totalNo = adminNo + minterNo
|
||||||
|
|
||||||
|
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo, totalYesWeight, totalNoWeight }
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleEncryptedComments = async (cardIdentifier) => {
|
||||||
|
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
|
||||||
|
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
|
||||||
|
await displayEncryptedComments(cardIdentifier);
|
||||||
|
commentsSection.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
commentsSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createLinkDisplayModal = async () => {
|
||||||
|
const modalHTML = `
|
||||||
|
<div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;">
|
||||||
|
<div style="position: relative; margin: 10% auto; width: 95%; height: 80%; background: white; border-radius: 10px; overflow: hidden;">
|
||||||
|
<iframe id="modalContent" src="" style="width: 100%; height: 100%; border: none;"></iframe>
|
||||||
|
<button onclick="closeLinkDisplayModal()" style="position: absolute; top: 10px; right: 10px; background: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to open the modal
|
||||||
|
const openLinkDisplayModal = async (link) => {
|
||||||
|
const processedLink = await processQortalLinkForRendering(link) // Process the link to replace `qortal://` for rendering in modal
|
||||||
|
const modal = document.getElementById('modal');
|
||||||
|
const modalContent = document.getElementById('modalContent');
|
||||||
|
modalContent.src = processedLink; // Set the iframe source to the link
|
||||||
|
modal.style.display = 'block'; // Show the modal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to close the modal
|
||||||
|
const closeLinkDisplayModal = async () => {
|
||||||
|
const modal = document.getElementById('modal');
|
||||||
|
const modalContent = document.getElementById('modalContent');
|
||||||
|
modal.style.display = 'none'; // Hide the modal
|
||||||
|
modalContent.src = ''; // Clear the iframe source
|
||||||
|
}
|
||||||
|
|
||||||
|
const processQortalLinkForRendering = async (link) => {
|
||||||
|
if (link.startsWith('qortal://')) {
|
||||||
|
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
|
||||||
|
if (match) {
|
||||||
|
const firstParam = match[1].toUpperCase(); // Convert to uppercase
|
||||||
|
const remainingPath = match[2] || ""; // Rest of the URL
|
||||||
|
// Perform any asynchronous operation if necessary
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation
|
||||||
|
return `/render/${firstParam}${remainingPath}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return link; // Return unchanged if not a Qortal link
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the overall Minter Card HTML -----------------------------------------------
|
||||||
|
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier) => {
|
||||||
|
const { minterName, header, content, links, creator, timestamp, poll } = cardData;
|
||||||
|
const formattedDate = new Date(timestamp).toLocaleString();
|
||||||
|
const minterAvatar = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`;
|
||||||
|
const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
|
||||||
|
const linksHTML = links.map((link, index) => `
|
||||||
|
<button onclick="openLinkDisplayModal('${link}')">
|
||||||
|
${`Link ${index + 1} - ${link}`}
|
||||||
|
</button>
|
||||||
|
`).join("");
|
||||||
|
|
||||||
|
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 } = await calculateAdminBoardPollResults(pollResults, minterGroupMembers, minterAdmins)
|
||||||
|
await createModal()
|
||||||
|
return `
|
||||||
|
<div class="admin-card">
|
||||||
|
<div class="minter-card-header">
|
||||||
|
<h2 class="support-header"> Posted By:</h2>
|
||||||
|
<img src="${creatorAvatar}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">
|
||||||
|
<h2>${creator}</h2>
|
||||||
|
<div class="support-header"><h5> Regarding Minter: </h5>
|
||||||
|
<img src="${minterAvatar}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">
|
||||||
|
<h3>${minterName}</h3>
|
||||||
|
<p>${header}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
<div class="support-header"><h5>Informational Links:</h5></div>
|
||||||
|
<div class="info-links">
|
||||||
|
${linksHTML}
|
||||||
|
</div>
|
||||||
|
<div class="results-header support-header"><h5>Resulting Support:</h5></div>
|
||||||
|
<div class="minter-card-results">
|
||||||
|
<div class="admin-results">
|
||||||
|
<span class="admin-yes">Admin Yes: ${adminYes}</span>
|
||||||
|
<span class="admin-no">Admin No: ${adminNo}</span>
|
||||||
|
</div>
|
||||||
|
<div class="minter-results">
|
||||||
|
<span class="minter-yes">TBD ${minterYes}</span>
|
||||||
|
<span class="minter-no">TBD ${minterNo}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-results">
|
||||||
|
<span class="total-yes">Total Yes: ${totalYes}</span>
|
||||||
|
<span class="total-no">Total No: ${totalNo}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="support-header"><h5>Support ${minterName}?</h5></div>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="actions-buttons">
|
||||||
|
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
|
||||||
|
<button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS</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="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||||
|
<button onclick="postEncryptedComment('${cardIdentifier}')">Post Comment</button>
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user