Version 0.65beta includes detailed poll results table for every minter card, extensive code cleanup on JS files, and removal of the ability for already-existing-minters to publish a minter card.

This commit is contained in:
crowetic 2024-12-28 22:49:18 -08:00
parent e99929876f
commit cfccfab99a
5 changed files with 618 additions and 435 deletions

View File

@ -755,6 +755,30 @@ body {
color: #dddddd; color: #dddddd;
} }
.minter-card-results button {
color: white;
background-color: rgb(48, 60, 63);
border-radius: 8px;
border-color: white;
border-style: groove;
font-size: 1.1em;
padding: 0.6vh 1.2vh;
margin: 0.15vh;
cursor: pointer;
}
.minter-card-results button:hover {
color: rgb(112, 113, 100);
background-color: rgb(0, 0, 0);
border-radius: 8px;
border-color: white;
border-style: inset;
font-size: 1.1em;
padding: 0.6vh 1.2vh;
margin: 0.15vh;
cursor: pointer;
}
.minter-card .info { .minter-card .info {
background-color: #2a2a2a; /* Very dark grey background */ background-color: #2a2a2a; /* Very dark grey background */
border: 1px solid #d3d3d3; /* Light grey border */ border: 1px solid #d3d3d3; /* Light grey border */

View File

@ -162,11 +162,11 @@ const loadOrFetchAdminGroupsData = async () => {
} }
} }
const extractCardsMinterName = (cardIdentifier) => { const extractEncryptedCardsMinterName = (cardIdentifier) => {
// Ensure the identifier starts with the prefix // Ensure the identifier starts with the prefix
if (!cardIdentifier.startsWith(`${encryptedCardIdentifierPrefix}-`)) { // if (!cardIdentifier.startsWith(`${encryptedCardIdentifierPrefix}-`)) {
throw new Error('Invalid identifier format or prefix mismatch'); // throw new Error('Invalid identifier format or prefix mismatch');
} // }
// Split the identifier into parts // Split the identifier into parts
const parts = cardIdentifier.split('-'); const parts = cardIdentifier.split('-');
// Ensure the format has at least 3 parts // Ensure the format has at least 3 parts
@ -176,7 +176,7 @@ const extractCardsMinterName = (cardIdentifier) => {
if (parts.slice(2, -1).join('-') === 'TOPIC') { if (parts.slice(2, -1).join('-') === 'TOPIC') {
console.log(`TOPIC found in identifier: ${cardIdentifier} - not including in duplicatesList`) console.log(`TOPIC found in identifier: ${cardIdentifier} - not including in duplicatesList`)
return 'topic' return
} }
// Extract minterName (everything from the second part to the second-to-last part) // Extract minterName (everything from the second part to the second-to-last part)
const minterName = parts.slice(2, -1).join('-'); const minterName = parts.slice(2, -1).join('-');
@ -243,6 +243,7 @@ const fetchAllEncryptedCards = async () => {
return; return;
} }
const finalCards = await processCards(validEncryptedCards) const finalCards = await processCards(validEncryptedCards)
console.log(`finalCards:`,finalCards) console.log(`finalCards:`,finalCards)
// Display skeleton cards immediately // Display skeleton cards immediately
encryptedCardsContainer.innerHTML = ""; encryptedCardsContainer.innerHTML = "";
@ -254,6 +255,8 @@ const fetchAllEncryptedCards = async () => {
// Fetch and update each card // Fetch and update each card
finalCards.forEach(async card => { finalCards.forEach(async card => {
try { try {
const hasMinterName = await extractEncryptedCardsMinterName(card.identifier)
if (hasMinterName) existingCardMinterNames.push(hasMinterName)
const cardDataResponse = await qortalRequest({ const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
name: card.name, name: card.name,
@ -349,7 +352,7 @@ const fetchExistingEncryptedCard = async (minterName) => {
// }) // })
//CHANGED to searchSimple to speed up search results. //CHANGED to searchSimple to speed up search results.
const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0) const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, minterName, 0)
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`); console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`);
@ -442,7 +445,7 @@ const validateMinterName = async(minterName) => {
const publishEncryptedCard = async (isTopicModePassed = false) => { const publishEncryptedCard = async (isTopicModePassed = false) => {
// If the user wants it to be a topic, we set global isTopic = true, else false // If the user wants it to be a topic, we set global isTopic = true, else false
isTopic = isTopicModePassed; isTopic = isTopicModePassed || isTopic
const minterNameInput = document.getElementById("minter-name-input").value.trim(); const minterNameInput = document.getElementById("minter-name-input").value.trim();
const header = document.getElementById("card-header").value.trim(); const header = document.getElementById("card-header").value.trim();
@ -461,23 +464,23 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
// If not topic mode, validate the user actually entered a valid Minter name // If not topic mode, validate the user actually entered a valid Minter name
if (!isTopic) { if (!isTopic) {
publishedMinterName = await validateMinterName(minterNameInput); publishedMinterName = await validateMinterName(minterNameInput)
if (!publishedMinterName) { if (!publishedMinterName) {
alert(`"${minterNameInput}" doesn't seem to be a valid Minter name. Please check or use topic mode.`); alert(`"${minterNameInput}" doesn't seem to be a valid Minter name. Please check or use topic mode.`)
return; return
} }
// Also check for existing card if not topic // Also check for existing card if not topic
if (!isExistingEncryptedCard && existingCardMinterNames.includes(publishedMinterName)) { if (!isExistingEncryptedCard && existingCardMinterNames.includes(publishedMinterName)) {
const updateCard = confirm( const updateCard = confirm(
`Minter Name: ${publishedMinterName} already has a card. Update or Cancel?` `Minter Name: ${publishedMinterName} already has a card. Update or Cancel?`
); )
if (updateCard) { if (updateCard) {
await fetchExistingEncryptedCard(publishedMinterName); await fetchExistingEncryptedCard(publishedMinterName)
await loadEncryptedCardIntoForm(); await loadEncryptedCardIntoForm()
isExistingEncryptedCard = true; isExistingEncryptedCard = true
return; return
} else { } else {
return; return
} }
} }
} }
@ -485,12 +488,12 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
// Determine final card identifier // Determine final card identifier
const newCardIdentifier = isTopic const newCardIdentifier = isTopic
? `${encryptedCardIdentifierPrefix}-TOPIC-${await uid()}` ? `${encryptedCardIdentifierPrefix}-TOPIC-${await uid()}`
: `${encryptedCardIdentifierPrefix}-${publishedMinterName}-${await uid()}`; : `${encryptedCardIdentifierPrefix}-${publishedMinterName}-${await uid()}`
const cardIdentifier = isExistingEncryptedCard ? existingEncryptedCardIdentifier : newCardIdentifier; const cardIdentifier = isExistingEncryptedCard ? existingEncryptedCardIdentifier : newCardIdentifier
// Build cardData // Build cardData
const pollName = `${cardIdentifier}-poll`; const pollName = `${cardIdentifier}-poll`
const cardData = { const cardData = {
minterName: publishedMinterName, minterName: publishedMinterName,
header, header,
@ -500,7 +503,7 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
timestamp: Date.now(), timestamp: Date.now(),
poll: pollName, poll: pollName,
topicMode: isTopic topicMode: isTopic
}; }
try { try {
// Convert to base64 or fallback // Convert to base64 or fallback
@ -523,7 +526,6 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
} }
verifiedAdminPublicKeys = loadedAdminKeys verifiedAdminPublicKeys = loadedAdminKeys
} }
await qortalRequest({ await qortalRequest({
@ -534,7 +536,7 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
data64: base64CardData, data64: base64CardData,
encrypt: true, encrypt: true,
publicKeys: verifiedAdminPublicKeys publicKeys: verifiedAdminPublicKeys
}); })
// Possibly create a poll if its a brand new card // Possibly create a poll if its a brand new card
if (!isExistingEncryptedCard) { if (!isExistingEncryptedCard) {
@ -544,18 +546,20 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
pollDescription: `Admin Board Poll Published By ${userState.accountName}`, pollDescription: `Admin Board Poll Published By ${userState.accountName}`,
pollOptions: ["Yes, No"], pollOptions: ["Yes, No"],
pollOwnerAddress: userState.accountAddress pollOwnerAddress: userState.accountAddress
}); })
alert("Card and poll published successfully!"); alert("Card and poll published successfully!")
// If its a real Minter name, store it so we know we have a card for them // If its a real Minter name, store it so we know we have a card for them
if (!isTopic) { if (!isTopic) {
existingCardMinterNames.push(publishedMinterName); if (!existingCardMinterNames.contains(publishedMinterName)) {
existingCardMinterNames.push(publishedMinterName)
} }
}
} else { } else {
alert("Card updated successfully! (No poll updates possible currently...)"); alert("Card updated successfully! (No poll updates possible currently...)");
} }
// Cleanup UI
document.getElementById("publish-card-form").reset(); document.getElementById("publish-card-form").reset();
document.getElementById("publish-card-view").style.display = "none"; document.getElementById("publish-card-view").style.display = "none";
document.getElementById("encrypted-cards-container").style.display = "flex"; document.getElementById("encrypted-cards-container").style.display = "flex";
@ -569,40 +573,31 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
const getEncryptedCommentCount = async (cardIdentifier) => { const getEncryptedCommentCount = async (cardIdentifier) => {
try { try {
// const response = await qortalRequest({
// action: 'SEARCH_QDN_RESOURCES',
// service: 'MAIL_PRIVATE',
// query: `comment-${cardIdentifier}`,
// mode: "ALL"
// })
const response = await searchSimple('MAIL_PRIVATE', `comment-${cardIdentifier}`, '', 0) const response = await searchSimple('MAIL_PRIVATE', `comment-${cardIdentifier}`, '', 0)
// Just return the count; no need to decrypt each comment here
return Array.isArray(response) ? response.length : 0 return Array.isArray(response) ? response.length : 0
} catch (error) { } catch (error) {
console.error(`Error fetching comment count for ${cardIdentifier}:`, error) console.error(`Error fetching comment count for ${cardIdentifier}:`, error)
return 0 return 0
} }
}; }
// Post a comment on a card. --------------------------------- // Post a comment on a card. ---------------------------------
const postEncryptedComment = async (cardIdentifier) => { const postEncryptedComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`); const commentInput = document.getElementById(`new-comment-${cardIdentifier}`)
const commentText = commentInput.value.trim(); const commentText = commentInput.value.trim()
if (!commentText) { if (!commentText) {
alert('Comment cannot be empty!'); alert('Comment cannot be empty!')
return; return
} }
const postTimestamp = Date.now() const postTimestamp = Date.now()
console.log(`timestmp to be posted: ${postTimestamp}`)
const commentData = { const commentData = {
content: commentText, content: commentText,
creator: userState.accountName, creator: userState.accountName,
timestamp: postTimestamp, timestamp: postTimestamp,
}; }
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0) || (!adminPublicKeys)) { if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0) || (!adminPublicKeys)) {
console.log('adminPpublicKeys variable failed checks, calling for admin public keys from API (comment)',adminPublicKeys) console.log('adminPpublicKeys variable failed checks, calling for admin public keys from API (comment)',adminPublicKeys)
@ -611,10 +606,10 @@ const postEncryptedComment = async (cardIdentifier) => {
} }
try { try {
const base64CommentData = await objectToBase64(commentData); const base64CommentData = await objectToBase64(commentData)
if (!base64CommentData) { if (!base64CommentData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`); console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`)
base64CommentData = btoa(JSON.stringify(commentData)); base64CommentData = btoa(JSON.stringify(commentData))
} }
await qortalRequest({ await qortalRequest({
@ -625,45 +620,35 @@ const postEncryptedComment = async (cardIdentifier) => {
data64: base64CommentData, data64: base64CommentData,
encrypt: true, encrypt: true,
publicKeys: adminPublicKeys publicKeys: adminPublicKeys
}); })
alert('Comment posted successfully!')
commentInput.value = ''
alert('Comment posted successfully!');
commentInput.value = ''; // Clear input
} catch (error) { } catch (error) {
console.error('Error posting comment:', error); console.error('Error posting comment:', error)
alert('Failed to post comment.'); alert('Failed to post comment.')
}
} }
};
//Fetch the comments for a card with passed card identifier ---------------------------- //Fetch the comments for a card with passed card identifier ----------------------------
const fetchEncryptedComments = async (cardIdentifier) => { const fetchEncryptedComments = async (cardIdentifier) => {
try { try {
// const response = await qortalRequest({
// action: 'SEARCH_QDN_RESOURCES',
// service: 'MAIL_PRIVATE',
// query: `comment-${cardIdentifier}`,
// mode: "ALL"
// })
const response = await searchSimple('MAIL_PRIVATE', `comment-${cardIdentifier}`, '', 0) const response = await searchSimple('MAIL_PRIVATE', `comment-${cardIdentifier}`, '', 0)
if (response) { if (response) {
return response; return response;
} }
} catch (error) { } catch (error) {
console.error(`Error fetching comments for ${cardIdentifier}:`, error); console.error(`Error fetching comments for ${cardIdentifier}:`, error)
return []; return []
}
} }
};
// display the comments on the card, with passed cardIdentifier to identify the card -------------- // display the comments on the card, with passed cardIdentifier to identify the card --------------
const displayEncryptedComments = async (cardIdentifier) => { const displayEncryptedComments = async (cardIdentifier) => {
try { try {
const comments = await fetchEncryptedComments(cardIdentifier)
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`)
const comments = await fetchEncryptedComments(cardIdentifier);
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
// Fetch and display each comment
for (const comment of comments) { for (const comment of comments) {
const commentDataResponse = await qortalRequest({ const commentDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
@ -671,13 +656,11 @@ const displayEncryptedComments = async (cardIdentifier) => {
service: "MAIL_PRIVATE", service: "MAIL_PRIVATE",
identifier: comment.identifier, identifier: comment.identifier,
encoding: "base64" encoding: "base64"
}); })
const decryptedCommentData = await decryptAndParseObject(commentDataResponse) const decryptedCommentData = await decryptAndParseObject(commentDataResponse)
const timestampCheck = comment.updated || comment.created || 0 const timestampCheck = comment.updated || comment.created || 0
const timestamp = await timestampToHumanReadableDate(timestampCheck); const timestamp = await timestampToHumanReadableDate(timestampCheck)
//TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section. //TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section.
const commentHTML = ` const commentHTML = `
<div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;"> <div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;">
@ -685,201 +668,120 @@ const displayEncryptedComments = async (cardIdentifier) => {
<p>${decryptedCommentData.content}</p> <p>${decryptedCommentData.content}</p>
<p><i>${timestamp}</p></i> <p><i>${timestamp}</p></i>
</div> </div>
`; `
commentsContainer.insertAdjacentHTML('beforeend', commentHTML); commentsContainer.insertAdjacentHTML('beforeend', commentHTML)
} }
} catch (error) { } catch (error) {
console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error); console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error)
}
};
const calculateAdminBoardPollResults = async (pollData, minterGroupMembers, minterAdmins) => {
// 1) Validate pollData structure
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
};
}
// 2) Prepare admin & minter addresses
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;
let minterYes = 0, minterNo = 0;
let yesWeight = 0, noWeight = 0;
// 3) Process voteWeights
pollData.voteWeights.forEach(weightData => {
if (weightData.optionName === 'Yes') {
yesWeight = weightData.voteWeight;
} else if (weightData.optionName === 'No') {
noWeight = weightData.voteWeight;
}
});
// 4) Process votes
for (const vote of pollData.votes) {
const voterAddress = await getAddressFromPublicKey(vote.voterPublicKey);
// console.log(`voter address: ${voterAddress}, optionIndex: ${vote.optionIndex}`);
if (vote.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 including results...`);
}
} else if (vote.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 including results...`);
} }
} }
}
// 5) Summaries
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 toggleEncryptedComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`) const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`)
const commentButton = document.getElementById(`comment-button-${cardIdentifier}`) const commentButton = document.getElementById(`comment-button-${cardIdentifier}`)
if (!commentsSection || !commentButton) return; if (!commentsSection || !commentButton) return
const count = commentButton.dataset.commentCount; const count = commentButton.dataset.commentCount;
const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display); const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display)
if (isHidden) { if (isHidden) {
// Show comments // Show comments
commentButton.textContent = "LOADING..."; commentButton.textContent = "LOADING..."
await displayEncryptedComments(cardIdentifier); await displayEncryptedComments(cardIdentifier)
commentsSection.style.display = 'block'; commentsSection.style.display = 'block'
// Change the button text to 'HIDE COMMENTS' // Change the button text to 'HIDE COMMENTS'
commentButton.textContent = 'HIDE COMMENTS'; commentButton.textContent = 'HIDE COMMENTS'
} else { } else {
// Hide comments // Hide comments
commentsSection.style.display = 'none'; commentsSection.style.display = 'none'
commentButton.textContent = `COMMENTS (${count})`; commentButton.textContent = `COMMENTS (${count})`
}
} }
};
const createLinkDisplayModal = async () => { const createLinkDisplayModal = async () => {
const modalHTML = ` 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 id="links-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;"> <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> <iframe id="links-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> <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>
</div> </div>
`; `
document.body.insertAdjacentHTML('beforeend', modalHTML); document.body.insertAdjacentHTML('beforeend', modalHTML)
} }
// Function to open the modal // Function to open the modal
const openLinkDisplayModal = async (link) => { const openLinkDisplayModal = async (link) => {
const processedLink = await processQortalLinkForRendering(link) // Process the link to replace `qortal://` for rendering in modal const processedLink = await processQortalLinkForRendering(link) // Process the link to replace `qortal://` for rendering in modal
const modal = document.getElementById('modal'); const modal = document.getElementById('links-modal');
const modalContent = document.getElementById('modalContent'); const modalContent = document.getElementById('links-modalContent');
modalContent.src = processedLink; // Set the iframe source to the link modalContent.src = processedLink; // Set the iframe source to the link
modal.style.display = 'block'; // Show the modal modal.style.display = 'block'; // Show the modal
} }
// Function to close the modal // Function to close the modal
const closeLinkDisplayModal = async () => { const closeLinkDisplayModal = async () => {
const modal = document.getElementById('modal'); const modal = document.getElementById('links-modal');
const modalContent = document.getElementById('modalContent'); const modalContent = document.getElementById('links-modalContent');
modal.style.display = 'none'; // Hide the modal modal.style.display = 'none'; // Hide the modal
modalContent.src = ''; // Clear the iframe source 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
// }
const processQortalLinkForRendering = async (link) => { const processQortalLinkForRendering = async (link) => {
if (link.startsWith('qortal://')) { if (link.startsWith('qortal://')) {
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/); const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/)
if (match) { if (match) {
const firstParam = match[1].toUpperCase(); const firstParam = match[1].toUpperCase();
const remainingPath = match[2] || ""; const remainingPath = match[2] || ""
const themeColor = window._qdnTheme || 'default'; // Fallback to 'default' if undefined const themeColor = window._qdnTheme || 'default' // Fallback to 'default' if undefined
// Simulating async operation if needed // Simulating async operation if needed
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10))
// Append theme as a query parameter return `/render/${firstParam}${remainingPath}?theme=${themeColor}`
return `/render/${firstParam}${remainingPath}?theme=${themeColor}`;
} }
} }
return link; return link
}; }
async function getMinterAvatar(minterName) {
const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`;
const getMinterAvatar = async (minterName) => {
const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`
try { try {
const response = await fetch(avatarUrl, { method: 'HEAD' }); const response = await fetch(avatarUrl, { method: 'HEAD' })
if (response.ok) { if (response.ok) {
// Avatar exists, return the image HTML return `<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">`
return `<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">`;
} else { } else {
// Avatar not found or no permission return ''
return '';
} }
} catch (error) { } catch (error) {
console.error('Error checking avatar availability:', error); console.error('Error checking avatar availability:', error)
return ''; return ''
} }
} }
// 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)
// modal.style.display = 'block'
// window.onclick = (event) => {
// if (event.target === modal) {
// modal.style.display = 'none'
// }
// }
// }
// Create the overall Minter Card HTML ----------------------------------------------- // Create the overall Minter Card HTML -----------------------------------------------
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => { const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
const { minterName, header, content, links, creator, timestamp, poll, topicMode } = cardData const { minterName, header, content, links, creator, timestamp, poll, topicMode } = cardData
const formattedDate = new Date(timestamp).toLocaleString() const formattedDate = new Date(timestamp).toLocaleString()
const minterAvatar = !topicMode ? await getMinterAvatar(minterName) : null const minterAvatar = !topicMode ? await getMinterAvatar(minterName) : null
// const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const creatorAvatar = await getMinterAvatar(creator) const creatorAvatar = await getMinterAvatar(creator)
const linksHTML = links.map((link, index) => ` const linksHTML = links.map((link, index) => `
<button onclick="openLinkDisplayModal('${link}')"> <button onclick="openLinkDisplayModal('${link}')">
@ -890,15 +792,14 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
const isUndefinedUser = (minterName === 'undefined') const isUndefinedUser = (minterName === 'undefined')
const hasTopicMode = Object.prototype.hasOwnProperty.call(cardData, 'topicMode') const hasTopicMode = Object.prototype.hasOwnProperty.call(cardData, 'topicMode')
// 2) Decide if this card is showing as "Topic" or "Name"
let showTopic = false let showTopic = false
if (hasTopicMode) { if (hasTopicMode) {
// If present, see if it's actually "true" or true const modeVal = cardData.topicMode
const modeVal = cardData.topicMode;
showTopic = (modeVal === true || modeVal === 'true') showTopic = (modeVal === true || modeVal === 'true')
} else { } else {
if (!isUndefinedUser) { if (!isUndefinedUser) {
// No topicMode => older card => default to Name
showTopic = false showTopic = false
} }
} }
@ -915,8 +816,9 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
const minterGroupMembers = await fetchMinterGroupMembers() const minterGroupMembers = await fetchMinterGroupMembers()
const minterAdmins = await fetchMinterGroupAdmins() 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) const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator)
await createModal() createModal('links')
createModal('poll-details')
return ` return `
<div class="admin-card" style="background-color: ${cardColorCode}"> <div class="admin-card" style="background-color: ${cardColorCode}">
<div class="minter-card-header"> <div class="minter-card-header">
@ -935,6 +837,10 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
</div> </div>
<div class="results-header support-header"><h5>CURRENT RESULTS</h5></div> <div class="results-header support-header"><h5>CURRENT RESULTS</h5></div>
<div class="minter-card-results"> <div class="minter-card-results">
<button onclick="togglePollDetails('${cardIdentifier}')">Display Poll Details</button>
<div id="poll-details-${cardIdentifier}" style="display: none;">
${detailsHtml}
</div>
<div class="admin-results"> <div class="admin-results">
<span class="admin-yes">Admin Support: ${adminYes}</span> <span class="admin-yes">Admin Support: ${adminYes}</span>
<span class="admin-no">Admin Against: ${adminNo}</span> <span class="admin-no">Admin Against: ${adminNo}</span>

View File

@ -32,7 +32,7 @@ const loadMinterBoardPage = async () => {
<label for="card-header">Header:</label> <label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required> <input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label> <label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter detailed information about why you deserve to be a minter..." required></textarea> <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>
<label for="card-links">Links (qortal://...):</label> <label for="card-links">Links (qortal://...):</label>
<div id="links-container"> <div id="links-container">
<input type="text" class="card-link" placeholder="Enter QDN link"> <input type="text" class="card-link" placeholder="Enter QDN link">
@ -333,12 +333,12 @@ const fetchExistingCard = async () => {
// Changed to searchSimple to improve load times. // Changed to searchSimple to improve load times.
const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, `${userState.accountName}`, 0) const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, `${userState.accountName}`, 0)
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`); console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`)
// Step 2: Check if the response is an array and not empty // Step 2: Check if the response is an array and not empty
if (!response || !Array.isArray(response) || response.length === 0) { if (!response || !Array.isArray(response) || response.length === 0) {
console.log("No cards found for the current user."); console.log("No cards found for the current user.")
return null; 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. } 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] return response[0]
} }
@ -346,17 +346,17 @@ const fetchExistingCard = async () => {
// Validate cards asynchronously, check that they are not comments, etc. // Validate cards asynchronously, check that they are not comments, etc.
const validatedCards = await Promise.all( const validatedCards = await Promise.all(
response.map(async card => { response.map(async card => {
const isValid = await validateCardStructure(card); const isValid = await validateCardStructure(card)
return isValid ? card : null; return isValid ? card : null
}) })
); )
// Filter out invalid cards // Filter out invalid cards
const validCards = validatedCards.filter(card => card !== null); const validCards = validatedCards.filter(card => card !== null)
if (validCards.length > 0) { if (validCards.length > 0) {
// Sort by most recent timestamp // Sort by most recent timestamp
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0]; const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0]
// Fetch full card data // Fetch full card data
const cardDataResponse = await qortalRequest({ const cardDataResponse = await qortalRequest({
@ -364,23 +364,23 @@ const fetchExistingCard = async () => {
name: userState.accountName, // User's account name name: userState.accountName, // User's account name
service: "BLOG_POST", service: "BLOG_POST",
identifier: mostRecentCard.identifier identifier: mostRecentCard.identifier
}); })
existingCardIdentifier = mostRecentCard.identifier; existingCardIdentifier = mostRecentCard.identifier
existingCardData = cardDataResponse; existingCardData = cardDataResponse
console.log("Full card data fetched successfully:", cardDataResponse); console.log("Full card data fetched successfully:", cardDataResponse)
return cardDataResponse; return cardDataResponse
} }
console.log("No valid cards found."); console.log("No valid cards found.")
return null; return null
} catch (error) { } catch (error) {
console.error("Error fetching existing card:", error); console.error("Error fetching existing card:", error)
return null; return null
}
} }
};
// Validate that a card is indeed a card and not a comment. ------------------------------------- // Validate that a card is indeed a card and not a comment. -------------------------------------
const validateCardStructure = async (card) => { const validateCardStructure = async (card) => {
@ -390,42 +390,52 @@ const validateCardStructure = async (card) => {
card.service === "BLOG_POST" && card.service === "BLOG_POST" &&
card.identifier && !card.identifier.includes("comment") && card.identifier && !card.identifier.includes("comment") &&
card.created card.created
); )
} }
// Load existing card data passed, into the form for editing ------------------------------------- // Load existing card data passed, into the form for editing -------------------------------------
const loadCardIntoForm = async (cardData) => { const loadCardIntoForm = async (cardData) => {
console.log("Loading existing card data:", cardData); console.log("Loading existing card data:", cardData)
document.getElementById("card-header").value = cardData.header; document.getElementById("card-header").value = cardData.header
document.getElementById("card-content").value = cardData.content; document.getElementById("card-content").value = cardData.content
const linksContainer = document.getElementById("links-container"); const linksContainer = document.getElementById("links-container")
linksContainer.innerHTML = ""; // Clear previous links linksContainer.innerHTML = ""
cardData.links.forEach(link => { cardData.links.forEach(link => {
const linkInput = document.createElement("input"); const linkInput = document.createElement("input")
linkInput.type = "text"; linkInput.type = "text"
linkInput.className = "card-link"; linkInput.className = "card-link"
linkInput.value = link; linkInput.value = link;
linksContainer.appendChild(linkInput); linksContainer.appendChild(linkInput);
}); })
} }
// Main function to publish a new Minter Card ----------------------------------------------- // Main function to publish a new Minter Card -----------------------------------------------
const publishCard = async () => { 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;
}
const header = document.getElementById("card-header").value.trim(); const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim(); const content = document.getElementById("card-content").value.trim();
const links = Array.from(document.querySelectorAll(".card-link")) const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim()) .map(input => input.value.trim())
.filter(link => link.startsWith("qortal://")); .filter(link => link.startsWith("qortal://"))
if (!header || !content) { if (!header || !content) {
alert("Header and content are required!"); alert("Header and content are required!")
return; return
} }
const cardIdentifier = isExistingCard ? existingCardIdentifier : `${cardIdentifierPrefix}-${await uid()}`; const cardIdentifier = isExistingCard ? existingCardIdentifier : `${cardIdentifierPrefix}-${await uid()}`
const pollName = `${cardIdentifier}-poll`; const pollName = `${cardIdentifier}-poll`
const pollDescription = `Mintership Board Poll for ${userState.accountName}`; const pollDescription = `Mintership Board Poll for ${userState.accountName}`
const cardData = { const cardData = {
header, header,
@ -434,14 +444,13 @@ const publishCard = async () => {
creator: userState.accountName, creator: userState.accountName,
timestamp: Date.now(), timestamp: Date.now(),
poll: pollName, poll: pollName,
}; }
try { try {
let base64CardData = await objectToBase64(cardData)
let base64CardData = await objectToBase64(cardData);
if (!base64CardData) { if (!base64CardData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`); console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`)
base64CardData = btoa(JSON.stringify(cardData)); base64CardData = btoa(JSON.stringify(cardData))
} }
await qortalRequest({ await qortalRequest({
@ -450,7 +459,8 @@ const publishCard = async () => {
service: "BLOG_POST", service: "BLOG_POST",
identifier: cardIdentifier, identifier: cardIdentifier,
data64: base64CardData, data64: base64CardData,
}); })
if (!isExistingCard){ if (!isExistingCard){
await qortalRequest({ await qortalRequest({
action: "CREATE_POLL", action: "CREATE_POLL",
@ -458,116 +468,266 @@ const publishCard = async () => {
pollDescription, pollDescription,
pollOptions: ['Yes, No'], pollOptions: ['Yes, No'],
pollOwnerAddress: userState.accountAddress, pollOwnerAddress: userState.accountAddress,
}); })
alert("Card and poll published successfully!")
alert("Card and poll published successfully!");
} }
if (isExistingCard){ if (isExistingCard){
alert("Card Updated Successfully! (No poll updates are possible at this time...)") 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("publish-card-form").reset()
document.getElementById("cards-container").style.display = "flex"; document.getElementById("publish-card-view").style.display = "none"
await loadCards(); document.getElementById("cards-container").style.display = "flex"
await loadCards()
} catch (error) { } catch (error) {
console.error("Error publishing card or poll:", error);
alert("Failed to publish card and poll."); console.error("Error publishing card or poll:", error)
alert("Failed to publish card and poll.")
} }
} }
//Calculate the poll results passed from other functions with minterGroupMembers and minterAdmins --------------------------- const processPollData= async (pollData, minterGroupMembers, minterAdmins, creator) => {
const calculatePollResults = async (pollData, minterGroupMembers, minterAdmins) => { if (!pollData || !Array.isArray(pollData.voteWeights) || !Array.isArray(pollData.votes)) {
const memberAddresses = minterGroupMembers.map(member => member.member) console.warn("Poll data is missing or invalid. pollData:", pollData)
const minterAdminAddresses = minterAdmins.map(member => member.member) 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 adminGroupsMembers = await fetchAllAdminGroupsMembers()
const groupAdminAddresses = adminGroupsMembers.map(member => member.member) const groupAdminAddresses = adminGroupsMembers.map(m => m.member)
const adminAddresses = []; const adminAddresses = [...minterAdminAddresses, ...groupAdminAddresses]
adminAddresses.push(...minterAdminAddresses,...groupAdminAddresses); let adminYes = 0, adminNo = 0
let minterYes = 0, minterNo = 0
let yesWeight = 0, noWeight = 0
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, 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
}
}
pollData.voteWeights.forEach(weightData => { const voterPromises = pollData.votes.map(async (vote) => {
if (weightData.optionName === 'Yes') { const optionIndex = vote.optionIndex; // 0 => yes, 1 => no
yesWeight = weightData.voteWeight const voterPublicKey = vote.voterPublicKey
} else if (weightData.optionName === 'No') { const voterAddress = await getAddressFromPublicKey(voterPublicKey)
noWeight = weightData.voteWeight
if (optionIndex === 0) {
if (adminAddresses.includes(voterAddress)) {
adminYes++
} else if (memberAddresses.includes(voterAddress)) {
minterYes++
} else {
console.log(`voter ${voterAddress} is not a minter nor an admin... Not included in aggregates.`)
}
} else if (optionIndex === 1) {
if (adminAddresses.includes(voterAddress)) {
adminNo++
} else if (memberAddresses.includes(voterAddress)) {
minterNo++
} else {
console.log(`voter ${voterAddress} is not a minter nor an admin... Not included in aggregates.`)
}
}
let voterName = ''
try {
const nameInfo = await getNameFromAddress(voterAddress)
if (nameInfo) {
voterName = nameInfo
if (nameInfo === voterAddress) voterName = ''
}
} catch (err) {
console.warn(`No name for address ${voterAddress}`, err)
}
let blocksMinted = 0
try {
const addressInfo = await getAddressInfo(voterAddress)
blocksMinted = addressInfo?.blocksMinted || 0
} catch (e) {
console.warn(`Failed to get addressInfo for ${voterAddress}`, e)
}
const isAdmin = adminAddresses.includes(voterAddress)
const isMinter = memberAddresses.includes(voterAddress)
return {
optionIndex,
voterPublicKey,
voterAddress,
voterName,
isAdmin,
isMinter,
blocksMinted
} }
}) })
for (const vote of pollData.votes) { const allVoters = await Promise.all(voterPromises)
const voterAddress = await getAddressFromPublicKey(vote.voterPublicKey) const yesVoters = []
// console.log(`voter address: ${voterAddress}`) const noVoters = []
let totalMinterAndAdminYesWeight = 0
let totalMinterAndAdminNoWeight = 0
if (vote.optionIndex === 0) { for (const v of allVoters) {
adminAddresses.includes(voterAddress) ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`) if (v.optionIndex === 0) {
} else if (vote.optionIndex === 1) { yesVoters.push(v)
adminAddresses.includes(voterAddress) ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : console.log(`voter ${voterAddress} is not a minter nor an admin...Not including results...`) totalMinterAndAdminYesWeight+=v.blocksMinted
} else if (v.optionIndex === 1) {
noVoters.push(v)
totalMinterAndAdminNoWeight+=v.blocksMinted
} }
} }
// TODO - create a new function to calculate the weights of each voting MINTER only. yesVoters.sort((a,b) => b.blocksMinted - a.blocksMinted);
// This will give ALL weight whether voter is in minter group or not... noVoters.sort((a,b) => b.blocksMinted - a.blocksMinted);
// until that is changed on the core we must calculate manually. const yesTableHtml = buildVotersTableHtml(yesVoters, /* tableColor= */ "green")
const totalYesWeight = yesWeight const noTableHtml = buildVotersTableHtml(noVoters, /* tableColor= */ "red")
const totalNoWeight = noWeight 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 totalYes = adminYes + minterYes
const totalNo = adminNo + minterNo const totalNo = adminNo + minterNo
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo, totalYesWeight, totalNoWeight } 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>
`
}
// Post a comment on a card. --------------------------------- // Post a comment on a card. ---------------------------------
const postComment = async (cardIdentifier) => { const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`); const commentInput = document.getElementById(`new-comment-${cardIdentifier}`)
const commentText = commentInput.value.trim(); const commentText = commentInput.value.trim()
if (!commentText) {
alert('Comment cannot be empty!');
return;
}
if (!commentText) {
alert('Comment cannot be empty!')
return
}
const commentData = { const commentData = {
content: commentText, content: commentText,
creator: userState.accountName, creator: userState.accountName,
timestamp: Date.now(), timestamp: Date.now(),
}; }
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
try { try {
const base64CommentData = await objectToBase64(commentData); const base64CommentData = await objectToBase64(commentData)
if (!base64CommentData) { if (!base64CommentData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`); console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`)
base64CommentData = btoa(JSON.stringify(commentData)); base64CommentData = btoa(JSON.stringify(commentData))
} }
await qortalRequest({ await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE', action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName, name: userState.accountName,
service: 'BLOG_POST', service: 'BLOG_POST',
identifier: commentIdentifier, identifier: commentIdentifier,
data64: base64CommentData, data64: base64CommentData,
}); })
alert('Comment posted successfully!'); alert('Comment posted successfully!')
commentInput.value = ''; // Clear input commentInput.value = ''
// await displayComments(cardIdentifier); // Refresh comments - We don't need to do this as comments will be displayed only after confirmation.
} catch (error) { } catch (error) {
console.error('Error posting comment:', error); console.error('Error posting comment:', error)
alert('Failed to post comment.'); alert('Failed to post comment.')
}
} }
};
//Fetch the comments for a card with passed card identifier ---------------------------- //Fetch the comments for a card with passed card identifier ----------------------------
const fetchCommentsForCard = async (cardIdentifier) => { const fetchCommentsForCard = async (cardIdentifier) => {
try { try {
// const response = await qortalRequest({ const response = await searchSimple('BLOG_POST',`comment-${cardIdentifier}`, '', 0, 0, '', 'false')
// action: 'SEARCH_QDN_RESOURCES',
// service: 'BLOG_POST',
// query: `comment-${cardIdentifier}`,
// mode: "ALL"
// })
const response = await searchSimple('BLOG_POST',`comment-${cardIdentifier}`, '', 0)
return response return response
} catch (error) { } catch (error) {
console.error(`Error fetching comments for ${cardIdentifier}:`, error) console.error(`Error fetching comments for ${cardIdentifier}:`, error)
@ -579,172 +739,218 @@ const fetchCommentsForCard = async (cardIdentifier) => {
const displayComments = async (cardIdentifier) => { const displayComments = async (cardIdentifier) => {
try { try {
const comments = await fetchCommentsForCard(cardIdentifier); const comments = await fetchCommentsForCard(cardIdentifier);
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`); const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`)
// Fetch and display each comment
for (const comment of comments) { for (const comment of comments) {
const commentDataResponse = await qortalRequest({ const commentDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
name: comment.name, name: comment.name,
service: "BLOG_POST", service: "BLOG_POST",
identifier: comment.identifier, identifier: comment.identifier,
}); })
const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp); 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 = ` const commentHTML = `
<div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;"> <div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;">
<p><strong><u>${commentDataResponse.creator}</strong>:</p></u> <p><strong><u>${commentDataResponse.creator}</strong>:</p></u>
<p>${commentDataResponse.content}</p> <p>${commentDataResponse.content}</p>
<p><i>${timestamp}</p></i> <p><i>${timestamp}</p></i>
</div> </div>
`; `
commentsContainer.insertAdjacentHTML('beforeend', commentHTML); commentsContainer.insertAdjacentHTML('beforeend', commentHTML)
} }
} catch (error) { } catch (error) {
console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error); console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error)
}
} }
};
// Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled -------------------- // Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled --------------------
const toggleComments = async (cardIdentifier) => { const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`); const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`)
const commentButton = document.getElementById(`comment-button-${cardIdentifier}`) const commentButton = document.getElementById(`comment-button-${cardIdentifier}`)
if (!commentsSection || !commentButton) return; if (!commentsSection || !commentButton) return
const count = commentButton.dataset.commentCount; const count = commentButton.dataset.commentCount
const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display); const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display)
if (isHidden) { if (isHidden) {
// Show comments // Show comments
commentButton.textContent = "LOADING..."; commentButton.textContent = "LOADING..."
await displayComments(cardIdentifier); await displayComments(cardIdentifier);
commentsSection.style.display = 'block'; commentsSection.style.display = 'block'
// Change the button text to 'HIDE COMMENTS' // Change the button text to 'HIDE COMMENTS'
commentButton.textContent = 'HIDE COMMENTS'; commentButton.textContent = 'HIDE COMMENTS'
} else { } else {
// Hide comments // Hide comments
commentsSection.style.display = 'none'; commentsSection.style.display = 'none'
commentButton.textContent = `COMMENTS (${count})`; commentButton.textContent = `COMMENTS (${count})`
}
} }
};
const countComments = async (cardIdentifier) => { const countComments = async (cardIdentifier) => {
try { try {
// const response = await qortalRequest({ const response = await searchSimple('BLOG_POST', `comment-${cardIdentifier}`, '', 0, 0, '', 'false')
// action: 'SEARCH_QDN_RESOURCES', return Array.isArray(response) ? response.length : 0
// service: 'BLOG_POST',
// query: `comment-${cardIdentifier}`,
// mode: "ALL"
// })
// Changed to searchSimple to hopefully improve load times...
const response = await searchSimple('BLOG_POST', `comment-${cardIdentifier}`, '', 0)
// Just return the count; no need to decrypt each comment here
return Array.isArray(response) ? response.length : 0;
} catch (error) { } catch (error) {
console.error(`Error fetching comment count for ${cardIdentifier}:`, error); console.error(`Error fetching comment count for ${cardIdentifier}:`, error)
return 0; return 0
}
} }
};
const createModal = async () => { const createModal = (modalType='') => {
if (document.getElementById(`${modalType}-modal`)) {
return
}
const isIframe = (modalType === 'links')
const modalHTML = ` 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 id="${modalType}-modal"
<div style="position: relative; margin: 10% auto; width: 95%; height: 80%; background: white; border-radius: 10px; overflow: hidden;"> style="display: none;
<iframe id="modalContent" src="" style="width: 100%; height: 100%; border: none;"></iframe> position: fixed;
<button onclick="closeModal()" style="position: absolute; top: 10px; right: 10px; background: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;">Close</button> top: 0; left: 0;
</div> width: 100%; height: 100%;
</div> background: rgba(0, 0, 0, 0.50);
`; z-index: 10000;">
document.body.insertAdjacentHTML('beforeend', modalHTML); <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>`
} }
// Function to open the modal <button onclick="closeModal('${modalType}')"
const openModal = async (link) => { style="position: absolute; top: 0.2rem; right: 0.2rem;
const processedLink = await processLink(link) // Process the link to replace `qortal://` for rendering in modal background:rgba(0, 0, 0, 0.66); color: white; border: none;
const modal = document.getElementById('modal'); font-size: 2.2rem;
const modalContent = document.getElementById('modalContent'); padding: 0.4rem 1rem;
modalContent.src = processedLink; // Set the iframe source to the link border-radius: 0.33rem;
modal.style.display = 'block'; // Show the modal 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)
}
})
} }
// Function to close the modal
const closeModal = async () => { const openLinksModal = async (link) => {
const modal = document.getElementById('modal'); const processedLink = await processLink(link)
const modalContent = document.getElementById('modalContent'); const modal = document.getElementById('links-modal')
modal.style.display = 'none'; // Hide the modal const modalContent = document.getElementById('links-modalContent')
modalContent.src = ''; // Clear the iframe source 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) => { const processLink = async (link) => {
if (link.startsWith('qortal://')) { if (link.startsWith('qortal://')) {
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/); const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/)
if (match) { if (match) {
const firstParam = match[1].toUpperCase(); const firstParam = match[1].toUpperCase()
const remainingPath = match[2] || ""; const remainingPath = match[2] || ""
const themeColor = window._qdnTheme || 'default'; // Fallback to 'default' if undefined const themeColor = window._qdnTheme || 'default'
// Simulating async operation if needed await new Promise(resolve => setTimeout(resolve, 10))
await new Promise(resolve => setTimeout(resolve, 10));
// Append theme as a query parameter return `/render/${firstParam}${remainingPath}?theme=${themeColor}`
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'
}
} }
} }
return link;
};
// Hash the name and map it to a dark pastel color
const generateDarkPastelBackgroundBy = (name) => { const generateDarkPastelBackgroundBy = (name) => {
// 1) Basic string hashing let hash = 0
let hash = 0;
for (let i = 0; i < name.length; i++) { for (let i = 0; i < name.length; i++) {
hash = (hash << 5) - hash + name.charCodeAt(i); hash = (hash << 5) - hash + name.charCodeAt(i)
hash |= 0; // Convert to 32-bit integer hash |= 0
} }
const safeHash = Math.abs(hash); const safeHash = Math.abs(hash)
const hueSteps = 69.69
// 2) Restrict hue to a 'blue-ish' range (150..270 = 120 degrees total) const hueIndex = safeHash % hueSteps
const hueRange = 288
const hueSteps = 69.69; const hue = 140 + (hueIndex * (hueRange / hueSteps))
const hueIndex = safeHash % hueSteps;
const hueRange = 288;
const hue = 140 + (hueIndex * (hueRange / hueSteps));
// 3) Satura­tion:
const satSteps = 13.69;
const satIndex = safeHash % satSteps;
const saturation = 18 + (satIndex * 1.333);
// 4) Lightness:
const lightSteps = 3.69;
const lightIndex = safeHash % lightSteps;
const lightness = 7 + lightIndex;
// 5) Return the HSL color string
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
};
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 ----------------------------------------------- // Create the overall Minter Card HTML -----------------------------------------------
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, BgColor) => { const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, BgColor) => {
const { header, content, links, creator, timestamp, poll } = cardData; const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString() const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
// const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const avatarHtml = await getMinterAvatar(creator) const avatarHtml = await getMinterAvatar(creator)
const linksHTML = links.map((link, index) => ` const linksHTML = links.map((link, index) => `
<button onclick="openModal('${link}')"> <button onclick="openLinksModal('${link}')">
${`Link ${index + 1} - ${link}`} ${`Link ${index + 1} - ${link}`}
</button> </button>
`).join(""); `).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')
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 calculatePollResults(pollResults, minterGroupMembers, minterAdmins)
await createModal()
return ` return `
<div class="minter-card" style="background-color: ${BgColor}"> <div class="minter-card" style="background-color: ${BgColor}">
<div class="minter-card-header"> <div class="minter-card-header">
@ -762,6 +968,10 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
</div> </div>
<div class="results-header support-header"><h5>CURRENT RESULTS</h5></div> <div class="results-header support-header"><h5>CURRENT RESULTS</h5></div>
<div class="minter-card-results"> <div class="minter-card-results">
<button onclick="togglePollDetails('${cardIdentifier}')">Display Poll Details</button>
<div id="poll-details-${cardIdentifier}" style="display: none;">
${detailsHtml}
</div>
<div class="admin-results"> <div class="admin-results">
<span class="admin-yes">Admin Yes: ${adminYes}</span> <span class="admin-yes">Admin Yes: ${adminYes}</span>
<span class="admin-no">Admin No: ${adminNo}</span> <span class="admin-no">Admin No: ${adminNo}</span>

View File

@ -147,6 +147,32 @@ const getUserAddress = async () => {
} }
} }
const getAddressInfo = async (address) => {
try {
const response = await fetch (`${baseUrl}/addresses/${address}`, {
headers: { 'Accept': 'application/json' },
method: 'GET',
})
const addressData = await response.json()
console.log(`address data:`,addressData)
return {
address: addressData.address,
reference: addressData.reference,
publicKey: addressData.publicKey,
defaultGroupId: addressData.defaultGroupId,
flags: addressData.flags,
level: addressData.level,
blocksMinted: addressData.blocksMinted,
blocksMintedAdjustment: addressData.blocksMintedAdjustment,
blocksMintedPenalty: addressData.blocksMintedPenalty
}
} catch(error){
console.error(error)
throw error
}
}
const fetchOwnerAddressFromName = async (name) => { const fetchOwnerAddressFromName = async (name) => {
console.log('fetchOwnerAddressFromName called') console.log('fetchOwnerAddressFromName called')
console.log('name:', name) console.log('name:', name)
@ -324,7 +350,7 @@ const login = async () => {
} }
} }
const getNamesFromAddress = async (address) => { const getNameFromAddress = async (address) => {
try { try {
const response = await fetch(`${baseUrl}/names/address/${address}?limit=20`, { const response = await fetch(`${baseUrl}/names/address/${address}?limit=20`, {
method: 'GET', method: 'GET',

View File

@ -68,7 +68,7 @@
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt=""> <img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
</a> </a>
</span> </span>
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.64b<br></a></span> <span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.65b<br></a></span>
</div> </div>
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul> <ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
@ -197,6 +197,23 @@
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6"> <section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
New Version + Features 12-28-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
New Version now includes full POLL RESULT DETAILS for every card. This includes each vote, the voter name, their weight (blocksMinted), and much more. Many additional code-cleanup changes were made as well. Also... The ARBITRARY REBUILD BOOTSTRAP should now be available, IF YOU ARE HAVING ISSUES SEEING DATA, PLEASE BOOTSTRAP YOUR NODE TO OBTAIN A REBUILT DATABASE. Thank you!</p>
</div>
</div>
</div>
</div>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-7 card"> <div class="col-12 col-lg-7 card">
@ -366,7 +383,7 @@
</div> </div>
<a class="link-wrap" href="#"> <a class="link-wrap" href="#">
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.64beta</p> <p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.65beta</p>
</a> </a>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">