// 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 = false const encryptedCardIdentifierPrefix = "card-MAC" let isExistingEncryptedCard = false let existingDecryptedCardData = {} let existingEncryptedCardIdentifier = {} let cardMinterName = {} let existingCardMinterNames = [] let isTopic = false let attemptLoadAdminDataCount = 0 let adminMemberCount = 0 let adminPublicKeys = [] 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 = `
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.
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'.
Refreshing cards...
" 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(); const isTopicChecked = document.getElementById("topic-checkbox").checked; // Pass that boolean to publishEncryptedCard await publishEncryptedCard(isTopicChecked); }); // await fetchAndValidateAllAdminCards(); await fetchAllEncryptedCards(); await updateOrSaveAdminGroupsDataLocally(); } // Example: fetch and save admin public keys and count const updateOrSaveAdminGroupsDataLocally = async () => { try { // Fetch the array of admin public keys const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys() // Build an object containing the count and the array const adminData = { keysCount: verifiedAdminPublicKeys.length, publicKeys: verifiedAdminPublicKeys }; adminPublicKeys = verifiedAdminPublicKeys // Stringify and save to localStorage localStorage.setItem('savedAdminData', JSON.stringify(adminData)) console.log('Admin public keys saved locally:', adminData) } catch (error) { console.error('Error fetching/storing admin public keys:', error) attemptLoadAdminDataCount++ } }; const loadOrFetchAdminGroupsData = async () => { try { // Pull the JSON from localStorage const storedData = localStorage.getItem('savedAdminData') if (!storedData && attemptLoadAdminDataCount <= 3) { console.log('No saved admin public keys found in local storage. Fetching...') await updateOrSaveAdminGroupsDataLocally() attemptLoadAdminDataCount++ return null; } // Parse the JSON, then store the global variables. const parsedData = JSON.parse(storedData) adminMemberCount = parsedData.keysCount adminPublicKeys = parsedData.publicKeys console.log(typeof adminPublicKeys); // Should be "object" console.log(Array.isArray(adminPublicKeys)) console.log(`Loaded admins 'keysCount'=${adminMemberCount}, publicKeys=`, adminPublicKeys) attemptLoadAdminDataCount = 0 return parsedData; // and return { adminMemberCount, adminKeys } to the caller } catch (error) { console.error('Error loading/parsing saved admin public keys:', error) return null } } 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'); } if (parts.slice(2, -1).join('-') === 'TOPIC') { console.log(`TOPIC found in identifier: ${cardIdentifier} - not including in duplicatesList`) return } // 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 processCards = async (validEncryptedCards) => { const latestCardsMap = new Map() // Step 1: Filter and keep the most recent card per identifier 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) } }) // Step 2: Extract unique cards const uniqueValidCards = Array.from(latestCardsMap.values()) // Step 3: Group by minterName and select the most recent card per minterName const minterNameMap = new Map() for (const card of validEncryptedCards) { const minterName = await extractCardsMinterName(card.identifier) const existingCard = minterNameMap.get(minterName) const cardTimestamp = card.updated || card.created || 0 const existingTimestamp = existingCard?.updated || existingCard?.created || 0 if (!existingCardMinterNames.includes(minterName)) { existingCardMinterNames.push(minterName) console.log(`cardsMinterName: ${minterName} - added to list`) } // Keep only the most recent card for each minterName if (!existingCard || cardTimestamp > existingTimestamp) { minterNameMap.set(minterName, card) } } // Step 4: Filter cards to ensure each minterName is included only once const finalCards = [] const seenMinterNames = new Set() for (const [minterName, card] of minterNameMap.entries()) { if (!seenMinterNames.has(minterName)) { finalCards.push(card) seenMinterNames.add(minterName) // Mark the minterName as seen } } // Step 5: Sort by the most recent timestamp finalCards.sort((a, b) => { const timestampA = a.updated || a.created || 0 const timestampB = b.updated || b.created || 0 return timestampB - timestampA }) return finalCards } //Main function to load the Minter Cards ---------------------------------------- const fetchAllEncryptedCards = async () => { const encryptedCardsContainer = document.getElementById("encrypted-cards-container"); encryptedCardsContainer.innerHTML = "Loading cards...
"; 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 = "No cards found.
"; 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 = "No valid cards found.
"; return; } const finalCards = await processCards(validEncryptedCards) // Display skeleton cards immediately encryptedCardsContainer.innerHTML = ""; finalCards.forEach(card => { const skeletonHTML = createSkeletonCardHTML(card.identifier); encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML); }); // Fetch and update each card finalCards.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); const encryptedCommentCount = await getEncryptedCommentCount(card.identifier); // Generate final card HTML const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount); 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 = "Failed to load cards.
"; } }; 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 `${header}
(click COMMENTS button to open/close card comments)
By: ${creator} - ${formattedDate}
${decryptedCommentData.creator}:
${decryptedCommentData.content}
${timestamp}