Q-Mintership-Alpha/assets/js/MinterBoard.js

1010 lines
36 KiB
JavaScript
Raw Normal View History

// // NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
const testMode = false;
const cardIdentifierPrefix = "Minter-board-card";
2024-12-11 14:40:32 -08:00
let isExistingCard = false;
let existingCardData = {};
let existingCardIdentifier = {};
2024-12-11 14:40:32 -08:00
const loadMinterBoardPage = async () => {
2024-12-11 14:40:32 -08:00
// 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");
const publishButtonColor = '#527c9d'
const minterBoardNameColor = '#527c9d'
2024-12-11 14:40:32 -08:00
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: ${minterBoardNameColor};">Minter Board</h1>
<p style="font-size: 1.25em;"> Publish a Minter Card with Information, and obtain and view the support of the community. Welcome to the Minter Board!</p>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px; background-color: ${publishButtonColor}">Publish Minter Card</button>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</button>
2024-12-11 14:40:32 -08:00
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<form id="publish-card-form">
<h3>Create or Update Your Minter Card</h3>
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter detailed information about why you would like to be a minter... the more the better, and links to things you have published on QDN will help a lot! Give the Minter Admins things to make decisions by!" required></textarea>
2024-12-11 14:40:32 -08:00
<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);
document.getElementById("publish-card-button").addEventListener("click", async () => {
try {
const fetchedCard = await fetchExistingCard();
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?");
if (updateCard) {
isExistingCard = true;
await loadCardIntoForm(existingCardData);
alert("Edit your existing card and publish.");
} else {
alert("Test mode: You can now create a new card.");
isExistingCard = false;
existingCardData = {}; // Reset
document.getElementById("publish-card-form").reset();
}
2024-12-11 14:40:32 -08:00
} else {
// Not in test mode, force editing
alert("A card already exists. Publishing of multiple cards is not allowed. Please update your card.");
isExistingCard = true;
await loadCardIntoForm(existingCardData);
2024-12-11 14:40:32 -08:00
}
} else {
// No existing card found
2024-12-11 14:40:32 -08:00
alert("No existing card found. Create a new card.");
isExistingCard = false;
}
// Show the form
2024-12-11 14:40:32 -08:00
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.");
}
});
document.getElementById("refresh-cards-button").addEventListener("click", async () => {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Refreshing cards...</p>";
await loadCards();
});
document.getElementById("cancel-publish-button").addEventListener("click", async () => {
2024-12-11 14:40:32 -08:00
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 () => {
2024-12-11 14:40:32 -08:00
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 publishCard();
});
await loadCards();
}
const extractMinterCardsMinterName = async (cardIdentifier) => {
// Ensure the identifier starts with the prefix
if (!cardIdentifier.startsWith(`${cardIdentifierPrefix}-`)) {
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')
}
try {
const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1)
const minterName = await searchSimpleResults.name
return minterName
} catch (error) {
throw error
}
}
const processMinterCards = async (validMinterCards) => {
const latestCardsMap = new Map()
// Step 1: Filter and keep the most recent card per identifier
validMinterCards.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 validMinterCards) {
const minterName = await extractMinterCardsMinterName(card.identifier)
const existingCard = minterNameMap.get(minterName)
const cardTimestamp = card.updated || card.created || 0
const existingTimestamp = existingCard?.updated || existingCard?.created || 0
// 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 loadCards = async () => {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
// const response = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES",
// service: "BLOG_POST",
// query: cardIdentifierPrefix,
// mode: "ALL"
// })
const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, '' , 0)
if (!response || !Array.isArray(response) || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
// Validate cards and filter
const validatedCards = await Promise.all(
response.map(async card => {
const isValid = await validateCardStructure(card);
return isValid ? card : null;
})
);
const validCards = validatedCards.filter(card => card !== null);
if (validCards.length === 0) {
cardsContainer.innerHTML = "<p>No valid cards found.</p>";
return;
}
const finalCards = await processMinterCards(validCards)
// 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 = "";
finalCards.forEach(card => {
const skeletonHTML = createSkeletonCardHTML(card.identifier);
cardsContainer.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: "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);
const BgColor = generateDarkPastelBackgroundBy(card.name)
// Generate final card HTML
const commentCount = await countComments(card.identifier)
const cardUpdatedTime = card.updated || null
const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, BgColor);
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 = "<p>Failed to load cards.</p>";
}
};
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 `
<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><p style="color:rgb(174, 174, 174)">LOADING CARD...</p></div>
<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: 80px; background-color: #eee; color:rgb(17, 24, 28); padding: 0.22vh"><p>PLEASE BE PATIENT</p><p style="color: #11121c"> While data loads from QDN...</div>
</div>
</div>
`;
};
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
const fetchExistingCard = async () => {
2024-12-11 14:40:32 -08:00
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
// })
// Changed to searchSimple to improve load times.
const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, `${userState.accountName}`, 0)
2024-12-11 14:40:32 -08:00
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`)
2024-12-11 14:40:32 -08:00
// 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
} else if (response.length === 1) { // we don't need to go through all of the rest of the checks and filtering nonsense if there's only a single result, just return it.
return response[0]
2024-12-11 14:40:32 -08:00
}
// Validate cards asynchronously, check that they are not comments, etc.
const validatedCards = await Promise.all(
response.map(async card => {
const isValid = await validateCardStructure(card)
return isValid ? card : null
})
)
// Filter out invalid cards
const validCards = validatedCards.filter(card => card !== null)
2024-12-11 14:40:32 -08:00
if (validCards.length > 0) {
// Sort by most recent timestamp
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0]
2024-12-11 14:40:32 -08:00
// 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
})
2024-12-11 14:40:32 -08:00
existingCardIdentifier = mostRecentCard.identifier
existingCardData = cardDataResponse
2024-12-11 14:40:32 -08:00
console.log("Full card data fetched successfully:", cardDataResponse)
2024-12-11 14:40:32 -08:00
return cardDataResponse
}
2024-12-11 14:40:32 -08:00
console.log("No valid cards found.")
return null
2024-12-11 14:40:32 -08:00
} catch (error) {
console.error("Error fetching existing card:", error)
return null
2024-12-11 14:40:32 -08:00
}
}
// Validate that a card is indeed a card and not a comment. -------------------------------------
const validateCardStructure = async (card) => {
2024-12-11 14:40:32 -08:00
return (
typeof card === "object" &&
card.name &&
card.service === "BLOG_POST" &&
card.identifier && !card.identifier.includes("comment") &&
card.created
)
2024-12-11 14:40:32 -08:00
}
// Load existing card data passed, into the form for editing -------------------------------------
const loadCardIntoForm = async (cardData) => {
console.log("Loading existing card data:", cardData)
document.getElementById("card-header").value = cardData.header
document.getElementById("card-content").value = cardData.content
2024-12-11 14:40:32 -08:00
const linksContainer = document.getElementById("links-container")
linksContainer.innerHTML = ""
2024-12-11 14:40:32 -08:00
cardData.links.forEach(link => {
const linkInput = document.createElement("input")
linkInput.type = "text"
linkInput.className = "card-link"
2024-12-11 14:40:32 -08:00
linkInput.value = link;
linksContainer.appendChild(linkInput);
})
2024-12-11 14:40:32 -08:00
}
// Main function to publish a new Minter Card -----------------------------------------------
const publishCard = async () => {
const minterGroupData = await fetchMinterGroupMembers();
const minterGroupAddresses = minterGroupData.map(m => m.member); // array of addresses
// 2) check if user is a minter
const userAddress = userState.accountAddress;
if (minterGroupAddresses.includes(userAddress)) {
alert("You are already a Minter and cannot publish a new card!");
return;
}
2024-12-11 14:40:32 -08:00
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://"))
2024-12-11 14:40:32 -08:00
if (!header || !content) {
alert("Header and content are required!")
return
2024-12-11 14:40:32 -08:00
}
const cardIdentifier = isExistingCard ? existingCardIdentifier : `${cardIdentifierPrefix}-${await uid()}`
const pollName = `${cardIdentifier}-poll`
const pollDescription = `Mintership Board Poll for ${userState.accountName}`
2024-12-11 14:40:32 -08:00
const cardData = {
header,
content,
links,
creator: userState.accountName,
timestamp: Date.now(),
poll: pollName,
}
2024-12-11 14:40:32 -08:00
try {
let base64CardData = await objectToBase64(cardData)
2024-12-11 14:40:32 -08:00
if (!base64CardData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`)
base64CardData = btoa(JSON.stringify(cardData))
2024-12-11 14:40:32 -08:00
}
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()
2024-12-11 14:40:32 -08:00
} catch (error) {
console.error("Error publishing card or poll:", error)
alert("Failed to publish card and poll.")
2024-12-11 14:40:32 -08:00
}
}
const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator) => {
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>`
}
}
const memberAddresses = minterGroupMembers.map(m => m.member)
const minterAdminAddresses = minterAdmins.map(m => m.member)
const adminGroupsMembers = await fetchAllAdminGroupsMembers()
const groupAdminAddresses = adminGroupsMembers.map(m => m.member)
const adminAddresses = [...minterAdminAddresses, ...groupAdminAddresses]
let adminYes = 0, adminNo = 0
let minterYes = 0, minterNo = 0
let yesWeight = 0, noWeight = 0
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)
2024-12-11 14:40:32 -08:00
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.`)
}
2024-12-11 14:40:32 -08:00
}
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)
}
2024-12-11 14:40:32 -08:00
let blocksMinted = 0
try {
const addressInfo = await getAddressInfo(voterAddress)
blocksMinted = addressInfo?.blocksMinted || 0
} catch (e) {
console.warn(`Failed to get addressInfo for ${voterAddress}`, e)
2024-12-11 14:40:32 -08:00
}
const isAdmin = adminAddresses.includes(voterAddress)
const isMinter = memberAddresses.includes(voterAddress)
return {
optionIndex,
voterPublicKey,
voterAddress,
voterName,
isAdmin,
isMinter,
blocksMinted
}
})
2024-12-11 14:40:32 -08:00
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
}
}
2024-12-11 14:40:32 -08:00
yesVoters.sort((a,b) => b.blocksMinted - a.blocksMinted);
noVoters.sort((a,b) => b.blocksMinted - a.blocksMinted);
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
}
}
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>
`
}
2024-12-11 14:40:32 -08:00
// Post a comment on a card. ---------------------------------
2024-12-11 14:40:32 -08:00
const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`)
const commentText = commentInput.value.trim()
2024-12-11 14:40:32 -08:00
if (!commentText) {
alert('Comment cannot be empty!')
return
2024-12-11 14:40:32 -08:00
}
const commentData = {
content: commentText,
creator: userState.accountName,
timestamp: Date.now(),
}
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`
2024-12-11 14:40:32 -08:00
try {
const base64CommentData = await objectToBase64(commentData)
if (!base64CommentData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`)
base64CommentData = btoa(JSON.stringify(commentData))
}
2024-12-11 14:40:32 -08:00
await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName,
service: 'BLOG_POST',
identifier: commentIdentifier,
data64: base64CommentData,
})
alert('Comment posted successfully!')
commentInput.value = ''
2024-12-11 14:40:32 -08:00
} catch (error) {
console.error('Error posting comment:', error)
alert('Failed to post comment.')
2024-12-11 14:40:32 -08:00
}
}
2024-12-11 14:40:32 -08:00
//Fetch the comments for a card with passed card identifier ----------------------------
2024-12-11 14:40:32 -08:00
const fetchCommentsForCard = async (cardIdentifier) => {
try {
const response = await searchSimple('BLOG_POST',`comment-${cardIdentifier}`, '', 0, 0, '', 'false')
return response
2024-12-11 14:40:32 -08:00
} catch (error) {
console.error(`Error fetching comments for ${cardIdentifier}:`, error)
return []
2024-12-11 14:40:32 -08:00
}
}
2024-12-11 14:40:32 -08:00
// display the comments on the card, with passed cardIdentifier to identify the card --------------
2024-12-11 14:40:32 -08:00
const displayComments = async (cardIdentifier) => {
try {
const comments = await fetchCommentsForCard(cardIdentifier);
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`)
2024-12-11 14:40:32 -08:00
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)
2024-12-11 14:40:32 -08:00
const commentHTML = `
<div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;">
<p><strong><u>${commentDataResponse.creator}</strong>:</p></u>
<p>${commentDataResponse.content}</p>
<p><i>${timestamp}</p></i>
</div>
`
commentsContainer.insertAdjacentHTML('beforeend', commentHTML)
2024-12-11 14:40:32 -08:00
}
2024-12-11 14:40:32 -08:00
} catch (error) {
console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error)
2024-12-11 14:40:32 -08:00
}
}
2024-12-11 14:40:32 -08:00
// Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled --------------------
2024-12-11 14:40:32 -08:00
const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`)
const commentButton = document.getElementById(`comment-button-${cardIdentifier}`)
if (!commentsSection || !commentButton) return
const count = commentButton.dataset.commentCount
const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display)
if (isHidden) {
// Show comments
commentButton.textContent = "LOADING..."
2024-12-11 14:40:32 -08:00
await displayComments(cardIdentifier);
commentsSection.style.display = 'block'
// Change the button text to 'HIDE COMMENTS'
commentButton.textContent = 'HIDE COMMENTS'
2024-12-11 14:40:32 -08:00
} else {
// Hide comments
commentsSection.style.display = 'none'
commentButton.textContent = `COMMENTS (${count})`
2024-12-11 14:40:32 -08:00
}
}
2024-12-11 14:40:32 -08:00
const countComments = async (cardIdentifier) => {
try {
const response = await searchSimple('BLOG_POST', `comment-${cardIdentifier}`, '', 0, 0, '', 'false')
return Array.isArray(response) ? response.length : 0
} catch (error) {
console.error(`Error fetching comment count for ${cardIdentifier}:`, error)
return 0
}
}
const createModal = (modalType='') => {
if (document.getElementById(`${modalType}-modal`)) {
return
}
const isIframe = (modalType === 'links')
const modalHTML = `
<div id="${modalType}-modal"
style="display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.50);
z-index: 10000;">
<div id="${modalType}-modalContainer"
style="position: relative;
margin: 10% auto;
width: 80%;
height: 70%;
background:rgba(0, 0, 0, 0.80) ;
border-radius: 10px;
overflow: hidden;">
${
isIframe
? `<iframe id="${modalType}-modalContent"
src=""
style="width: 100%; height: 100%; border: none;">
</iframe>`
: `<div id="${modalType}-modalContent"
style="width: 100%; height: 100%; overflow: auto;">
</div>`
}
<button onclick="closeModal('${modalType}')"
style="position: absolute; top: 0.2rem; right: 0.2rem;
background:rgba(0, 0, 0, 0.66); color: white; border: none;
font-size: 2.2rem;
padding: 0.4rem 1rem;
border-radius: 0.33rem;
border-style: dashed;
border-color:rgb(213, 224, 225);
"
onmouseover="this.style.backgroundColor='rgb(73, 7, 7) '"
onmouseout="this.style.backgroundColor='rgba(5, 14, 11, 0.63) '">
X
</button>
</div>
</div>
`
document.body.insertAdjacentHTML('beforeend', modalHTML)
const modal = document.getElementById(`${modalType}-modal`)
window.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal(modalType)
}
})
}
const openLinksModal = async (link) => {
const processedLink = await processLink(link)
const modal = document.getElementById('links-modal')
const modalContent = document.getElementById('links-modalContent')
modalContent.src = processedLink
modal.style.display = 'block'
}
const closeModal = async (modalType='links') => {
const modal = document.getElementById(`${modalType}-modal`)
const modalContent = document.getElementById(`${modalType}-modalContent`)
if (modal) {
modal.style.display = 'none'
}
if (modalContent) {
modalContent.src = ''
}
}
const processLink = async (link) => {
if (link.startsWith('qortal://')) {
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/)
if (match) {
const firstParam = match[1].toUpperCase()
const remainingPath = match[2] || ""
const themeColor = window._qdnTheme || 'default'
await new Promise(resolve => setTimeout(resolve, 10))
return `/render/${firstParam}${remainingPath}?theme=${themeColor}`
}
}
return link
}
const togglePollDetails = (cardIdentifier) => {
const detailsDiv = document.getElementById(`poll-details-${cardIdentifier}`)
const modal = document.getElementById(`poll-details-modal`)
const modalContent = document.getElementById(`poll-details-modalContent`)
if (!detailsDiv || !modal || !modalContent) return
// modalContent.appendChild(detailsDiv)
modalContent.innerHTML = detailsDiv.innerHTML
modal.style.display = 'block'
window.onclick = (event) => {
if (event.target === modal) {
modal.style.display = 'none'
}
}
}
const generateDarkPastelBackgroundBy = (name) => {
let hash = 0
for (let i = 0; i < name.length; i++) {
hash = (hash << 5) - hash + name.charCodeAt(i)
hash |= 0
}
const safeHash = Math.abs(hash)
const hueSteps = 69.69
const hueIndex = safeHash % hueSteps
const hueRange = 288
const hue = 140 + (hueIndex * (hueRange / hueSteps))
const satSteps = 13.69
const satIndex = safeHash % satSteps
const saturation = 18 + (satIndex * 1.333)
const lightSteps = 3.69
const lightIndex = safeHash % lightSteps
const lightness = 7 + lightIndex
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
}
// Create the overall Minter Card HTML -----------------------------------------------
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, BgColor) => {
2024-12-11 14:40:32 -08:00
const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
const avatarHtml = await getMinterAvatar(creator)
2024-12-11 14:40:32 -08:00
const linksHTML = links.map((link, index) => `
<button onclick="openLinksModal('${link}')">
2024-12-11 14:40:32 -08:00
${`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, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator)
createModal('links')
createModal('poll-details')
2024-12-11 14:40:32 -08:00
return `
<div class="minter-card" style="background-color: ${BgColor}">
2024-12-11 14:40:32 -08:00
<div class="minter-card-header">
${avatarHtml}
2024-12-11 14:40:32 -08:00
<h3>${creator}</h3>
<p>${header}</p>
</div>
<div class="support-header"><h5>MINTER'S POST</h5></div>
2024-12-11 14:40:32 -08:00
<div class="info">
${content}
</div>
<div class="support-header"><h5>MINTER'S LINKS</h5></div>
2024-12-11 14:40:32 -08:00
<div class="info-links">
${linksHTML}
</div>
<div class="results-header support-header"><h5>CURRENT RESULTS</h5></div>
2024-12-11 14:40:32 -08:00
<div class="minter-card-results">
<button onclick="togglePollDetails('${cardIdentifier}')">Display Poll Details</button>
<div id="poll-details-${cardIdentifier}" style="display: none;">
${detailsHtml}
</div>
2024-12-11 14:40:32 -08:00
<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">Minter Yes: ${minterYes}</span>
<span class="minter-no">Minter No: ${minterNo}</span>
</div>
<div class="total-results">
<span class="total-yes">Total Yes: ${totalYes}</span>
<span class="total-yes">Weight: ${totalYesWeight}</span>
2024-12-11 14:40:32 -08:00
<span class="total-no">Total No: ${totalNo}</span>
<span class="total-no">Weight: ${totalNoWeight}</span>
2024-12-11 14:40:32 -08:00
</div>
</div>
<div class="support-header"><h5>SUPPORT</h5><h5 style="color: #ffae42;">${creator}</h5>
<p style="color: #c7c7c7; font-size: .65rem; margin-top: 1vh">(click COMMENTS button to open/close card comments)</p>
</div>
2024-12-11 14:40:32 -08:00
<div class="actions">
<div class="actions-buttons">
2024-12-11 18:32:48 -08:00
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
<button class="comment" id="comment-button-${cardIdentifier}" data-comment-count="${commentCount}" onclick="toggleComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
2024-12-11 18:32:48 -08:00
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
2024-12-11 14:40:32 -08:00
</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="postComment('${cardIdentifier}')">Post Comment</button>
</div>
<p style="font-size: 0.75rem; margin-top: 3vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
2024-12-11 14:40:32 -08:00
</div>
`;
}