Refreshing cards...
";
+ await loadCards();
+ });
+
+
document.getElementById("cancel-publish-button").addEventListener("click", async () => {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.style.display = "flex"; // Restore visibility
@@ -133,16 +147,14 @@ const loadCards = async () => {
return;
}
- // Validate cards
+ // Validate cards and filter
const validatedCards = await Promise.all(
response.map(async card => {
- console.log("Validating card:", card);
const isValid = await validateCardStructure(card);
return isValid ? card : null;
})
);
- // Filter valid cards
const validCards = validatedCards.filter(card => card !== null);
if (validCards.length === 0) {
@@ -150,60 +162,92 @@ const loadCards = async () => {
return;
}
- // Sort valid cards by timestamp (oldest to newest)
+ // Sort cards by timestamp descending (newest first)
validCards.sort((a, b) => {
- const timestampA = a.updated || a.created || 0; // Use updated or created timestamp
+ const timestampA = a.updated || a.created || 0;
const timestampB = b.updated || b.created || 0;
- return timestampA - timestampB;
+ return timestampB - timestampA;
});
- // Reverse the sorted order (newest first)
- validCards.reverse();
-
- // Clear the container before adding cards
+ // Display skeleton cards immediately
cardsContainer.innerHTML = "";
+ validCards.forEach(card => {
+ const skeletonHTML = createSkeletonCardHTML(card.identifier);
+ cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
+ });
- // Process and display each card
- await Promise.all(
- validCards.map(async card => {
- try {
- const cardDataResponse = await qortalRequest({
- action: "FETCH_QDN_RESOURCE",
- name: card.name,
- service: "BLOG_POST",
- identifier: card.identifier,
- });
-
- const cardData = cardDataResponse;
- if (!cardData || !cardData.poll) {
- console.warn(`Skipping card with missing poll data: ${JSON.stringify(cardData)}`);
- return;
- }
-
- // Fetch poll results and check admin votes
- const pollResults = await fetchPollResults(cardData.poll);
- console.log(`Poll Results Fetched - totalVotes: ${pollResults.totalVotes}`);
-
- // Check if more than 3 admins voted "No"
- if (pollResults.adminNo > 3) {
- console.log(`Card ${card.identifier} hidden due to more than 3 admin downvotes.`);
- return; // Skip this card
- }
-
- const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
- cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
- } catch (error) {
- console.error(`Error processing card ${card.identifier}:`, error);
+ // Fetch and update each card
+ validCards.forEach(async card => {
+ try {
+ const cardDataResponse = await qortalRequest({
+ action: "FETCH_QDN_RESOURCE",
+ name: card.name,
+ service: "BLOG_POST",
+ identifier: card.identifier,
+ });
+
+ if (!cardDataResponse) {
+ console.warn(`Skipping invalid card: ${JSON.stringify(card)}`);
+ removeSkeleton(card.identifier);
+ return;
}
- })
- );
+
+ // Skip cards without polls
+ if (!cardDataResponse.poll) {
+ console.warn(`Skipping card with no poll: ${card.identifier}`);
+ removeSkeleton(card.identifier);
+ return;
+ }
+
+ // Fetch poll results
+ const pollResults = await fetchPollResults(cardDataResponse.poll);
+
+ // Generate final card HTML
+ const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier);
+ replaceSkeleton(card.identifier, finalCardHTML);
+ } catch (error) {
+ console.error(`Error processing card ${card.identifier}:`, error);
+ removeSkeleton(card.identifier); // Silently remove skeleton on error
+ }
+ });
+
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "Failed to load cards.
";
}
};
+const removeSkeleton = (cardIdentifier) => {
+ const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
+ if (skeletonCard) {
+ skeletonCard.remove(); // Remove the skeleton silently
+ }
+};
+const replaceSkeleton = (cardIdentifier, htmlContent) => {
+ const skeletonCard = document.getElementById(`skeleton-${cardIdentifier}`);
+ if (skeletonCard) {
+ skeletonCard.outerHTML = htmlContent;
+ }
+};
+
+// Function to create a skeleton card
+const createSkeletonCardHTML = (cardIdentifier) => {
+ return `
+
+
Minter Board
+
The Minter Data 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.
+
Publish Encrypted Card
+
Refresh Cards
+
+
+
+ `;
+ document.body.appendChild(mainContent);
+
+ document.getElementById("publish-card-button").addEventListener("click", async () => {
+ try {
+ const fetchedCard = await checkDuplicateEncryptedCard();
+ if (fetchedCard) {
+ // An existing card is found
+ if (testMode) {
+ // In test mode, ask user what to do
+ const updateCard = confirm("A card already exists. Do you want to update it? Note, updating it will overwrite the data of the original publisher, only do this if necessary!");
+ if (updateCard) {
+ isExistingCard = true;
+ await loadEncryptedCardIntoForm(existingCardData);
+ alert("Edit the existing ");
+ } else {
+ alert("Test mode: You can now create a new card.");
+ isExistingCard = false;
+ existingCardData = {}; // Reset
+ document.getElementById("publish-card-form").reset();
+ }
+ } else {
+ // Not in test mode, force editing
+ alert("A card already exists. Publishing of multiple cards for the same minter is not allowed, either use existing card or update it.");
+ isExistingCard = true;
+ await loadEncryptedCardIntoForm(existingCardData);
+ }
+ } else {
+ // No existing card found
+ alert("No existing card found. Create a new card.");
+ isExistingCard = false;
+ }
+
+ // Show the form
+ const publishCardView = document.getElementById("publish-card-view");
+ publishCardView.style.display = "flex";
+ document.getElementById("cards-container").style.display = "none";
+ } catch (error) {
+ console.error("Error checking for existing card:", error);
+ alert("Failed to check for existing card. Please try again.");
+ }
+ });
+
+ const checkDuplicateEncryptedCard = async (minterName) => {
+ const response = await qortalRequest({
+ action: "SEARCH_QDN_RESOURCES",
+ service: "MAIL_PRIVATE",
+ query: `${cardIdentifierPrefix}-${minterName}`,
+ mode: "ALL",
+ });
+ return response && response.length > 0;
+ };
+
+ document.getElementById("refresh-cards-button").addEventListener("click", async () => {
+ const cardsContainer = document.getElementById("cards-container");
+ cardsContainer.innerHTML = "Refreshing cards...
";
+ await fetchEncryptedCards();
+ });
+
+
+ document.getElementById("cancel-publish-button").addEventListener("click", async () => {
+ const cardsContainer = document.getElementById("cards-container");
+ cardsContainer.style.display = "flex"; // Restore visibility
+ const publishCardView = document.getElementById("publish-card-view");
+ publishCardView.style.display = "none"; // Hide the publish form
+ });
+
+ document.getElementById("add-link-button").addEventListener("click", async () => {
+ const linksContainer = document.getElementById("links-container");
+ const newLinkInput = document.createElement("input");
+ newLinkInput.type = "text";
+ newLinkInput.className = "card-link";
+ newLinkInput.placeholder = "Enter QDN link";
+ linksContainer.appendChild(newLinkInput);
+ });
+
+ document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
+ event.preventDefault();
+ await publishEncryptedCard();
+ });
+
+ await fetchEncryptedCards();
+}
+
+//Main function to load the Minter Cards ----------------------------------------
+const fetchEncryptedCards = async () => {
+ const cardsContainer = document.getElementById("cards-container");
+ cardsContainer.innerHTML = "Loading cards...
";
+
+ try {
+ const response = await qortalRequest({
+ action: "SEARCH_QDN_RESOURCES",
+ service: "MAIL_PRIVATE",
+ query: cardIdentifierPrefix,
+ mode: "ALL"
+ });
+
+ if (!response || !Array.isArray(response) || response.length === 0) {
+ cardsContainer.innerHTML = "No cards found.
";
+ return;
+ }
+
+ // Validate cards and filter
+ const validatedCards = await Promise.all(
+ response.map(async card => {
+ const isValid = await validateEncryptedCardStructure(card);
+ return isValid ? card : null;
+ })
+ );
+
+ const validCards = validatedCards.filter(card => card !== null);
+
+ if (validCards.length === 0) {
+ cardsContainer.innerHTML = "No valid cards found.
";
+ return;
+ }
+
+ // Sort cards by timestamp descending (newest first)
+ validCards.sort((a, b) => {
+ const timestampA = a.updated || a.created || 0;
+ const timestampB = b.updated || b.created || 0;
+ return timestampB - timestampA;
+ });
+
+ // Display skeleton cards immediately
+ cardsContainer.innerHTML = "";
+ validCards.forEach(card => {
+ const skeletonHTML = createSkeletonCardHTML(card.identifier);
+ cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
+ });
+
+ // Fetch and update each card
+ validCards.forEach(async card => {
+ try {
+ const cardDataResponse = await qortalRequest({
+ action: "FETCH_QDN_RESOURCE",
+ name: card.name,
+ service: "MAIL_PRIVATE",
+ identifier: card.identifier,
+ });
+
+ if (!cardDataResponse) {
+ console.warn(`Skipping invalid card: ${JSON.stringify(card)}`);
+ removeSkeleton(card.identifier);
+ return;
+ }
+
+ // Skip cards without polls
+ if (!cardDataResponse.poll) {
+ console.warn(`Skipping card with no poll: ${card.identifier}`);
+ removeSkeleton(card.identifier);
+ return;
+ }
+
+ // Fetch poll results
+ const pollResults = await fetchPollResults(cardDataResponse.poll);
+
+ // Generate final card HTML
+ const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier);
+ replaceSkeleton(card.identifier, finalCardHTML);
+ } catch (error) {
+ console.error(`Error processing card ${card.identifier}:`, error);
+ removeSkeleton(card.identifier); // Silently remove skeleton on error
+ }
+ });
+
+ } catch (error) {
+ console.error("Error loading cards:", error);
+ cardsContainer.innerHTML = "Failed to load cards.
";
+ }
+};
+
+
+// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
+const fetchExistingEncryptedCard = async () => {
+ try {
+ // Step 1: Perform the search
+ const response = await qortalRequest({
+ action: "SEARCH_QDN_RESOURCES",
+ service: "BLOG_POST",
+ identifier: cardIdentifierPrefix,
+ name: userState.accountName,
+ mode: "ALL",
+ exactMatchNames: true // Search for the exact userName only when finding existing cards
+ });
+
+ 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 validateEncryptedCardStructure(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: userState.accountName, // User's account name
+ service: "BLOG_POST",
+ identifier: mostRecentCard.identifier
+ });
+
+ existingCardIdentifier = mostRecentCard.identifier;
+ existingCardData = cardDataResponse;
+
+ console.log("Full card data fetched successfully:", cardDataResponse);
+
+ return cardDataResponse;
+ }
+
+ 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 validateEncryptedCardStructure = async (card) => {
+ return (
+ typeof card === "object" &&
+ card.name &&
+ card.service === "BLOG_POST" &&
+ card.identifier && !card.identifier.includes("comment") &&
+ card.created
+ );
+}
+
+// Load existing card data passed, into the form for editing -------------------------------------
+const loadEncryptedCardIntoForm = async (cardData) => {
+ console.log("Loading existing card data:", cardData);
+ document.getElementById("card-header").value = cardData.header;
+ document.getElementById("card-content").value = cardData.content;
+
+ const linksContainer = document.getElementById("links-container");
+ linksContainer.innerHTML = ""; // Clear previous links
+ cardData.links.forEach(link => {
+ const linkInput = document.createElement("input");
+ linkInput.type = "text";
+ linkInput.className = "card-link";
+ linkInput.value = link;
+ linksContainer.appendChild(linkInput);
+ });
+}
+
+// Main function to publish a new Minter Card -----------------------------------------------
+const publishEncryptedCard = async () => {
+ const header = document.getElementById("card-header").value.trim();
+ const content = document.getElementById("card-content").value.trim();
+ const links = Array.from(document.querySelectorAll(".card-link"))
+ .map(input => input.value.trim())
+ .filter(link => link.startsWith("qortal://"));
+
+ if (!header || !content) {
+ alert("Header and content are required!");
+ return;
+ }
+
+ const cardIdentifier = isExistingCard ? existingCardIdentifier : `${cardIdentifierPrefix}-${await uid()}`;
+ const pollName = `${cardIdentifier}-poll`;
+ const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
+
+ const cardData = {
+ 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));
+ }
+
+ await qortalRequest({
+ action: "PUBLISH_QDN_RESOURCE",
+ name: userState.accountName,
+ service: "BLOG_POST",
+ identifier: cardIdentifier,
+ data64: base64CardData,
+ });
+ if (!isExistingCard){
+ await qortalRequest({
+ action: "CREATE_POLL",
+ pollName,
+ pollDescription,
+ pollOptions: ['Yes, No'],
+ pollOwnerAddress: userState.accountAddress,
+ });
+
+ alert("Card and poll published successfully!");
+ }
+ if (isExistingCard){
+ alert("Card Updated Successfully! (No poll updates are possible at this time...)")
+ }
+ document.getElementById("publish-card-form").reset();
+ document.getElementById("publish-card-view").style.display = "none";
+ document.getElementById("cards-container").style.display = "flex";
+ await loadCards();
+ } 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 commentData = {
+ content: commentText,
+ creator: userState.accountName,
+ timestamp: Date.now(),
+ };
+
+ const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
+
+ 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: 'BLOG_POST',
+ identifier: commentIdentifier,
+ data64: base64CommentData,
+ });
+
+ alert('Comment posted successfully!');
+ commentInput.value = ''; // Clear input
+ // await displayComments(cardIdentifier); // Refresh comments - We don't need to do this as comments will be displayed only after confirmation.
+ } catch (error) {
+ console.error('Error posting comment:', error);
+ alert('Failed to post comment.');
+ }
+};
+
+//Fetch the comments for a card with passed card identifier ----------------------------
+const fetchCommentsForEncryptedCard = async (cardIdentifier) => {
+ try {
+ const response = await qortalRequest({
+ action: 'SEARCH_QDN_RESOURCES',
+ service: 'BLOG_POST',
+ 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 fetchCommentsForCard(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: "BLOG_POST",
+ identifier: comment.identifier,
+ });
+ const timestamp = await timestampToHumanReadableDate(commentDataResponse.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 = `
+
+ `;
+ commentsContainer.insertAdjacentHTML('beforeend', commentHTML);
+ }
+ } catch (error) {
+ console.error(`Error displaying comments for ${cardIdentifier}:`, error);
+ alert("Failed to load comments. Please try again.");
+ }
+};
+
+
+
+// Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled --------------------
+const toggleComments = async (cardIdentifier) => {
+ const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
+ if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
+ await displayComments(cardIdentifier);
+ commentsSection.style.display = 'block';
+ } else {
+ commentsSection.style.display = 'none';
+ }
+};
+
+const createModal = async () => {
+ const modalHTML = `
+
+
+
+
+ ${content}
+
+
+
+ ${linksHTML}
+
+
+
+
+ Admin Yes: ${adminYes}
+ Admin No: ${adminNo}
+
+
+ Minter Yes: ${minterYes}
+ Minter No: ${minterNo}
+
+
+ Total Yes: ${totalYes}
+ Total No: ${totalNo}
+
+
+
+
+
+
Published by: ${creator} on ${formattedDate}
+
+ `;
+}
+
diff --git a/assets/js/QortalApi.js b/assets/js/QortalApi.js
index d86f0ed..da933c5 100644
--- a/assets/js/QortalApi.js
+++ b/assets/js/QortalApi.js
@@ -793,6 +793,24 @@ const fetchPollResults = async (pollName) => {
}
};
+ // Vote YES on a poll ------------------------------
+const voteYesOnPoll = async (poll) => {
+ await qortalRequest({
+ action: "VOTE_ON_POLL",
+ pollName: poll,
+ optionIndex: 0,
+ });
+ }
+
+ // Vote NO on a poll -----------------------------
+ const voteNoOnPoll = async (poll) => {
+ await qortalRequest({
+ action: "VOTE_ON_POLL",
+ pollName: poll,
+ optionIndex: 1,
+ });
+ }
+
// export {
// userState,
// adminGroups,