2024-12-19 03:39:15 +00:00
// NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
2024-12-20 05:28:36 +00:00
const isEncryptedTestMode = true
const encryptedCardIdentifierPrefix = "test-MDC"
let isExistingEncryptedCard = false
let existingDecryptedCardData = { }
let existingEncryptedCardIdentifier = { }
2024-12-19 03:39:15 +00:00
let cardMinterName = { }
let existingCardMinterNames = [ ]
console . log ( "Attempting to load AdminBoard.js" ) ;
const loadAdminBoardPage = async ( ) => {
// Clear existing content on the page
const bodyChildren = document . body . children ;
for ( let i = bodyChildren . length - 1 ; i >= 0 ; i -- ) {
const child = bodyChildren [ i ] ;
if ( ! child . classList . contains ( "menu" ) ) {
child . remove ( ) ;
}
}
// Add the "Minter Board" content
const mainContent = document . createElement ( "div" ) ;
mainContent . innerHTML = `
< div class = "minter-board-main" style = "padding: 20px; text-align: center;" >
< h1 style = "color: lightblue;" > AdminBoard < / h 1 >
< p style = "font-size: 1.25em;" > The Admin Board is an encrypted card publishing board to keep track of minter data for the Minter Admins . Any Admin may publish a card , and related data , make comments on existing cards , and vote on existing card data in support or not of the name on the card . It is essentially a 'project management' tool to assist the Minter Admins in keeping track of the data related to minters they are adding / removing from the minter group . < / p >
< p > More functionality will be added over time . One of the first features will be the ability to output the existing card data 'decisions' , to a json formatted list in order to allow crowetic to run his script easily until the final Mintership proposal changes are completed , and the MINTER group is transferred to 'null' . < / p >
< button id = "publish-card-button" class = "publish-card-button" style = "margin: 20px; padding: 10px;" > Publish Encrypted Card < / b u t t o n >
< button id = "refresh-cards-button" class = "refresh-cards-button" style = "padding: 10px;" > Refresh Cards < / b u t t o n >
< div id = "encrypted-cards-container" class = "cards-container" style = "margin-top: 20px;" > < / d i v >
< 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 < / h 3 >
< label for = "minter-name-input" > Minter Name : < / l a b e l >
< input type = "text" id = "minter-name-input" maxlength = "100" placeholder = "Enter Minter's Name" required >
< label for = "card-header" > Header : < / l a b e l >
< input type = "text" id = "card-header" maxlength = "100" placeholder = "Explain main point/issue" required >
< label for = "card-content" > Content : < / l a b e l >
< textarea id = "card-content" placeholder = "Enter any information you like...You may also attach links to more in-depth information, etc." required > < / t e x t a r e a >
< label for = "card-links" > Links ( qortal : //...):</label>
< div id = "links-container" >
< input type = "text" class = "card-link" placeholder = "Enter QDN link" >
< / d i v >
< button type = "button" id = "add-link-button" > Add Another Link < / b u t t o n >
< button type = "submit" id = "submit-publish-button" > Publish Card < / b u t t o n >
< button type = "button" id = "cancel-publish-button" > Cancel < / b u t t o n >
< / f o r m >
< / d i v >
< / d i v >
` ;
document . body . appendChild ( mainContent ) ;
const publishCardButton = document . getElementById ( "publish-card-button" )
if ( publishCardButton ) {
publishCardButton . addEventListener ( "click" , async ( ) => {
const publishCardView = document . getElementById ( "publish-card-view" )
publishCardView . style . display = "flex"
document . getElementById ( "encrypted-cards-container" ) . style . display = "none"
} )
}
const refreshCardsButton = document . getElementById ( "refresh-cards-button" )
if ( refreshCardsButton ) {
refreshCardsButton . addEventListener ( "click" , async ( ) => {
const encryptedCardsContainer = document . getElementById ( "encrypted-cards-container" )
encryptedCardsContainer . innerHTML = "<p>Refreshing cards...</p>"
await fetchAllEncryptedCards ( )
} )
}
const cancelPublishButton = document . getElementById ( "cancel-publish-button" )
if ( cancelPublishButton ) {
cancelPublishButton . addEventListener ( "click" , async ( ) => {
const encryptedCardsContainer = document . getElementById ( "encrypted-cards-container" )
encryptedCardsContainer . style . display = "flex" ; // Restore visibility
const publishCardView = document . getElementById ( "publish-card-view" )
publishCardView . style . display = "none" ; // Hide the publish form
} )
}
const addLinkButton = document . getElementById ( "add-link-button" )
if ( addLinkButton ) {
addLinkButton . addEventListener ( "click" , async ( ) => {
const linksContainer = document . getElementById ( "links-container" )
const newLinkInput = document . createElement ( "input" )
newLinkInput . type = "text"
newLinkInput . className = "card-link"
newLinkInput . placeholder = "Enter QDN link"
linksContainer . appendChild ( newLinkInput )
} )
}
document . getElementById ( "publish-card-form" ) . addEventListener ( "submit" , async ( event ) => {
event . preventDefault ( ) ;
await publishEncryptedCard ( ) ;
} ) ;
2024-12-20 05:28:36 +00:00
// await fetchAndValidateAllAdminCards();
2024-12-19 03:39:15 +00:00
await fetchAllEncryptedCards ( ) ;
}
const extractCardsMinterName = ( cardIdentifier ) => {
// Ensure the identifier starts with the prefix
if ( ! cardIdentifier . startsWith ( ` ${ encryptedCardIdentifierPrefix } - ` ) ) {
throw new Error ( 'Invalid identifier format or prefix mismatch' ) ;
}
// Split the identifier into parts
const parts = cardIdentifier . split ( '-' ) ;
// Ensure the format has at least 3 parts
if ( parts . length < 3 ) {
throw new Error ( 'Invalid identifier format' ) ;
}
// Extract minterName (everything from the second part to the second-to-last part)
const minterName = parts . slice ( 2 , - 1 ) . join ( '-' ) ;
// Return the extracted minterName
return minterName ;
}
2024-12-20 05:28:36 +00:00
const processCards = async ( validEncryptedCards ) => {
const latestCardsMap = new Map ( )
2024-12-19 03:39:15 +00:00
2024-12-20 05:28:36 +00:00
// Step 1: Filter and keep the most recent card per identifier
validEncryptedCards . forEach ( card => {
const timestamp = card . updated || card . created || 0
const existingCard = latestCardsMap . get ( card . identifier )
2024-12-19 03:39:15 +00:00
2024-12-20 05:28:36 +00:00
if ( ! existingCard || timestamp > ( existingCard . updated || existingCard . created || 0 ) ) {
latestCardsMap . set ( card . identifier , card )
}
} )
2024-12-19 03:39:15 +00:00
2024-12-20 05:28:36 +00:00
// Step 2: Extract unique cards
const uniqueValidCards = Array . from ( latestCardsMap . values ( ) )
2024-12-19 03:39:15 +00:00
2024-12-20 05:28:36 +00:00
// Step 3: Group by minterName and select the most recent card per minterName
const minterNameMap = new Map ( )
2024-12-19 03:39:15 +00:00
2024-12-20 05:28:36 +00:00
for ( const card of validEncryptedCards ) {
const minterName = await extractCardsMinterName ( card . identifier )
const existingCard = minterNameMap . get ( minterName )
const cardTimestamp = card . updated || card . created || 0
const existingTimestamp = existingCard ? . updated || existingCard ? . created || 0
2024-12-19 03:39:15 +00:00
if ( ! existingCardMinterNames . includes ( minterName ) ) {
existingCardMinterNames . push ( minterName )
console . log ( ` cardsMinterName: ${ minterName } - added to list ` )
}
2024-12-20 05:28:36 +00:00
// Keep only the most recent card for each minterName
if ( ! existingCard || cardTimestamp > existingTimestamp ) {
minterNameMap . set ( minterName , card )
}
2024-12-19 03:39:15 +00:00
}
2024-12-20 05:28:36 +00:00
// 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
}
2024-12-19 03:39:15 +00:00
//Main function to load the Minter Cards ----------------------------------------
const fetchAllEncryptedCards = async ( ) => {
const encryptedCardsContainer = document . getElementById ( "encrypted-cards-container" ) ;
encryptedCardsContainer . innerHTML = "<p>Loading cards...</p>" ;
try {
const response = await qortalRequest ( {
action : "SEARCH_QDN_RESOURCES" ,
service : "MAIL_PRIVATE" ,
query : encryptedCardIdentifierPrefix ,
mode : "ALL"
} ) ;
if ( ! response || ! Array . isArray ( response ) || response . length === 0 ) {
encryptedCardsContainer . innerHTML = "<p>No cards found.</p>" ;
return ;
}
// Validate cards and filter
const validatedEncryptedCards = await Promise . all (
response . map ( async card => {
const isValid = await validateEncryptedCardIdentifier ( card ) ;
return isValid ? card : null ;
} )
) ;
const validEncryptedCards = validatedEncryptedCards . filter ( card => card !== null ) ;
2024-12-20 05:28:36 +00:00
2024-12-19 03:39:15 +00:00
if ( validEncryptedCards . length === 0 ) {
encryptedCardsContainer . innerHTML = "<p>No valid cards found.</p>" ;
return ;
}
2024-12-20 05:28:36 +00:00
const finalCards = await processCards ( validEncryptedCards )
2024-12-19 03:39:15 +00:00
// Display skeleton cards immediately
encryptedCardsContainer . innerHTML = "" ;
2024-12-20 05:28:36 +00:00
finalCards . forEach ( card => {
2024-12-19 03:39:15 +00:00
const skeletonHTML = createSkeletonCardHTML ( card . identifier ) ;
encryptedCardsContainer . insertAdjacentHTML ( "beforeend" , skeletonHTML ) ;
} ) ;
// Fetch and update each card
2024-12-20 05:28:36 +00:00
finalCards . forEach ( async card => {
2024-12-19 03:39:15 +00:00
try {
const cardDataResponse = await qortalRequest ( {
action : "FETCH_QDN_RESOURCE" ,
name : card . name ,
service : "MAIL_PRIVATE" ,
identifier : card . identifier ,
encoding : "base64"
} ) ;
if ( ! cardDataResponse ) {
console . warn ( ` Skipping invalid card: ${ JSON . stringify ( card ) } ` ) ;
removeSkeleton ( card . identifier ) ;
return ;
}
const decryptedCardData = await decryptAndParseObject ( cardDataResponse ) ;
// Skip cards without polls
if ( ! decryptedCardData . poll ) {
console . warn ( ` Skipping card with no poll: ${ card . identifier } ` ) ;
removeSkeleton ( card . identifier ) ;
return ;
}
// Fetch poll results
const pollResults = await fetchPollResults ( decryptedCardData . poll ) ;
const minterNameFromIdentifier = await extractCardsMinterName ( card . identifier ) ;
2024-12-21 06:07:18 +00:00
const encryptedCommentCount = await getEncryptedCommentCount ( card . identifier ) ;
2024-12-19 03:39:15 +00:00
// Generate final card HTML
2024-12-21 06:07:18 +00:00
const finalCardHTML = await createEncryptedCardHTML ( decryptedCardData , pollResults , card . identifier , encryptedCommentCount ) ;
2024-12-19 03:39:15 +00:00
replaceEncryptedSkeleton ( card . identifier , finalCardHTML ) ;
} catch ( error ) {
console . error ( ` Error processing card ${ card . identifier } : ` , error ) ;
removeEncryptedSkeleton ( card . identifier ) ; // Silently remove skeleton on error
}
} ) ;
} catch ( error ) {
console . error ( "Error loading cards:" , error ) ;
encryptedCardsContainer . innerHTML = "<p>Failed to load cards.</p>" ;
}
} ;
const removeEncryptedSkeleton = ( cardIdentifier ) => {
const encryptedSkeletonCard = document . getElementById ( ` skeleton- ${ cardIdentifier } ` ) ;
if ( encryptedSkeletonCard ) {
encryptedSkeletonCard . remove ( ) ; // Remove the skeleton silently
}
} ;
const replaceEncryptedSkeleton = ( cardIdentifier , htmlContent ) => {
const encryptedSkeletonCard = document . getElementById ( ` skeleton- ${ cardIdentifier } ` ) ;
if ( encryptedSkeletonCard ) {
encryptedSkeletonCard . outerHTML = htmlContent ;
}
} ;
// Function to create a skeleton card
const createEncryptedSkeletonCardHTML = ( cardIdentifier ) => {
return `
< div id = "skeleton-${cardIdentifier}" class = "skeleton-card" style = "padding: 10px; border: 1px solid gray; margin: 10px 0;" >
< div style = "display: flex; align-items: center;" >
< div style = "width: 50px; height: 50px; background-color: #ccc; border-radius: 50%;" > < / d i v >
< div style = "margin-left: 10px;" >
< div style = "width: 120px; height: 20px; background-color: #ccc; margin-bottom: 5px;" > < / d i v >
< div style = "width: 80px; height: 15px; background-color: #ddd;" > < / d i v >
< / d i v >
< / d i v >
< div style = "margin-top: 10px;" >
< div style = "width: 100%; height: 40px; background-color: #eee;" > < / d i v >
< / d i v >
< / d i v >
` ;
} ;
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
const fetchExistingEncryptedCard = async ( minterName ) => {
try {
// Step 1: Perform the search
const response = await qortalRequest ( {
action : "SEARCH_QDN_RESOURCES" ,
service : "MAIL_PRIVATE" ,
identifier : encryptedCardIdentifierPrefix ,
query : minterName ,
mode : "ALL" ,
} ) ;
console . log ( ` SEARCH_QDN_RESOURCES response: ${ JSON . stringify ( response , null , 2 ) } ` ) ;
// Step 2: Check if the response is an array and not empty
if ( ! response || ! Array . isArray ( response ) || response . length === 0 ) {
console . log ( "No cards found for the current user." ) ;
return null ;
}
// Step 3: Validate cards asynchronously
const validatedCards = await Promise . all (
response . map ( async card => {
const isValid = await validateEncryptedCardIdentifier ( card )
return isValid ? card : null ;
} )
) ;
// Step 4: Filter out invalid cards
const validCards = validatedCards . filter ( card => card !== null ) ;
if ( validCards . length > 0 ) {
// Step 5: Sort by most recent timestamp
const mostRecentCard = validCards . sort ( ( a , b ) => b . created - a . created ) [ 0 ] ;
// Step 6: Fetch full card data
const cardDataResponse = await qortalRequest ( {
action : "FETCH_QDN_RESOURCE" ,
name : mostRecentCard . name ,
service : mostRecentCard . service ,
identifier : mostRecentCard . identifier ,
encoding : "base64"
} ) ;
existingEncryptedCardIdentifier = mostRecentCard . identifier ;
existingDecryptedCardData = await decryptAndParseObject ( cardDataResponse )
console . log ( "Full card data fetched successfully:" , existingDecryptedCardData ) ;
return existingDecryptedCardData ;
}
console . log ( "No valid cards found." ) ;
return null ;
} catch ( error ) {
console . error ( "Error fetching existing card:" , error ) ;
return null ;
}
} ;
// Validate that a card is indeed a card and not a comment. -------------------------------------
const validateEncryptedCardIdentifier = async ( card ) => {
return (
typeof card === "object" &&
card . name &&
card . service === "MAIL_PRIVATE" &&
card . identifier && ! card . identifier . includes ( "comment" ) &&
card . created
) ;
}
// Load existing card data passed, into the form for editing -------------------------------------
const loadEncryptedCardIntoForm = async ( ) => {
if ( existingDecryptedCardData ) {
console . log ( "Loading existing card data:" , existingDecryptedCardData ) ;
document . getElementById ( "minter-name-input" ) . value = existingDecryptedCardData . minterName
document . getElementById ( "card-header" ) . value = existingDecryptedCardData . header
document . getElementById ( "card-content" ) . value = existingDecryptedCardData . content
const linksContainer = document . getElementById ( "links-container" ) ;
linksContainer . innerHTML = "" ; // Clear previous links
existingDecryptedCardData . links . forEach ( link => {
const linkInput = document . createElement ( "input" ) ;
linkInput . type = "text" ;
linkInput . className = "card-link" ;
linkInput . value = link ;
linksContainer . appendChild ( linkInput ) ;
} ) ;
}
}
const validateMinterName = async ( minterName ) => {
try {
const nameInfo = await getNameInfo ( minterName )
if ( ! nameInfo ) {
return error ( ` No NameInfo able to be obtained? Did you pass name? ` )
}
const name = nameInfo . name
return name
} catch ( error ) {
console . error ( ` extracting name from name info: ${ minterName } failed. ` , error )
}
}
// Main function to publish a new Minter Card -----------------------------------------------
const publishEncryptedCard = async ( ) => {
const minterNameInput = document . getElementById ( "minter-name-input" ) . value . trim ( ) ;
const header = document . getElementById ( "card-header" ) . value . trim ( ) ;
const content = document . getElementById ( "card-content" ) . value . trim ( ) ;
const links = Array . from ( document . querySelectorAll ( ".card-link" ) )
. map ( input => input . value . trim ( ) )
. filter ( link => link . startsWith ( "qortal://" ) ) ;
const publishedMinterName = await validateMinterName ( minterNameInput )
if ( ! header || ! content ) {
alert ( "Header and Content are required!" ) ;
return ;
}
if ( ! publishedMinterName ) {
alert ( ` Minter name invalid! Name input: ${ minterNameInput } - please check the name and try again! ` )
return ;
}
if ( ! isExistingEncryptedCard ) {
if ( existingCardMinterNames . includes ( publishedMinterName ) ) {
const updateCard = confirm ( ` Minter Name: ${ publishedMinterName } - CARD ALREADY EXISTS, you can update it (overwriting existing publish) or cancel... ` )
if ( updateCard ) {
await fetchExistingEncryptedCard ( publishedMinterName )
await loadEncryptedCardIntoForm ( )
isExistingEncryptedCard = true
return
} else {
return ;
}
}
}
const cardIdentifier = isExistingEncryptedCard ? existingEncryptedCardIdentifier : ` ${ encryptedCardIdentifierPrefix } - ${ publishedMinterName } - ${ await uid ( ) } ` ;
const pollName = ` ${ cardIdentifier } -poll ` ;
const pollDescription = ` Admin Board Poll Published By ${ userState . accountName } ` ;
const cardData = {
minterName : ` ${ publishedMinterName } ` ,
header ,
content ,
links ,
creator : userState . accountName ,
timestamp : Date . now ( ) ,
poll : pollName ,
} ;
try {
let base64CardData = await objectToBase64 ( cardData ) ;
if ( ! base64CardData ) {
console . log ( ` initial base64 object creation with objectToBase64 failed, using btoa... ` ) ;
base64CardData = btoa ( JSON . stringify ( cardData ) ) ;
}
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys ( )
adminPublicKeys = verifiedAdminPublicKeys
await qortalRequest ( {
action : "PUBLISH_QDN_RESOURCE" ,
name : userState . accountName ,
service : "MAIL_PRIVATE" ,
identifier : cardIdentifier ,
data64 : base64CardData ,
encrypt : true ,
publicKeys : verifiedAdminPublicKeys
} ) ;
if ( ! isExistingEncryptedCard ) {
await qortalRequest ( {
action : "CREATE_POLL" ,
pollName ,
pollDescription ,
pollOptions : [ 'Yes, No' ] ,
pollOwnerAddress : userState . accountAddress ,
} ) ;
alert ( "Card and poll published successfully!" ) ;
existingCardMinterNames . push ( ` ${ publishedMinterName } ` )
}
if ( isExistingEncryptedCard ) {
alert ( "Card Updated Successfully! (No poll updates are possible at this time...)" )
}
document . getElementById ( "publish-card-form" ) . reset ( ) ;
document . getElementById ( "publish-card-view" ) . style . display = "none" ;
document . getElementById ( "encrypted-cards-container" ) . style . display = "flex" ;
} catch ( error ) {
console . error ( "Error publishing card or poll:" , error ) ;
alert ( "Failed to publish card and poll." ) ;
}
}
2024-12-21 06:07:18 +00:00
const getEncryptedCommentCount = async ( cardIdentifier ) => {
2024-12-20 05:28:36 +00:00
try {
const response = await qortalRequest ( {
action : 'SEARCH_QDN_RESOURCES' ,
service : 'MAIL_PRIVATE' ,
query : ` comment- ${ cardIdentifier } ` ,
mode : "ALL"
} ) ;
// Just return the count; no need to decrypt each comment here
return Array . isArray ( response ) ? response . length : 0 ;
} catch ( error ) {
console . error ( ` Error fetching comment count for ${ cardIdentifier } : ` , error ) ;
return 0 ;
}
} ;
2024-12-19 03:39:15 +00:00
// Post a comment on a card. ---------------------------------
const postEncryptedComment = async ( cardIdentifier ) => {
const commentInput = document . getElementById ( ` new-comment- ${ cardIdentifier } ` ) ;
const commentText = commentInput . value . trim ( ) ;
if ( ! commentText ) {
alert ( 'Comment cannot be empty!' ) ;
return ;
}
2024-12-20 05:28:36 +00:00
const postTimestamp = Date . now ( )
2024-12-19 03:39:15 +00:00
console . log ( ` timestmp to be posted: ${ postTimestamp } ` )
const commentData = {
content : commentText ,
creator : userState . accountName ,
timestamp : postTimestamp ,
} ;
const commentIdentifier = ` comment- ${ cardIdentifier } - ${ await uid ( ) } ` ;
if ( ! Array . isArray ( adminPublicKeys ) || ( adminPublicKeys . length === 0 ) ) {
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys ( )
adminPublicKeys = verifiedAdminPublicKeys
}
try {
const base64CommentData = await objectToBase64 ( commentData ) ;
if ( ! base64CommentData ) {
console . log ( ` initial base64 object creation with objectToBase64 failed, using btoa... ` ) ;
base64CommentData = btoa ( JSON . stringify ( commentData ) ) ;
}
await qortalRequest ( {
action : "PUBLISH_QDN_RESOURCE" ,
name : userState . accountName ,
service : "MAIL_PRIVATE" ,
identifier : commentIdentifier ,
data64 : base64CommentData ,
encrypt : true ,
publicKeys : adminPublicKeys
} ) ;
alert ( 'Comment posted successfully!' ) ;
commentInput . value = '' ; // Clear input
} catch ( error ) {
console . error ( 'Error posting comment:' , error ) ;
alert ( 'Failed to post comment.' ) ;
}
} ;
//Fetch the comments for a card with passed card identifier ----------------------------
const fetchEncryptedComments = async ( cardIdentifier ) => {
try {
const response = await qortalRequest ( {
action : 'SEARCH_QDN_RESOURCES' ,
service : 'MAIL_PRIVATE' ,
query : ` comment- ${ cardIdentifier } ` ,
mode : "ALL"
} ) ;
return response ;
} catch ( error ) {
console . error ( ` Error fetching comments for ${ cardIdentifier } : ` , error ) ;
return [ ] ;
}
} ;
// display the comments on the card, with passed cardIdentifier to identify the card --------------
const displayEncryptedComments = async ( cardIdentifier ) => {
try {
const comments = await fetchEncryptedComments ( cardIdentifier ) ;
const commentsContainer = document . getElementById ( ` comments-container- ${ cardIdentifier } ` ) ;
// Fetch and display each comment
for ( const comment of comments ) {
const commentDataResponse = await qortalRequest ( {
action : "FETCH_QDN_RESOURCE" ,
name : comment . name ,
service : "MAIL_PRIVATE" ,
identifier : comment . identifier ,
encoding : "base64"
} ) ;
const decryptedCommentData = await decryptAndParseObject ( commentDataResponse )
2024-12-20 05:28:36 +00:00
const timestampCheck = comment . updated || comment . created || 0
const timestamp = await timestampToHumanReadableDate ( timestampCheck ) ;
2024-12-19 03:39:15 +00:00
//TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section.
const commentHTML = `
< div class = "comment" style = "border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;" >
< p > < strong > < u > $ { decryptedCommentData . creator } < / s t r o n g > : < / p > < / u >
< p > $ { decryptedCommentData . content } < / p >
< p > < i > $ { timestamp } < /p></i >
< / d i v >
` ;
commentsContainer . insertAdjacentHTML ( 'beforeend' , commentHTML ) ;
}
} catch ( error ) {
console . error ( ` Error displaying comments for ${ cardIdentifier } : ` , error ) ;
alert ( "Failed to load comments. Please try again." ) ;
}
} ;
const calculateAdminBoardPollResults = async ( pollData , minterGroupMembers , minterAdmins ) => {
const memberAddresses = minterGroupMembers . map ( member => member . member )
const minterAdminAddresses = minterAdmins . map ( member => member . member )
const adminGroupsMembers = await fetchAllAdminGroupsMembers ( )
const groupAdminAddresses = adminGroupsMembers . map ( member => member . member )
const adminAddresses = [ ] ;
adminAddresses . push ( ... minterAdminAddresses , ... groupAdminAddresses ) ;
let adminYes = 0 , adminNo = 0 , minterYes = 0 , minterNo = 0 , yesWeight = 0 , noWeight = 0
pollData . voteWeights . forEach ( weightData => {
if ( weightData . optionName === 'Yes' ) {
yesWeight = weightData . voteWeight
} else if ( weightData . optionName === 'No' ) {
noWeight = weightData . voteWeight
}
} )
for ( const vote of pollData . votes ) {
const voterAddress = await getAddressFromPublicKey ( vote . voterPublicKey )
console . log ( ` voter address: ${ voterAddress } ` )
if ( vote . optionIndex === 0 ) {
adminAddresses . includes ( voterAddress ) ? adminYes ++ : memberAddresses . includes ( voterAddress ) ? minterYes ++ : console . log ( ` voter ${ voterAddress } is not a minter nor an admin...Not including results... ` )
} else if ( vote . optionIndex === 1 ) {
adminAddresses . includes ( voterAddress ) ? adminNo ++ : memberAddresses . includes ( voterAddress ) ? minterNo ++ : console . log ( ` voter ${ voterAddress } is not a minter nor an admin...Not including results... ` )
}
}
// TODO - create a new function to calculate the weights of each voting MINTER only.
// This will give ALL weight whether voter is in minter group or not...
// until that is changed on the core we must calculate manually.
const totalYesWeight = yesWeight
const totalNoWeight = noWeight
const totalYes = adminYes + minterYes
const totalNo = adminNo + minterNo
return { adminYes , adminNo , minterYes , minterNo , totalYes , totalNo , totalYesWeight , totalNoWeight }
}
const toggleEncryptedComments = async ( cardIdentifier ) => {
const commentsSection = document . getElementById ( ` comments-section- ${ cardIdentifier } ` ) ;
if ( commentsSection . style . display === 'none' || ! commentsSection . style . display ) {
await displayEncryptedComments ( cardIdentifier ) ;
commentsSection . style . display = 'block' ;
} else {
commentsSection . style . display = 'none' ;
}
} ;
const createLinkDisplayModal = async ( ) => {
const modalHTML = `
< div id = "modal" style = "display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;" >
< div style = "position: relative; margin: 10% auto; width: 95%; height: 80%; background: white; border-radius: 10px; overflow: hidden;" >
< iframe id = "modalContent" src = "" style = "width: 100%; height: 100%; border: none;" > < / i f r a m e >
< button onclick = "closeLinkDisplayModal()" style = "position: absolute; top: 10px; right: 10px; background: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;" > Close < / b u t t o n >
< / d i v >
< / d i v >
` ;
document . body . insertAdjacentHTML ( 'beforeend' , modalHTML ) ;
}
// Function to open the modal
const openLinkDisplayModal = async ( link ) => {
const processedLink = await processQortalLinkForRendering ( link ) // Process the link to replace `qortal://` for rendering in modal
const modal = document . getElementById ( 'modal' ) ;
const modalContent = document . getElementById ( 'modalContent' ) ;
modalContent . src = processedLink ; // Set the iframe source to the link
modal . style . display = 'block' ; // Show the modal
}
// Function to close the modal
const closeLinkDisplayModal = async ( ) => {
const modal = document . getElementById ( 'modal' ) ;
const modalContent = document . getElementById ( 'modalContent' ) ;
modal . style . display = 'none' ; // Hide the modal
modalContent . src = '' ; // Clear the iframe source
}
2024-12-21 06:07:18 +00:00
// 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
// }
2024-12-19 03:39:15 +00:00
const processQortalLinkForRendering = async ( link ) => {
if ( link . startsWith ( 'qortal://' ) ) {
const match = link . match ( /^qortal:\/\/([^/]+)(\/.*)?$/ ) ;
if ( match ) {
2024-12-21 06:07:18 +00:00
const firstParam = match [ 1 ] . toUpperCase ( ) ;
const remainingPath = match [ 2 ] || "" ;
const themeColor = window . _qdnTheme || 'default' ; // Fallback to 'default' if undefined
// Simulating async operation if needed
await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
// Append theme as a query parameter
return ` /render/ ${ firstParam } ${ remainingPath } ?theme= ${ themeColor } ` ;
}
}
return link ;
} ;
async function getMinterAvatar ( minterName ) {
const avatarUrl = ` /arbitrary/THUMBNAIL/ ${ minterName } /qortal_avatar ` ;
try {
const response = await fetch ( avatarUrl , { method : 'HEAD' } ) ;
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;"> ` ;
} else {
// Avatar not found or no permission
return '' ;
2024-12-19 03:39:15 +00:00
}
2024-12-21 06:07:18 +00:00
} catch ( error ) {
console . error ( 'Error checking avatar availability:' , error ) ;
return '' ;
2024-12-19 03:39:15 +00:00
}
}
// Create the overall Minter Card HTML -----------------------------------------------
2024-12-20 05:28:36 +00:00
const createEncryptedCardHTML = async ( cardData , pollResults , cardIdentifier , commentCount ) => {
2024-12-19 03:39:15 +00:00
const { minterName , header , content , links , creator , timestamp , poll } = cardData ;
const formattedDate = new Date ( timestamp ) . toLocaleString ( ) ;
2024-12-21 06:07:18 +00:00
const minterAvatar = await getMinterAvatar ( minterName )
// const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const creatorAvatar = await getMinterAvatar ( creator )
2024-12-19 03:39:15 +00:00
const linksHTML = links . map ( ( link , index ) => `
< button onclick = "openLinkDisplayModal('${link}')" >
$ { ` Link ${ index + 1 } - ${ link } ` }
< / b u t t o n >
` ).join("");
const minterGroupMembers = await fetchMinterGroupMembers ( ) ;
const minterAdmins = await fetchMinterGroupAdmins ( ) ;
const { adminYes = 0 , adminNo = 0 , minterYes = 0 , minterNo = 0 , totalYes = 0 , totalNo = 0 , totalYesWeight = 0 , totalNoWeight = 0 } = await calculateAdminBoardPollResults ( pollResults , minterGroupMembers , minterAdmins )
await createModal ( )
return `
< div class = "admin-card" >
< div class = "minter-card-header" >
2024-12-21 06:07:18 +00:00
< h2 class = "support-header" > Created By : < / h 2 >
$ { creatorAvatar }
2024-12-19 03:39:15 +00:00
< h2 > $ { creator } < / h 2 >
2024-12-21 06:07:18 +00:00
< div class = "support-header" > < h5 > REGARDING : < / h 5 > < / d i v >
$ { minterAvatar }
2024-12-19 03:39:15 +00:00
< h3 > $ { minterName } < / h 3 >
< p > $ { header } < / p >
< / d i v >
< div class = "info" >
$ { content }
< / d i v >
2024-12-21 06:07:18 +00:00
< div class = "support-header" > < h5 > LINKS < / h 5 > < / d i v >
2024-12-19 03:39:15 +00:00
< div class = "info-links" >
$ { linksHTML }
< / d i v >
2024-12-21 06:07:18 +00:00
< div class = "results-header support-header" > < h5 > CURRENT RESULTS < / h 5 > < / d i v >
2024-12-19 03:39:15 +00:00
< div class = "minter-card-results" >
< div class = "admin-results" >
2024-12-21 06:07:18 +00:00
< span class = "admin-yes" > Admin Support : $ { adminYes } < / s p a n >
< span class = "admin-no" > Admin Against : $ { adminNo } < / s p a n >
2024-12-19 03:39:15 +00:00
< / d i v >
< div class = "minter-results" >
2024-12-21 06:07:18 +00:00
< span class = "minter-yes" > Supporting Weight $ { totalYesWeight } < / s p a n >
< span class = "minter-no" > Denial Weight $ { totalNoWeight } < / s p a n >
2024-12-19 03:39:15 +00:00
< / d i v >
< / d i v >
2024-12-21 06:07:18 +00:00
< div class = "support-header" > < h5 > SUPPORT or DENY < / h 5 > < h 5 s t y l e = " c o l o r : # f f a e 4 2 ; " > $ { m i n t e r N a m e } < / h 5 >
< p style = "color: #c7c7c7; font-size: .65rem; margin-top: 1vh" > ( click COMMENTS button to open / close card comments ) < / p >
< / d i v >
2024-12-19 03:39:15 +00:00
< div class = "actions" >
< div class = "actions-buttons" >
2024-12-21 06:07:18 +00:00
< button class = "yes" onclick = "voteYesOnPoll('${poll}')" > SUPPORT < / b u t t o n >
2024-12-20 05:28:36 +00:00
< button class = "comment" onclick = "toggleEncryptedComments('${cardIdentifier}')" > COMMENTS ( $ { commentCount } ) < / b u t t o n >
2024-12-21 06:07:18 +00:00
< button class = "no" onclick = "voteNoOnPoll('${poll}')" > OPPOSE < / b u t t o n >
2024-12-19 03:39:15 +00:00
< / d i v >
< / d i v >
< div id = "comments-section-${cardIdentifier}" class = "comments-section" style = "display: none; margin-top: 20px;" >
< div id = "comments-container-${cardIdentifier}" class = "comments-container" > < / d i v >
2024-12-21 06:07:18 +00:00
< textarea id = "new-comment-${cardIdentifier}" placeholder = "Input your comment..." style = "width: 100%; margin-top: 10px;" > < / t e x t a r e a >
2024-12-19 03:39:15 +00:00
< button onclick = "postEncryptedComment('${cardIdentifier}')" > Post Comment < / b u t t o n >
< / d i v >
2024-12-21 06:07:18 +00:00
< p style = "font-size: 0.75rem; margin-top: 1vh; color: #4496a1" > By : $ { creator } - $ { formattedDate } < / p >
2024-12-19 03:39:15 +00:00
< / d i v >
` ;
}