diff --git a/assets/css/forum-styles.css b/assets/css/forum-styles.css index 556064b..73144d9 100644 --- a/assets/css/forum-styles.css +++ b/assets/css/forum-styles.css @@ -515,6 +515,16 @@ body { align-items: center; } +.promotion-section{ + display: flex; + text-align: left; + padding: 0em; + flex-wrap: wrap; + align-content: flex-start; + justify-content: center; + align-items: center; +} + /* Form Heading */ .publish-card-view h3 { @@ -726,7 +736,7 @@ body { .minter-card-results div { display: flex; - justify-content: space-between; + justify-content: center; } .minter-card-results span { diff --git a/assets/images/hashes.json b/assets/images/hashes.json deleted file mode 100644 index df2094b..0000000 --- a/assets/images/hashes.json +++ /dev/null @@ -1 +0,0 @@ -{"Vn2IvHf07wyKYZiqFbKdyA==":"image15.jpg","tr8+8zJLW6gL2hgztHPxGA==":"qortector-by-itself-back-1136x940.png","1c5eh/fGPHPcoR//H+fMFw==":"qortector-by-itself-back-1-1136x940.png","1XXCnXt4wgUqJ31QK84ILg==":"qortector-by-itself-back-2-1136x940.png","0r3mBnBwn9G6GckwOiIF9w==":"qortector-by-itself-back-3-1136x940.png","fDUJaob7KfPxKBSLvIHo7A==":"wallpaper-qortal-darkest-2000x1333.jpg","byAqpaYKeHDBS3MDCvXHNA==":"background1.jpg","ZNDFbTjNPpnwXY6SJmH+Pg==":"image7.jpg","20tXYuAvQ8cCWSfVJ29Xog==":"image9.jpg","G0BdAlJezcIP2wV6CN9Y7A==":"image1.jpg","mAQyI8jPFlEmdmzmXlZibA==":"image14.jpg","rRdnwqn5O6lQx/lniiGUNA==":"mbr-1920x1280.jpg","sRD0qybUsOSHBp21gr2THw==":"mbr-1920x1152.jpg","OD73VTiBY7I+hZSg7Qndgg==":"modded-circle-2-new-100x100.png","nDMsvlHYt+gwcsRrGOKqzw==":"chd-circle-with-letters-100x100.png","KjibQCxRAQK0AAcPVGRyqA==":"qcloud-4-1920x1080.png","eNwLDioCZHd0wl3+/CkvSw==":"mbr-1623x1112.jpg","I6N1Z4qWlbHgBNW0yZdFqA==":"mbr-1623x1082.jpg","Oy2MMqCPK1Fdznz8QkHStw==":"again-edited-qortal-minting-icon-156x156.png","RMdq6M8IxyqkmFOKAv9xlQ==":"qortal_cube_original_by-100x100.png","Uh0/Evlsn5kppPdhSgz23w==":"qortal_ui_tray_minting-32x32.png","jM4Hh1L4b8PFO5ZreHRe8w==":"coin-815x815.jpg","tLev6c8MzefvKwbPhoSYuA==":"qortal-think-tank-logo-new-multiply-test-800x800.png"} \ No newline at end of file diff --git a/assets/js/AdminBoard.js b/assets/js/AdminBoard.js index 5c0afe9..15382ec 100644 --- a/assets/js/AdminBoard.js +++ b/assets/js/AdminBoard.js @@ -1,4 +1,4 @@ -// NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards. + const isEncryptedTestMode = false const encryptedCardIdentifierPrefix = "card-MAC" let isUpdateCard = false @@ -13,7 +13,7 @@ let adminPublicKeys = [] let kickTransactions = [] let banTransactions = [] let adminBoardState = { - kickedCards: new Set(), // store identifiers or addresses + kickedCards: new Set(), // store identifiers bannedCards: new Set(), // likewise hiddenList: new Set(), // user-hidden // ... we can add other things to state if needed... @@ -107,6 +107,7 @@ const loadAdminBoardPage = async () => { ` + document.body.appendChild(mainContent) const publishCardButton = document.getElementById("publish-card-button") @@ -149,13 +150,21 @@ const loadAdminBoardPage = async () => { }) } - document.getElementById('show-kicked-banned-checkbox')?.addEventListener('change', () => { - fetchAllEncryptedCards() - }) + const showKickedBannedCheckbox = document.getElementById('admin-show-kicked-banned-checkbox') - document.getElementById('show-admin-hidden-checkbox')?.addEventListener('change', () => { - fetchAllEncryptedCards() - }) + if (showKickedBannedCheckbox) { + showKickedBannedCheckbox.addEventListener('change', async (event) => { + await fetchAllEncryptedCards(true); + }) + } + + const showHiddenCardsCheckbox = document.getElementById('admin-show-hidden-checkbox') + if (showHiddenCardsCheckbox) { + showHiddenCardsCheckbox.addEventListener('change', async (event) => { + await fetchAllEncryptedCards(true) + }) + } + document.getElementById("publish-card-form").addEventListener("submit", async (event) => { event.preventDefault() @@ -172,10 +181,30 @@ const loadAdminBoardPage = async () => { } const fetchAllKicKBanTxData = async () => { - const kickTxType = "GROUP_KICK"; - const banTxType = "GROUP_BAN"; + const kickTxType = "GROUP_KICK" + const banTxType = "GROUP_BAN" - // 1) Fetch ban transactions + // Helper function to filter transactions + const filterTransactions = (rawTransactions) => { + // Group transactions by member + const memberTxMap = rawTransactions.reduce((map, tx) => { + if (!map[tx.member]) { + map[tx.member] = [] + } + map[tx.member].push(tx) + return map + }, {}) + + // Filter out members with both pending and non-pending transactions + return Object.values(memberTxMap) + .filter(txs => txs.every(tx => tx.approvalStatus !== 'PENDING')) + .flat() + // .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') && + // txs.some(tx => tx.approvalStatus !== 'PENDING'))) + // .flat() + } + + // Fetch ban transactions const rawBanTransactions = await searchTransactions({ txTypes: [banTxType], address: '', @@ -186,12 +215,13 @@ const fetchAllKicKBanTxData = async () => { startBlock: 1990000, blockLimit: 0, txGroupId: 0, - }); - // Filter out 'PENDING' - banTransactions = rawBanTransactions.filter((tx) => tx.approvalStatus !== 'PENDING'); - console.warn('banTxData (no PENDING):', banTransactions); + }) - // 2) Fetch kick transactions + // Filter transactions for bans + banTransactions = filterTransactions(rawBanTransactions) + console.warn('banTxData (filtered):', banTransactions) + + // Fetch kick transactions const rawKickTransactions = await searchTransactions({ txTypes: [kickTxType], address: '', @@ -202,12 +232,12 @@ const fetchAllKicKBanTxData = async () => { startBlock: 1990000, blockLimit: 0, txGroupId: 0, - }); - // Filter out 'PENDING' - kickTransactions = rawKickTransactions.filter((tx) => tx.approvalStatus !== 'PENDING'); - console.warn('kickTxData (no PENDING):', kickTransactions); -}; + }) + // Filter transactions for kicks + kickTransactions = filterTransactions(rawKickTransactions) + console.warn('kickTxData (filtered):', kickTransactions) +} // Example: fetch and save admin public keys and count diff --git a/assets/js/MinterBoard.js b/assets/js/MinterBoard.js index 7214594..fe7221e 100644 --- a/assets/js/MinterBoard.js +++ b/assets/js/MinterBoard.js @@ -1,11 +1,11 @@ // // NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards. const testMode = false -const cardIdentifierPrefix = "Minter-board-card" +const minterCardIdentifierPrefix = "Minter-board-card" let isExistingCard = false let existingCardData = {} let existingCardIdentifier = {} const MIN_ADMIN_YES_VOTES = 9; -const GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT = 9999950 //TODO update this to correct featureTrigger height when known, either that, or pull from core. +const GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT = 2012800 //TODO update this to correct featureTrigger height when known, either that, or pull from core. let featureTriggerPassed = false let isApproved = false @@ -54,7 +54,7 @@ const loadMinterBoardPage = async () => { document.getElementById("publish-card-button").addEventListener("click", async () => { try { - const fetchedCard = await fetchExistingCard() + const fetchedCard = await fetchExistingCard(minterCardIdentifierPrefix) if (fetchedCard) { // An existing card is found if (testMode) { @@ -84,7 +84,7 @@ const loadMinterBoardPage = async () => { // Show the form const publishCardView = document.getElementById("publish-card-view") - publishCardView.style.display = "flex"; + publishCardView.style.display = "flex" document.getElementById("cards-container").style.display = "none" } catch (error) { console.error("Error checking for existing card:", error) @@ -95,7 +95,7 @@ const loadMinterBoardPage = async () => { document.getElementById("refresh-cards-button").addEventListener("click", async () => { const cardsContainer = document.getElementById("cards-container") cardsContainer.innerHTML = "

Refreshing cards...

" - await loadCards(); + await loadCards(minterCardIdentifierPrefix) }) @@ -117,17 +117,17 @@ const loadMinterBoardPage = async () => { document.getElementById("publish-card-form").addEventListener("submit", async (event) => { event.preventDefault() - await publishCard() + await publishCard(minterCardIdentifierPrefix) }) await featureTriggerCheck() - await loadCards() + await loadCards(minterCardIdentifierPrefix) } const extractMinterCardsMinterName = async (cardIdentifier) => { // Ensure the identifier starts with the prefix - if (!cardIdentifier.startsWith(`${cardIdentifierPrefix}-`)) { - throw new Error('Invalid identifier format or prefix mismatch') - } + if ((!cardIdentifier.startsWith(minterCardIdentifierPrefix)) && (!cardIdentifier.startsWith(addRemoveIdentifierPrefix))) { + throw new Error('minterCard does not match identifier check') + } // Split the identifier into parts const parts = cardIdentifier.split('-') // Ensure the format has at least 3 parts @@ -135,9 +135,22 @@ const extractMinterCardsMinterName = async (cardIdentifier) => { throw new Error('Invalid identifier format') } try { - const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1) - const minterName = await searchSimpleResults.name - return minterName + if (cardIdentifier.startsWith(minterCardIdentifierPrefix)){ + const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1) + const minterName = await searchSimpleResults.name + return minterName + } else if (cardIdentifier.startsWith(addRemoveIdentifierPrefix)) { + const searchSimpleResults = await searchSimple('BLOG_POST', `${cardIdentifier}`, '', 1) + const publisherName = searchSimpleResults.name + const cardDataResponse = await qortalRequest({ + action: "FETCH_QDN_RESOURCE", + name: publisherName, + service: "BLOG_POST", + identifier: cardIdentifier, + }) + const minterName = cardDataResponse.minterName + return minterName + } } catch (error) { throw error } @@ -146,6 +159,7 @@ const extractMinterCardsMinterName = async (cardIdentifier) => { const processMinterCards = async (validMinterCards) => { const latestCardsMap = new Map() + // Deduplicate by identifier, keeping the most recent validMinterCards.forEach(card => { const timestamp = card.updated || card.created || 0 const existingCard = latestCardsMap.get(card.identifier) @@ -155,28 +169,54 @@ const processMinterCards = async (validMinterCards) => { } }) + // Convert Map back to array + const uniqueValidCards = Array.from(latestCardsMap.values()) + const minterGroupMembers = await fetchMinterGroupMembers() const minterGroupAddresses = minterGroupMembers.map(m => m.member) const minterNameMap = new Map() - for (const card of validMinterCards) { - const minterName = await extractMinterCardsMinterName(card.identifier) + // For each card, extract minterName safely + for (const card of uniqueValidCards) { + let minterName + try { + // If this throws, we catch below and skip + minterName = await extractMinterCardsMinterName(card.identifier) + } catch (error) { + console.warn( + `Skipping card ${card.identifier} because extractMinterCardsMinterName failed:`, + error + ) + continue // Skip this card and move on + } + console.log(`minterName`, minterName) + // Next, get minterNameInfo const minterNameInfo = await getNameInfo(minterName) if (!minterNameInfo) { - console.warn(`minterNameInfo is null for minter: ${minterName}`) - continue - } - const minterAddress = await minterNameInfo.owner - - if (!minterAddress) { - console.warn(`minterAddress is FAKE or INVALID in some way! minter: ${minterName}`) - continue - } else if (minterGroupAddresses.includes(minterAddress)){ - console.log(`existing minter FOUND and/or FAKE NAME FOUND (if following is null then fake name: ${minterAddress}), not including minter card: ${card.identifier}`) + console.warn(`minterNameInfo is null for minter: ${minterName}, skipping card.`) continue } + const minterAddress = minterNameInfo.owner + // Validate the address + const addressValid = await getAddressInfo(minterAddress) + if (!minterAddress || !addressValid) { + console.warn(`minterAddress invalid or missing for: ${minterName}, skipping card.`, minterAddress) + continue + } + + // If this is a 'regular' minter card, skip if user is already a minter + if (!card.identifier.includes('QM-AR-card')) { + if (minterGroupAddresses.includes(minterAddress)) { + console.log( + `existing minter found or fake name detected. Not including minter card: ${card.identifier}` + ) + continue + } + } + + // Keep only the most recent card for each minterName const existingCard = minterNameMap.get(minterName) const cardTimestamp = card.updated || card.created || 0 const existingTimestamp = existingCard?.updated || existingCard?.created || 0 @@ -186,6 +226,7 @@ const processMinterCards = async (validMinterCards) => { } } + // Convert minterNameMap to final array const finalCards = [] const seenMinterNames = new Set() @@ -196,6 +237,7 @@ const processMinterCards = async (validMinterCards) => { } } + // Sort by timestamp descending finalCards.sort((a, b) => { const timestampA = a.updated || a.created || 0 const timestampB = b.updated || b.created || 0 @@ -205,35 +247,37 @@ const processMinterCards = async (validMinterCards) => { return finalCards } + //Main function to load the Minter Cards ---------------------------------------- -const loadCards = async () => { +const loadCards = async (cardIdentifierPrefix) => { const cardsContainer = document.getElementById("cards-container") + let isARBoard = false cardsContainer.innerHTML = "

Loading cards...

" + if ((cardIdentifierPrefix.startsWith(`QM-AR-card`))) { + isARBoard = true + console.warn(`ARBoard determined:`, isARBoard) + } try { - const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, '' , 0) if (!response || !Array.isArray(response) || response.length === 0) { cardsContainer.innerHTML = "

No cards found.

" - return; + return } - // Validate cards and filter const validatedCards = await Promise.all( response.map(async card => { const isValid = await validateCardStructure(card) return isValid ? card : null }) - ); - + ) const validCards = validatedCards.filter(card => card !== null) if (validCards.length === 0) { cardsContainer.innerHTML = "

No valid cards found.

" return } - const finalCards = await processMinterCards(validCards) // Display skeleton cards immediately @@ -264,7 +308,6 @@ const loadCards = async () => { removeSkeleton(card.identifier) return } - const pollPublisherPublicKey = await getPollPublisherPublicKey(cardDataResponse.poll) const cardPublisherPublicKey = await getPublicKeyByName(card.name) @@ -273,14 +316,39 @@ const loadCards = async () => { removeSkeleton(card.identifier) return } - const pollResults = await fetchPollResults(cardDataResponse.poll) const bgColor = generateDarkPastelBackgroundBy(card.name) const commentCount = await countComments(card.identifier) const cardUpdatedTime = card.updated || null - const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor) - + + if (isARBoard) { + const name = await getNameInfo(cardDataResponse.minterName) + const address = name.owner + if (minterAdminAddresses && minterGroupAddresses) { + if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) { + console.warn(`Found card from ARBoard that contained a non-minter!`) + removeSkeleton(card.identifier) + return + } + } else if (!minterAdminAddresses || !minterGroupAddresses){ + const minterGroup = await fetchMinterGroupMembers() + const adminGroup = await fetchMinterGroupAdmins() + minterAdminAddresses = adminGroup.map(m => m.member) + minterGroupAddresses = minterGroup.map(m => m.member) + if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) { + console.warn(`Found card from ARBoard that contained a non-minter!`) + removeSkeleton(card.identifier) + return + } + } + } + + const finalCardHTML = isARBoard ? // If we're calling from the ARBoard, we will create HTML with a different function. + await createARCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor) + : + await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor) replaceSkeleton(card.identifier, finalCardHTML) + } catch (error) { console.error(`Error processing card ${card.identifier}:`, error) removeSkeleton(card.identifier) @@ -326,7 +394,7 @@ const createSkeletonCardHTML = (cardIdentifier) => { } // Function to check and fech an existing Minter Card if attempting to publish twice ---------------------------------------- -const fetchExistingCard = async () => { +const fetchExistingCard = async (cardIdentifierPrefix) => { try { const response = await searchSimple('BLOG_POST', `${cardIdentifierPrefix}`, `${userState.accountName}`, 0, 0, '', true) @@ -344,7 +412,6 @@ const fetchExistingCard = async () => { service: "BLOG_POST", identifier: mostRecentCard.identifier }) - existingCardIdentifier = mostRecentCard.identifier existingCardData = cardDataResponse isExistingCard = true @@ -418,7 +485,7 @@ const loadCardIntoForm = async (cardData) => { } // Main function to publish a new Minter Card ----------------------------------------------- -const publishCard = async () => { +const publishCard = async (cardIdentifierPrefix) => { const minterGroupData = await fetchMinterGroupMembers() const minterGroupAddresses = minterGroupData.map(m => m.member) @@ -486,7 +553,7 @@ const publishCard = async () => { document.getElementById("publish-card-form").reset() document.getElementById("publish-card-view").style.display = "none" document.getElementById("cards-container").style.display = "flex" - await loadCards() + await loadCards(minterCardIdentifierPrefix) } catch (error) { @@ -1084,44 +1151,75 @@ const featureTriggerCheck = async () => { } const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => { + + if (!userState.isMinterAdmin){ + console.warn(`User is NOT an admin, not displaying invite/approve button...`) + return null + } + const isBlockPassed = await featureTriggerCheck() - let minAdminCount const minterAdmins = await fetchMinterGroupAdmins() - if (!isBlockPassed){ - console.warn(`feature trigger not passed, using static number for minAdminCount`) - minAdminCount = 9 - } - - if ((minterAdmins) && (minterAdmins.length === 1)){ - console.warn(`simply a double-check that there is only one MINTER group admin, in which case the group hasn't been transferred to null...keeping default minAdminCount of: ${minAdminCount}`) - } else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){ - const totalAdmins = minterAdmins.length - const fortyPercent = totalAdmins * 0.40 - minAdminCount = Math.round(fortyPercent) - console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`) - } - + let minAdminCount = 9 if (isBlockPassed) { - const minterNameInfo = await getNameInfo(creator) - const minterAddress = await minterNameInfo.owner - if (userState.isMinterAdmin){ - let groupApprovalHtml = await checkGroupApprovalAndCreateButton(minterAddress, cardIdentifier, "GROUP_INVITE") - if (groupApprovalHtml) { - return groupApprovalHtml - } - }else{ - console.log(`USER NOT ADMIN, no need for group approval buttons...`) + minAdminCount = Math.round(minterAdmins.length * 0.4) + console.warn(`Using 40% => ${minAdminCount}`) + } + + if (adminYes < minAdminCount) { + console.warn(`Admin votes not high enough (have=${adminYes}, need=${minAdminCount}). No button.`) + return null + } + console.log(`passed initial button creation checks, pulling additional data...`) + + const minterNameInfo = await getNameInfo(creator) + const minterAddress = await minterNameInfo.owner + + const previousBanTx = await searchTransactions({ + txTypes: ['GROUP_BAN'], + address: `${minterAddress}`, + confirmationStatus: 'CONFIRMED', + limit: 0, + reverse: true, + offset: 0, + startBlock: 1990000, + blockLimit: 0, + txGroupId: 0, + }) + const previousBan = previousBanTx.filter((tx) => tx.approvalStatus !== 'PENDING') + const previousKickTx = await searchTransactions({ + txTypes: ['GROUP_KICK'], + address: `${minterAddress}`, + confirmationStatus: 'CONFIRMED', + limit: 0, + reverse: true, + offset: 0, + startBlock: 1990000, + blockLimit: 0, + txGroupId: 0, + }) + const previousKick = previousKickTx.filter((tx) => tx.approvalStatus !== 'PENDING') + const priorBanOrKick = (previousKick.length > 0 || previousBan.length > 0) + + console.warn(`PriorBanOrKick determination:`, priorBanOrKick) + + const inviteButtonHtml = createInviteButtonHtml(creator, cardIdentifier) + const groupApprovalHtml = await checkGroupApprovalAndCreateButton(minterAddress, cardIdentifier, "GROUP_INVITE") + + if (!priorBanOrKick) { + console.log(`No prior kick/ban found, creating invite (or approve) button...` ) + console.warn(`Existing Numbers - adminYes/minAdminCount: ${adminYes}/${minAdminCount}`) + if (groupApprovalHtml){ + console.warn(`groupApprovalCheck found existing groupApproval, returning approval button instead of invite button...`) + return groupApprovalHtml } - } - - if (adminYes >= minAdminCount && (userState.isMinterAdmin)) { - const inviteButtonHtml = createInviteButtonHtml(creator, cardIdentifier) - console.log(`admin votes over 9, creating invite button...`, adminYes) + console.warn(`No pending approvals or prior kick/ban found, but votes are high enough, returning invite button...`) return inviteButtonHtml - } - return null + } else if (priorBanOrKick){ + console.warn(`Prior kick/ban found! Including BOTH buttons (due to complexities in checking, displaying both buttons is simpler than attempting to display only one)...`) + return inviteButtonHtml + groupApprovalHtml + } } const findPendingApprovalTxForAddress = async (address, txType, limit = 0, offset = 0) => { @@ -1132,17 +1230,12 @@ const findPendingApprovalTxForAddress = async (address, txType, limit = 0, offse if (txType) { relevantTypes = new Set([txType]) } else { - relevantTypes = new Set(["GROUP_INVITE", "GROUP_BAN", "GROUP_KICK"]) + relevantTypes = new Set(["GROUP_INVITE", "GROUP_BAN", "GROUP_KICK", "ADD_GROUP_ADMIN", "REMOVE_GROUP_ADMIN"]) } // Filter pending TX for relevant types const relevantTxs = pendingTxs.filter((tx) => relevantTypes.has(tx.type)) - // Further filter by whether 'address' matches the correct field - // - GROUP_INVITE => invitee - // - GROUP_BAN => offender - // - GROUP_KICK => member - // If the user passed a specific txType, only one branch might matter. const matchedTxs = relevantTxs.filter((tx) => { switch (tx.type) { case "GROUP_INVITE": @@ -1151,33 +1244,22 @@ const findPendingApprovalTxForAddress = async (address, txType, limit = 0, offse return tx.offender === address case "GROUP_KICK": return tx.member === address + case "ADD_GROUP_ADMIN": + return tx.member === address + case "REMOVE_GROUP_ADMIN": + return tx.admin === address default: return false } }) + console.warn(`matchedTxs:`,matchedTxs) return matchedTxs // Array of matching pending transactions } const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transactionType) => { - const txTypes = [transactionType] - - const txSearchResults = await searchTransactions({ - txTypes, - address: `${address}`, - confirmationStatus: 'CONFIRMED', - limit: 0, - reverse: true, - offset: 0, - startBlock: 1990000, - blockLimit: 0, - txGroupId: 694 - }) - - const approvalTxType = ['GROUP_APPROVAL'] const approvalSearchResults = await searchTransactions({ - txTypes: approvalTxType, - address: `${address}`, + txTypes: ['GROUP_APPROVAL'], confirmationStatus: 'CONFIRMED', limit: 0, reverse: true, @@ -1186,73 +1268,246 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa blockLimit: 0, txGroupId: 0 }) + const pendingApprovals = await findPendingApprovalTxForAddress(address, transactionType); - console.warn(`transaction search results, this is for comparison to pendingApprovals search, these are not used:`,txSearchResults) - const pendingApprovals = await findPendingApprovalTxForAddress(address, transactionType) - - if (pendingApprovals) { - console.warn(`this is what is used for pending results... pendingApprovals FOUND:`, pendingApprovals) + // If no pending transaction found, return null + if (!pendingApprovals || pendingApprovals.length === 0) { + console.warn("no pending approval transactions found, returning null...") + return null; } - - if ((pendingApprovals.length === 0) || (!pendingApprovals)) { - console.warn(`no pending approval transactions found, returning null...`) - return null - } - const existingApprovalCount = approvalSearchResults.length const txSig = pendingApprovals[0].signature + // Among the already-confirmed GROUP_APPROVAL, filter for those referencing this txSig + const relevantApprovals = approvalSearchResults.filter( + (approvalTx) => approvalTx.pendingSignature === txSig + ) + const { tableHtml, uniqueApprovalCount } = await buildApprovalTableHtml( + relevantApprovals, + getNameFromAddress + ) - if (transactionType === `GROUP_INVITE`){ - + if (transactionType === "GROUP_INVITE") { const approvalButtonHtml = ` -
-

Existing Invite Approvals: ${existingApprovalCount}

- -
+
+

+ Existing ${transactionType} Approvals: ${uniqueApprovalCount} +

+ ${tableHtml} +
+ +
+
` return approvalButtonHtml } - - if (transactionType === `GROUP_KICK`){ - + + if (transactionType === "GROUP_KICK") { const approvalButtonHtml = ` -
-

Existing Kick Approvals: ${existingApprovalCount}

- -
+
+

+ Existing ${transactionType} Approvals: ${uniqueApprovalCount} +

+ ${tableHtml} +
+ +
+
` return approvalButtonHtml } - - if (transactionType === `GROUP_BAN`){ - + + if (transactionType === "GROUP_BAN") { const approvalButtonHtml = ` -
-

Existing Ban Approvals: ${existingApprovalCount}

- -
+
+

+ Existing ${transactionType} Approvals: ${uniqueApprovalCount} +

+ ${tableHtml} +
+ +
+
` return approvalButtonHtml } + + if (transactionType === "ADD_GROUP_ADMIN") { + const approvalButtonHtml = ` +
+

+ Existing ${transactionType} Approvals: ${uniqueApprovalCount} +

+ ${tableHtml} +
+ +
+
+ ` + return approvalButtonHtml + } + + if (transactionType === "REMOVE_GROUP_ADMIN") { + const approvalButtonHtml = ` +
+

+ Existing ${transactionType} Approvals: ${uniqueApprovalCount} +

+ ${tableHtml} +
+ +
+
+ ` + return approvalButtonHtml + } + +} + +async function buildApprovalTableHtml(approvalTxs, getNameFunc) { + // Build a Map of adminAddress => one transaction (to handle multiple approvals from same admin) + const approvalMap = new Map() + for (const tx of approvalTxs) { + const adminAddr = tx.creatorAddress + if (!approvalMap.has(adminAddr)) { + approvalMap.set(adminAddr, tx) + } + } + + // Turn the map into an array for iteration + const approvalArray = Array.from(approvalMap, ([adminAddr, tx]) => ({ adminAddr, tx })) + + // Build table rows asynchronously, since we need getNameFromAddress + const tableRows = await Promise.all( + approvalArray.map(async ({ adminAddr, tx }) => { + let adminName + try { + adminName = await getNameFunc(adminAddr) + } catch (err) { + console.warn(`Error fetching name for ${adminAddr}:`, err) + adminName = null + } + + const displayName = + adminName && adminName !== adminAddr + ? adminName + : "(No registered name)" + + // Format the transaction timestamp + const dateStr = new Date(tx.timestamp).toLocaleString() + + return ` + + ${displayName} + ${dateStr} + + ` + }) + ) + + // The total unique approvals = number of entries in approvalMap + const uniqueApprovalCount = approvalMap.size; + + // 4) Wrap the table in a container with horizontal scroll: + // 1) max-width: 100% makes it fit the parent (card) width + // 2) overflow-x: auto allows scrolling if the table is too wide + const containerHtml = ` +
+ + + + + + + + + ${tableRows.join("")} + +
Admin NameApproval Time
+
+ ` + + // Return both the container-wrapped table and the count of unique approvals + return { + tableHtml: containerHtml, + uniqueApprovalCount + } } + const handleGroupApproval = async (pendingSignature) => { try{ if (!userState.isMinterAdmin) { diff --git a/assets/js/Q-Mintership.js b/assets/js/Q-Mintership.js index dd90e5e..340da78 100644 --- a/assets/js/Q-Mintership.js +++ b/assets/js/Q-Mintership.js @@ -56,7 +56,7 @@ function loadMessagesFromLocalStorage() { console.log(`Loaded ${messageOrder.length} messages from localStorage.`) } } catch (error) { - console.error("Error loading messages from localStorage:", error); + console.error("Error loading messages from localStorage:", error) } } @@ -98,6 +98,23 @@ document.addEventListener("DOMContentLoaded", async () => { }) }) + const addRemoveAdminLinks = document.querySelectorAll('a[href="ADDREMOVEADMIN"]') + addRemoveAdminLinks.forEach(link => { + link.addEventListener('click', async (event) => { + event.preventDefault() + // Possibly require user to login if not logged + if (!userState.isLoggedIn) { + await login() + } + if (typeof loadMinterBoardPage === "undefined") { + console.log("loadMinterBoardPage not found, loading script dynamically...") + await loadScript("./assets/js/MinterBoard.js") + } + await loadAddRemoveAdminPage() + }) + }) + + // --- ADMIN CHECK --- await verifyUserIsAdmin() @@ -116,7 +133,7 @@ document.addEventListener("DOMContentLoaded", async () => { } } - if (userState.isAdmin) { + if (userState.isAdmin || userState.isForumAdmin || userState.isMinterAdmin) { console.log(`User is an Admin. Admin-specific buttons will remain visible.`) // DATA-BOARD Links for Admins @@ -233,7 +250,7 @@ const loadForumPage = async () => { ` - document.body.appendChild(mainContent); + document.body.appendChild(mainContent) // Add event listeners to room buttons document.getElementById("minters-room").addEventListener("click", () => { diff --git a/assets/js/QortalApi.js b/assets/js/QortalApi.js index 0c9ac77..ac34c6e 100644 --- a/assets/js/QortalApi.js +++ b/assets/js/QortalApi.js @@ -4,6 +4,7 @@ let adminGroupIDs = ["721", "1", "673"] // Settings to allow non-devmode development with 'live-server' module let baseUrl = '' let isOutsideOfUiDevelopment = false +let nullAddress = 'QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG' if (typeof qortalRequest === 'function') { console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.') @@ -194,6 +195,13 @@ const userState = { isForumAdmin: false } +const validateQortalAddress = async (address) => { + // Regular expression to match Qortal addresses + const qortalAddressRegex = /^Q[a-zA-Z0-9]{32}$/ + // Test the address against the regex + return qortalAddressRegex.test(address) +} + // USER-RELATED QORTAL CALLS ------------------------------------------ // Obtain the address of the authenticated user checking userState.accountAddress first. const getUserAddress = async () => { @@ -1458,6 +1466,120 @@ const createGroupKickTransaction = async (recipientAddress, adminPublicKey, grou } } +const createAddGroupAdminTransaction = async (ownerPublicKey, groupId=694, member, txGroupId, fee) => { +// If utilized to create a GROUP_APPROVAL tx, for MINTER group, then 'txCreatorPublicKey' takes the place of 'ownerPublicKey', and 'txGroupId' is required. Otherwise, txGroupId is 0 and ownerPublicKey is the tx creator, as creator = owner. + try { + + let reference + + if (!ownerPublicKey){ + console.warn(`ownerPublicKey not passed, obtaining user public key...`) + const info = await getAddressInfo(userState.accountAddress) + reference = info.reference + ownerPublicKey = info.publicKey + }else { + // Fetch account reference correctly + const addr = await getAddressFromPublicKey(ownerPublicKey) + const accountInfo = await getAddressInfo(addr) + reference = accountInfo.reference + } + + // Validate inputs before making the request + if (!ownerPublicKey || !reference) { + throw new Error("Missing required parameters for group invite transaction.") + } + + const payload = { + timestamp: Date.now(), + reference, + fee, + txGroupId, + ownerPublicKey, + groupId, + member + } + console.log("Sending ADD_GROUP_ADMIN transaction payload:", payload) + const response = await fetch(`${baseUrl}/groups/addadmin`, { + method: 'POST', + headers: { + 'Accept': 'text/plain', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to create transaction: ${response.status}, ${errorText}`) + } + const rawTransaction = await response.text() + console.log("Raw unsigned transaction created:", rawTransaction) + return rawTransaction + + } catch (error) { + console.error("Error creating ADD_GROUP_ADMIN transaction:", error) + throw error + } +} + +const createRemoveGroupAdminTransaction = async (ownerPublicKey, groupId=694, admin, txGroupId, fee) => { + console.log(`removeGroupAdminTxCreationInfo:`,ownerPublicKey, groupId, fee, txGroupId, admin) + + try { + let reference + + if (!ownerPublicKey){ + console.warn(`ownerPublicKey not passed, obtaining user public key...`) + const info = getAddressInfo(userState.accountAddress) + reference = info.reference + ownerPublicKey = info.publicKey + } else { + // Fetch account reference correctly + const addr = await getAddressFromPublicKey(ownerPublicKey) + const accountInfo = await getAddressInfo(addr) + reference = accountInfo.reference + console.warn(`reference for removeTx:`, reference) + console.warn(`ownerPublicKey for removeTx`, ownerPublicKey) + } + + // Validate inputs before making the request + if (!ownerPublicKey || !reference) { + throw new Error("Missing required parameters for transaction.") + } + + const payload = { + timestamp: Date.now(), + reference, + fee, + txGroupId, + ownerPublicKey, + groupId, + admin, + } + console.log("Sending REMOVE_GROUP_ADMINtransaction payload:", payload) + const response = await fetch(`${baseUrl}/groups/removeadmin`, { + method: 'POST', + headers: { + 'Accept': 'text/plain', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to create transaction: ${response.status}, ${errorText}`) + } + const rawTransaction = await response.text() + console.log("Raw unsigned transaction created:", rawTransaction) + return rawTransaction + + } catch (error) { + console.error("Error creating REMOVE_GROUP_ADMIN transaction:", error) + throw error + } +} + const createGroupApprovalTransaction = async (adminPublicKey, pendingSignature, txGroupId=0, fee=0.01) => { try { @@ -1468,7 +1590,7 @@ const createGroupApprovalTransaction = async (adminPublicKey, pendingSignature, // Validate inputs before making the request if (!adminPublicKey || !accountReference ) { - throw new Error("Missing required parameters for group invite transaction.") + throw new Error("Missing required parameters for transaction.") } const payload = { @@ -1771,33 +1893,3 @@ const searchPendingTransactions = async (limit = 20, offset = 0) => { } } - - - - - -// export { -// userState, -// adminGroups, -// searchResourcesWithMetadata, -// searchResourcesWithStatus, -// getResourceMetadata, -// renderData, -// getProductDetails, -// getUserGroups, -// getUserAddress, -// login, -// timestampToHumanReadableDate, -// base64EncodeString, -// verifyUserIsAdmin, -// fetchAllDataByIdentifier, -// fetchOwnerAddressFromName, -// verifyAddressIsAdmin, -// uid, -// fetchAllGroups, -// getNameInfo, -// publishMultipleResources, -// getPublicKeyByName, -// objectToBase64, -// fetchMinterGroupAdmins -// } diff --git a/index.html b/index.html index 138c604..3913b26 100644 --- a/index.html +++ b/index.html @@ -1,37 +1,34 @@ - - - - - - - - - - - Home - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + Home + + + + + + + + + + + + + + + + + + @@ -169,6 +191,23 @@
+
+
+
+
+

+ v1.0beta 01-21-2025

+
+
+
+
+

+ v1.0b information coming soon.- All Features related to new featureTriggers is completed and tested. +

+
+
+
+
@@ -509,6 +548,7 @@ + diff --git a/project.mobirise b/project.mobirise deleted file mode 100644 index 939d577..0000000 --- a/project.mobirise +++ /dev/null @@ -1,1434 +0,0 @@ -{ - "settings": { - "currentPage": "index.html", - "theme": { - "name": "boldm5", - "title": "BoldM5", - "styling": { - "primaryColor": "#ffffff", - "secondaryColor": "#a4a2a2", - "successColor": "#324c6d", - "infoColor": "#000000", - "warningColor": "#265e98", - "dangerColor": "#b6b6b6", - "mainFont": "Space Grotesk", - "display1Font": "DM Sans", - "display1Size": 4.4, - "display2Font": "DM Sans", - "display2Size": 2.6, - "display5Font": "DM Sans", - "display5Size": "1.5", - "display7Font": "DM Sans", - "display7Size": 1.125, - "display4Font": "Space Grotesk", - "display4Size": "1.25", - "isRoundedButtons": true, - "isGhostButtonBorder": false, - "underlinedLinks": false, - "isAnimatedOnScroll": true, - "isScrollToTopButton": false - }, - "titlePreset": "Technology Company", - "nameSelectPreset": "technology-company", - "presetSourceTheme": "boldm5", - "additionalSetColors": [ - "#20a6e2", - "#324c6d", - "#317e78", - "#376277", - "#265e98" - ] - }, - "path": "@PROJECT_PATH@", - "name": "Mintership-Forum-Alpha", - "versionFirst": "5.9.7", - "siteFonts": [ - { - "css": "'Space Grotesk', sans-serif", - "name": "Space Grotesk", - "url": "https://fonts.googleapis.com/css?family=Space+Grotesk:300,400,500,600,700" - }, - { - "css": "'DM Sans', sans-serif", - "name": "DM Sans", - "url": "https://fonts.googleapis.com/css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i" - } - ], - "imageResize": true, - "uniqCompNum": 7, - "versionPublish": "5.9.19", - "screenshot": "screenshot.png", - "favicon": "@PROJECT_PATH@/assets/images/modded-circle-2-new-128x128.png", - "mbrsiteDomain": "dl77uhfpwc", - "usedWebp": false, - "robotsSwitcher": false, - "sitemapSwitcher": false, - "sitemapSwitcherAuto": false, - "siteUrl": false, - "cookiesAlert": false, - "gdpr": false, - "robotsText": "User-agent: *\r\nDisallow: /cgi-bin\r\n", - "mcSmartCart": { - "currency": "USD", - "paymentSystem": "stripe", - "ACCOUNT_KEY": "acct_1PVPEZHAkeqfKTT8_sCmif7Aai6Y72hn0ZMASd7pfyfDWmbZhw1xF1xiUYvW", - "stripe_button": "Pay Now" - } - }, - "pages": { - "index.html": { - "settings": { - "main": true, - "title": "Home", - "meta_descr": "Welcome to the Mintership Forum (alpha version)", - "header_custom": "\n\n\n\n \n", - "footer_custom": "\n\n", - "html_before": "" - }, - "components": [ - { - "alias": false, - "_styles": { - ".navbar-dropdown": { - "& when not (@transparent)": { - "background-color": "@menuBgColor !important" - }, - "& when (@transparent)": { - "background": "rgba(red(@menuBgColor), green(@menuBgColor), blue(@menuBgColor), @opacity) !important" - }, - "padding": "0", - "& when (@sticky)": { - "position": "fixed" - } - }, - ".navbar.navbar-expand-lg .dropdown": { - ".dropdown-menu": { - "& when not (@transparent)": { - "background-color": "@menuBgColor !important" - }, - "& when (@transparent)": { - "background": "rgba(red(@menuBgColor), green(@menuBgColor), blue(@menuBgColor), @opacity) !important" - }, - ".dropdown-submenu": { - "margin": "0", - "left": "100%" - }, - "background": "@menuBgColor" - } - }, - ".menu_box": { - "@media (max-width: 991px)": { - ".navbar.opened, .navbar-collapse": { - "background-color": "@menuBgColor !important", - "transition": "all 0s ease 0s" - } - } - }, - "& when not (@sticky)": { - ".navbar-dropdown": { - "position": "relative !important" - } - }, - "& when (@sticky)": { - "z-index": "1000", - "width": "100%", - "nav.navbar": { - "position": "fixed", - "padding-top": "1rem", - "padding-bottom": "1rem" - } - }, - "& when (@showSidebar)": { - ".icons-menu-main": { - "display": "none", - "@media (max-width: 991px)": { - "max-width": "100%", - "margin": "1rem 0 1rem 1rem", - "display": "flex" - } - }, - ".mbr-section-btn-main": { - "display": "none", - "@media (max-width: 991px)": { - "margin-top": "1rem", - "display": "block" - } - } - }, - "& when not (@showSidebar)": { - ".icons-menu-main": { - "display": "flex", - "flex-wrap": "wrap", - "max-width": "150px", - "@media (max-width: 991px)": { - "max-width": "100%", - "margin": "1rem 0 1rem 1rem" - } - }, - ".mbr-section-btn-main": { - "@media (max-width: 991px)": { - "margin-top": "1rem" - } - } - }, - ".btn": { - "min-height": "auto", - "box-shadow": "none", - "margin-top": "0", - "&:hover": { - "box-shadow": "none" - } - }, - "@media (min-width: 992px)": { - ".offcanvas": { - "padding": "12rem 80px 0", - "width": "30%", - "background-color": "@offSidebarColor" - }, - ".offcanvas_image img": { - "width": "auto", - "object-fit": "cover", - "display": "inline-block" - }, - ".offcanvas-header": { - "position": "relative", - "padding": "0", - ".btn-close": { - "position": "absolute", - "top": "-70px", - "right": "0", - "width": "35px", - "height": "30px" - } - }, - ".offcanvas-body": { - "text-align": "center", - "padding": "0", - ".mbr-text, .mbr-section-subtitle": { - "margin-top": "14px" - }, - ".offcanvas_contact": { - "margin": "35px 0" - } - }, - ".offcanvas_box": { - "button.btn_offcanvas": { - "outline": "none", - "width": "40px", - "height": "40px", - "cursor": "pointer", - "transition": "all 0.2s", - "position": "relative", - "align-self": "center", - ".hamburger span": { - "position": "absolute", - "right": "0", - "width": "40px", - "height": "2px", - "border-right": "5px", - "background-color": "@hamburgerColor", - "&:nth-child(1)": { - "top": "18px", - "transition": "all 0.2s" - }, - "&:nth-child(2)": { - "top": "25px", - "transition": "all 0.2s" - } - }, - "&:hover .hamburger span": { - "width": "36px", - "&:nth-child(2)": { - "width": "33px", - "transition-delay": "0.2s" - } - } - } - }, - "ul.navbar-nav": { - "padding-bottom": "1.5rem" - }, - ".dropdown-menu .dropdown-toggle[data-toggle=\"dropdown-submenu\"]::after, .link.dropdown-toggle::after": { - "display": "inline-block", - "width": "7px", - "height": "7px", - "margin-left": ".5rem", - "margin-bottom": "2px", - "content": "\"\"", - "border": "2px solid", - "border-left": "none", - "border-top": "none", - "transform": "rotate(-45deg)" - }, - ".link.dropdown-toggle::after": { - "padding": "0 !important", - "transform": "rotate(45deg)" - }, - "li.nav-item": { - "position": "relative", - "display": "inline-block", - "padding": "1px 7px !important", - "vertical-align": "middle", - "line-height": "2em !important", - "font-weight": "600 !important", - "text-decoration": "none", - "letter-spacing": "0 !important", - "z-index": "1" - }, - ".lg_brand": { - "margin": "0 1rem" - } - }, - ".nav-item": { - "margin": "4px 15px", - "@media (min-width: 1200px)": { - "margin": "4px 20px" - }, - "@media (max-width: 991px)": { - "margin": "0 !important" - } - }, - ".dropdown-menu": { - "border-radius": "0", - "box-shadow": "none", - "text-align": "left", - "@media (min-width: 992px)": { - "padding": "18px 34px 22px", - "min-width": "250px", - "top": "auto !important", - "left": "-40px !important", - "&.dropdown-submenu": { - "left": "215px !important", - "top": "-45% !important" - } - } - }, - "@media (max-width: 991px)": { - ".dropdown-menu .dropdown-toggle[data-toggle=\"dropdown-submenu\"]::after, .link.dropdown-toggle::after": { - "display": "inline-block", - "width": "7px", - "height": "7px", - "margin-left": ".5rem", - "margin-bottom": "2px", - "content": "\"\"", - "border": "2px solid", - "border-left": "none", - "border-top": "none", - "transform": "rotate(-45deg)", - "right": "15px", - "position": "absolute", - "margin-top": "-2px" - }, - ".show.dropdown-toggle[aria-expanded=\"true\"]::after": { - "transform": "rotate(45deg)", - "margin-top": "-4px" - }, - ".offcanvas_box": { - "display": "none" - } - }, - ".dropdown-item": { - "border": "none", - "font-weight": "400 !important" - }, - ".nav-dropdown .link": { - "font-weight": "400 !important", - "padding": "0 !important", - "margin": "0 !important" - }, - ".nav-dropdown .link.dropdown-toggle::after": { - "margin-left": "0.5rem", - "margin-top": "0" - }, - ".container": { - "display": "flex", - "margin": "auto" - }, - ".iconfont-wrapper": { - "color": "@iconsColor", - "font-size": "17px", - "margin-right": "10px", - "margin-bottom": "5px", - "&:last-child": { - "margin-right": "0" - }, - "width": "25px", - "height": "25px", - "border-radius": "50%", - "display": "flex", - "justify-content": "center", - "align-items": "center", - "transition": "all 0.2s ease-in-out", - "&:hover": { - "opacity": ".5" - } - }, - ".navbar-caption": { - "color": "#ffffff" - }, - ".navbar-nav": { - "@media (min-width: 992px)": { - "margin": "0" - }, - "margin": "0 1rem" - }, - ".dropdown-menu, .navbar.opened": { - "background-color": "@transparent !important" - }, - ".nav-item:focus, .nav-link:focus": { - "outline": "none" - }, - ".dropdown .dropdown-menu .dropdown-item": { - "width": "auto", - "transition": "all 0.25s ease-in-out", - "&::after": { - "right": "0.5rem" - }, - ".mbr-iconfont": { - "margin-right": "0.5rem", - "vertical-align": "sub", - "&:before": { - "display": "inline-block", - "transform": "scale(1, 1)", - "transition": "all 0.25s ease-in-out" - } - } - }, - ".collapsed": { - ".dropdown-menu .dropdown-item:before": { - "display": "none" - }, - ".dropdown .dropdown-menu .dropdown-item": { - "padding": "0.235em 1.5em 0.235em 1.5em !important", - "transition": "none", - "margin": "0 !important" - } - }, - ".navbar": { - "min-height": "70px", - "padding": "20px 0", - "transition": "all 0.3s", - "border-bottom-width": "0", - "@media (max-width: 992px)": { - "min-height": "30px", - "max-height": "none" - }, - "&:not(.navbar-short)": {}, - "&.opened": { - "transition": "all 0.3s" - }, - ".dropdown-item": { - "padding": "0", - "margin": "8px 0" - }, - ".navbar-logo img": { - "max-width": "130px", - "max-height": "130px", - "object-fit": "contain" - }, - ".navbar-collapse": { - "justify-content": "space-between", - "& when (@showLogo), (@showBrand)": { - "justify-content": "space-between" - }, - "z-index": "1" - }, - "&.collapsed": { - "justify-content": "center", - ".nav-item .nav-link::before": { - "display": "none" - }, - "&.opened": { - ".dropdown-menu": { - "top": "0" - } - }, - ".dropdown-menu": { - ".dropdown-submenu": { - "left": "0 !important" - }, - ".dropdown-item:after": { - "right": "auto" - } - }, - "ul.navbar-nav": { - "li": { - "margin": "auto" - } - }, - ".dropdown-menu .dropdown-item": { - "padding": "0.25rem 1.5rem", - "text-align": "left" - }, - ".icons-menu": { - "padding": "0" - } - }, - "@media (max-width: 991px)": { - ".nav-item": { - "padding": ".5rem 0" - }, - ".navbar-collapse": { - "padding": "34px 0", - "border-radius": "25px" - }, - ".nav-item .nav-link::before": { - "display": "none" - }, - "&.opened": { - ".dropdown-menu": { - "top": "0" - } - }, - ".dropdown-menu": { - "padding": "6px 0 6px 15px", - ".dropdown-submenu": { - "left": "0 !important" - }, - ".dropdown-item:after": { - "right": "auto", - "margin-top": "-0.4rem" - } - }, - ".navbar-logo": { - "img": { - "height": "3rem !important" - } - }, - "ul.navbar-nav": { - "overflow": "hidden", - "li": { - "margin": "0" - } - }, - ".dropdown-menu .dropdown-item": { - "padding": "0 !important", - "margin": "0", - "margin-top": "8px", - "text-align": "left" - }, - ".navbar-brand": { - "flex-shrink": "initial", - "flex-basis": "auto", - "word-break": "break-word", - "padding-right": "2rem" - }, - ".navbar-toggler": { - "flex-basis": "auto" - }, - ".icons-menu": { - "padding": "0" - } - }, - "&.navbar-short": { - "min-height": "60px", - ".navbar-logo": { - "img": { - "height": "2.5rem !important" - } - }, - ".navbar-brand": { - "min-height": "60px", - "padding": "0" - } - } - }, - ".navbar-brand": { - "min-height": "70px", - "flex-shrink": "0", - "align-items": "center", - "margin-right": "0", - "padding": "10px 0", - "transition": "all 0.3s", - "word-break": "break-word", - "z-index": "1", - ".navbar-caption": { - "line-height": "inherit !important" - }, - ".navbar-logo a": { - "outline": "none" - } - }, - ".dropdown-item.active, .dropdown-item:active": { - "background-color": "transparent" - }, - ".navbar-expand-lg .navbar-nav .nav-link": { - "padding": "0" - }, - ".nav-dropdown .link.dropdown-toggle": { - "margin-right": "1.667em", - "&[aria-expanded=\"true\"]": { - "margin-right": "0", - "padding": "0.667em 1.667em" - } - }, - ".navbar .dropdown.open > .dropdown-menu": { - "display": "block" - }, - "ul.navbar-nav": { - "flex-wrap": "wrap", - "padding": "0" - }, - ".navbar-buttons": { - "text-align": "center", - "min-width": "170px" - }, - "button.navbar-toggler": { - "outline": "none", - "width": "48px", - "height": "48px", - "border-radius": "50%", - "cursor": "pointer", - "transition": "all 0.2s", - "position": "relative", - "align-self": "center", - "color": "@menuBgColor", - "background": "@hamburger", - ".hamburger span": { - "position": "absolute", - "right": "10px", - "margin-top": "14px", - "width": "26px", - "height": "2px", - "border-right": "5px", - "background-color": "@hamburgerColor", - "&:nth-child(1)": { - "top": "0", - "transition": "all 0.2s" - }, - "&:nth-child(2)": { - "top": "8px", - "transition": "all 0.15s" - }, - "&:nth-child(3)": { - "top": "8px", - "transition": "all 0.15s" - }, - "&:nth-child(4)": { - "top": "16px", - "transition": "all 0.2s" - } - } - }, - "nav.opened .hamburger span": { - "&:nth-child(1)": { - "top": "8px", - "width": "0", - "opacity": "0", - "right": "50%", - "transition": "all 0.2s" - }, - "&:nth-child(2)": { - "transform": "rotate(45deg)", - "transition": "all 0.25s" - }, - "&:nth-child(3)": { - "transform": "rotate(-45deg)", - "transition": "all 0.25s" - }, - "&:nth-child(4)": { - "top": "8px", - "width": "0", - "opacity": "0", - "right": "50%", - "transition": "all 0.2s" - } - }, - "a.nav-link": { - "display": "flex", - "align-items": "center", - "justify-content": "flex-start" - }, - ".icons-menu": { - "flex-wrap": "nowrap", - "display": "flex", - "justify-content": "center", - "padding": "0", - "text-align": "center", - "margin-bottom": "35px" - }, - "@media screen and (~'-ms-high-contrast: active'), (~'-ms-high-contrast: none')": { - ".navbar": { - "height": "70px", - "&.opened": { - "height": "auto" - } - }, - ".nav-item .nav-link:hover::before": { - "width": "175%", - "max-width": "calc(100% ~\"+\" 2rem)", - "left": "-1rem" - } - }, - ".navbar-dropdown .navbar-logo": { - "margin-right": "15px" - }, - "@media (min-width: 768px)": { - ".container-fluid": { - "padding-left": "30px", - "padding-right": "30px" - } - }, - ".mbr-section-btn-main": { - "padding-top": "5px", - ".btn": { - "margin": "0 4px 4px 4px" - } - }, - ".navbar-caption:hover": { - "color": "@primaryColor" - }, - ".dropdown-menu.dropdown-submenu": { - "@media (min-width: 992px)": { - "left": "175px !important", - "top": "-45% !important" - } - }, - ".mbr-section-btn, .mbr-section-btn-main": { - ".btn": { - "background-image": "linear-gradient(99deg, rgba(255, 255, 255, 0) 30%, @buttonAni 100%), radial-gradient(circle at 50% 50%, @buttonAni 0, rgba(255, 255, 255, 0) 70%)", - "color": "@buttonColor !important" - } - }, - ".mbr-section-subtitle": { - "color": "#000000" - }, - ".mbr-text": { - "color": "#000000", - "text-align": "center" - }, - ".text_widget": { - "color": "#000000" - }, - ".mbr-section-subtitle, .text_widget, .mbr-section-btn": { - "text-align": "center" - }, - "a[class*=\"text-\"]:not(.nav-link):not(.dropdown-item):not([role]):not(.navbar-caption):hover": { - "background-image": "none" - } - }, - "_name": "menu1", - "_sourceTheme": "boldm5", - "_customHTML": "
\n\n \n
Size
\n \n
Show/Hide
\n \n \n \n \n \n \n \n \n \n
Styles
\n \n \n \n \n \n \n \n
Sidebar
\n \n \n \n \n \n
\n\n \n
", - "_cid": "ttRnktJ11Q", - "_anchor": "menu1-0", - "_PHPplaceholders": [], - "_JSplaceholders": [], - "_protectedParams": [], - "_global": true, - "_once": "menu", - "_params": {} - }, - { - "alias": false, - "_styles": { - "& when not (@fullScreen)": { - "padding-top": "(@paddingTop * 1rem)", - "padding-bottom": "(@paddingBottom * 1rem)" - }, - "& when (@bg-type = 'color')": { - "background-color": "@bg-value" - }, - "& when (@bg-type = 'image')": { - "background-image": "url(@bg-value)" - }, - ".mbr-fallback-image.disabled": { - "display": "none" - }, - ".mbr-fallback-image": { - "display": "block", - "background-size": "cover", - "background-position": "center center", - "width": "100%", - "height": "100%", - "position": "absolute", - "top": "0", - "& when (@bg-type = 'video')": { - "background-image": "url(@fallBackImage)" - } - }, - "& when (@fullWidth)": { - ".container-fluid": { - "margin": "0", - "padding": "0 60px", - "@media (max-width: 992px)": { - "padding": "0 25px" - }, - ".row": { - "padding": "0" - } - } - }, - "& when not (@fullWidth)": { - "@media (max-width: 992px)": { - ".row": { - "padding": "0 13px" - } - } - }, - ".title-wrapper": { - "padding-top": "35%", - "@media (max-width: 992px)": { - "padding-top": "0" - }, - ".mbr-section-title": { - "margin-bottom": "48px" - }, - ".mbr-text": { - "margin-bottom": "48px" - }, - ".mbr-section-btn": { - ".btn": { - "background-image": "linear-gradient(99deg, rgba(255, 255, 255, 0) 30%, @buttonAni 100%), radial-gradient(circle at 50% 50%, @buttonAni 0, rgba(255, 255, 255, 0) 70%)", - "color": "@buttonColor !important" - } - } - }, - ".mbr-section-title": { - "color": "#ffffff" - }, - ".mbr-text": { - "color": "#b6b6b6" - }, - ".mbr-section-title, .mbr-section-btn": { - "text-align": "left" - } - }, - "_name": "header1", - "_sourceTheme": "boldm5", - "_customHTML": "
\n \n
Size
\n \n \n \n \n
Show/Hide
\n \n \n \n \n \n
Background
\n
\n \n \n \n
\n
Fallback Image
\n \n \n \n \n
\n\n
\n
\n
\n\n
\n
\n
\n
\n

Q-Mintership Alpha

\n

This is the innitial 'alpha' of the Mintership Forum / Mintership tools that will be built into the final Q-Mintership app. This is a simplistic version built by crowetic that will offer a very simple communciations location, and the tools for the minter admins to accomplish the necessary GROUP_APPROVAL transactions. Scroll down for the currently available tools... 

\n \n
\n
\n
\n
\n
", - "_cid": "ttRnlSkg2R", - "_anchor": "header1-1", - "_protectedParams": [], - "_global": false, - "_once": false, - "_params": {} - }, - { - "alias": false, - "_styles": { - "& when not (@fullScreen)": { - "padding-top": "(@paddingTop * 1rem)", - "padding-bottom": "(@paddingBottom * 1rem)" - }, - "& when (@bg-type = 'color')": { - "background-color": "@bg-value" - }, - "& when (@bg-type = 'image')": { - "background-image": "url(@bg-value)" - }, - ".mbr-fallback-image.disabled": { - "display": "none" - }, - ".mbr-fallback-image": { - "display": "block", - "background-size": "cover", - "background-position": "center center", - "width": "100%", - "height": "100%", - "position": "absolute", - "top": "0", - "& when (@bg-type = 'video')": { - "background-image": "url(@fallBackImage)" - } - }, - "& when (@fullWidth)": { - ".container-fluid": { - "margin": "0", - "padding": "0 51px", - "@media (max-width: 992px)": { - "padding": "0 12px" - }, - ".row": { - "padding": "0" - } - } - }, - "& when not (@fullWidth)": { - "@media (max-width: 992px)": { - ".row": { - "padding": "0 13px" - } - } - }, - ".row": { - "justify-content": "center" - }, - ".item": { - "position": "relative", - "margin-bottom": "25px", - "padding": "0 25px", - ".item-link": { - "width": "100%", - "height": "100%", - ".item-wrapper": { - "position": "relative", - "display": "flex", - "justify-content": "center", - "align-items": "center", - "height": "300px", - "overflow": "hidden", - "&:hover": { - "img": { - "transform": "scale(1.03)" - } - }, - "img": { - "position": "absolute", - "top": "0", - "left": "0", - "width": "100%", - "height": "100%", - "object-fit": "cover", - "transform": "scale(1)", - "transition": "all 0.3s ease-out" - }, - "&::before": { - "content": "''", - "position": "absolute", - "width": "100%", - "height": "100%", - "top": "0", - "left": "0", - "background-color": "@active", - "opacity": ".5", - "transition": "all 0.3s ease-out", - "z-index": "1", - "pointer-events": "none" - }, - ".item-content": { - "position": "relative", - "z-index": "1", - ".card-title": { - "margin-bottom": "0", - "position": "relative" - } - } - } - } - }, - ".card-title": { - "color": "#ffffff" - } - }, - "_name": "features7", - "_sourceTheme": "boldm5", - "_customHTML": "
\n \n
Size
\n \n \n \n \n
Cards
\n \n \n
Background
\n
\n \n \n
\n
Fallback Image
\n \n \n \n \n
\n\n
\n
\n
\n\n \n
", - "_cid": "ttRnAijqXt", - "_anchor": "features7-6", - "_PHPplaceholders": [], - "_JSplaceholders": [], - "_protectedParams": [], - "_global": false, - "_once": false, - "_params": {} - }, - { - "alias": false, - "_styles": { - "& when not (@fullScreen)": { - "padding-top": "(@paddingTop * 1rem)", - "padding-bottom": "(@paddingBottom * 1rem)" - }, - "& when (@bg-type = 'color')": { - "background-color": "@bg-value" - }, - "& when (@bg-type = 'image')": { - "background-image": "url(@bg-value)" - }, - ".mbr-fallback-image.disabled": { - "display": "none" - }, - ".mbr-fallback-image": { - "display": "block", - "background-size": "cover", - "background-position": "center center", - "width": "100%", - "height": "100%", - "position": "absolute", - "top": "0", - "& when (@bg-type = 'video')": { - "background-image": "url(@fallBackImage)" - } - }, - "& when (@fullWidth)": { - ".container-fluid": { - "margin": "0", - "padding": "0 60px", - "@media (max-width: 992px)": { - "padding": "0 25px" - }, - ".row": { - "padding": "0" - } - } - }, - "& when not (@fullWidth)": { - "@media (max-width: 992px)": { - ".row": { - "padding": "0 13px" - } - } - }, - ".row": { - "justify-content": "center" - }, - ".card-wrapper": { - "@media (max-width: 992px)": { - "margin-bottom": "55px" - }, - ".icon-wrapper": { - "margin-bottom": "18px", - "justify-content": "center", - "align": "center", - ".mbr-iconfont": { - "display": "inline", - "font-size": "82px", - "width": "auto", - "color": "@icon" - } - }, - ".mbr-section-title": { - "margin-bottom": "18px" - }, - ".mbr-text": { - "margin-bottom": "0" - } - }, - ".mbr-section-title": { - "color": "#ffffff" - }, - ".mbr-text": { - "color": "#b6b6b6", - "text-align": "center" - }, - ".mbr-section-title, .icon-wrapper": { - "text-align": "center" - } - }, - "_name": "features1", - "_sourceTheme": "boldm5", - "_customHTML": "
\n \n
Size
\n \n \n \n \n
Cards
\n \n \n \n \n \n
Background
\n
\n \n \n
\n
Fallback Image
\n \n \n \n \n
\n\n
\n
\n
\n\n
\n
\n
\n
\n
\n \n
\n

\n Mintership Details

\n

\n Learn more about the Mintership concept, and why it was needed. The days of 'sponsorship' are a thing of the past on the Qortal Network. No more will there be the ability to self-sponsor. A new era of Qortal begins!

\n
\n
\n
1\">\n
\n
\n \n
\n

Become A Minter

\n

\n Not already minting? You've come to the right place to get started. Check the Mintership Forum link for more information. The updated Q-Mintership app will be a more fully featured application.

\n
\n
\n
2\">\n
\n
\n \n
\n

Minter Admin Tools

\n

\n Are you one of the initially selected Minter Admins? We have the tools here you need to create and approve GROUP_APPROVAL transactions, and communicate securely with your fellow admins.

\n
\n
\n
\n
\n
", - "_cid": "utzh0dnVQB", - "_anchor": "features1-2", - "_PHPplaceholders": [], - "_JSplaceholders": [], - "_protectedParams": [], - "_global": false, - "_once": false, - "_params": {} - }, - { - "alias": false, - "_styles": { - "& when not (@fullScreen)": { - "padding-top": "(@paddingTop * 1rem)", - "padding-bottom": "(@paddingBottom * 1rem)" - }, - "& when (@bg-type = 'color')": { - "background-color": "@bg-value" - }, - "& when (@bg-type = 'image')": { - "background-image": "url(@bg-value)" - }, - ".mbr-fallback-image.disabled": { - "display": "none" - }, - ".mbr-fallback-image": { - "display": "block", - "background-size": "cover", - "background-position": "center center", - "width": "100%", - "height": "100%", - "position": "absolute", - "top": "0", - "& when (@bg-type = 'video')": { - "background-image": "url(@fallBackImage)" - } - }, - "& when (@fullWidth)": { - ".container-fluid": { - "margin": "0", - "padding": "0 16px", - ".row": { - "padding": "0" - } - } - }, - "& when not (@fullWidth)": { - "@media (max-width: 992px)": { - ".row": { - "padding": "0 13px" - } - } - }, - ".row": { - "justify-content": "center" - }, - ".card": { - "padding": "0", - "border-top": "1px solid @border", - "border-bottom": "1px solid @border", - "border-radius": "0" - }, - ".title-wrapper": { - "padding": "14px 0 18px", - "white-space": "nowrap", - "overflow": "hidden", - "&:last-child": { - "padding": "18px 14px", - "border-top": "1px solid @border" - }, - ".mbr-section-title": { - "margin-bottom": "0" - } - }, - ".mbr-section-title": { - "color": "#ffffff", - "text-align": "center" - }, - ".mbr-title": { - "color": "#ffffff", - "text-align": "right" - } - }, - "_name": "content3", - "_sourceTheme": "boldm5", - "_customHTML": "
\n \n
Size
\n \n \n \n \n
Show/Hide
\n \n \n
Background
\n
\n \n \n
\n
Fallback Image
\n \n \n \n \n
\n\n
\n
\n
\n\n
\n
\n
\n
\n

\n More information...

\n
\n
1\">\n

Click to Open

\n
\n
\n
\n
\n
", - "_cid": "uu3bTy9Zr1", - "_anchor": "content3-4", - "_protectedParams": [], - "_global": false, - "_once": false, - "_params": {} - }, - { - "alias": false, - "_styles": { - "& when not (@fullScreen)": { - "padding-top": "(@paddingTop * 1rem)", - "padding-bottom": "(@paddingBottom * 1rem)" - }, - "& when (@bg-type = 'color')": { - "background-color": "@bg-value" - }, - "& when (@bg-type = 'image')": { - "background-image": "url(@bg-value)" - }, - ".mbr-fallback-image.disabled": { - "display": "none" - }, - ".mbr-fallback-image": { - "display": "block", - "background-size": "cover", - "background-position": "center center", - "width": "100%", - "height": "100%", - "position": "absolute", - "top": "0", - "& when (@bg-type = 'video')": { - "background-image": "url(@fallBackImage)" - } - }, - "& when (@fullWidth)": { - ".container-fluid": { - "margin": "0", - "padding": "0 60px", - "@media (max-width: 992px)": { - "padding": "0 25px" - }, - ".row": { - "padding": "0" - } - } - }, - "& when not (@fullWidth)": { - "@media (max-width: 992px)": { - ".row": { - "padding": "0 13px" - } - } - }, - ".row": { - "justify-content": "center" - }, - ".card-wrapper": { - "@media (max-width: 992px)": { - "margin-bottom": "55px" - }, - ".icon-wrapper": { - "margin-bottom": "18px", - ".mbr-iconfont": { - "display": "inline", - "font-size": "82px", - "width": "auto", - "color": "@icon" - } - }, - ".mbr-section-title": { - "margin-bottom": "18px" - }, - ".mbr-text": { - "margin-bottom": "0" - } - }, - ".mbr-section-title": { - "color": "#ffffff" - }, - ".mbr-text": { - "color": "#b6b6b6", - "text-align": "center" - }, - ".mbr-section-title, .icon-wrapper": { - "text-align": "center" - } - }, - "_name": "features1", - "_sourceTheme": "boldm5", - "_customHTML": "
\n \n
Size
\n \n \n \n \n
Cards
\n \n \n \n \n \n
Background
\n
\n \n \n
\n
Fallback Image
\n \n \n \n \n
\n\n
\n
\n
\n\n
\n
\n
\n
\n
\n \n
\n

\n Get Information

\n

\n Would you like to become a minter? You've come to the right place! Obtain details about the new Mintership-based minting process on Qortal.

\n
\n
\n
1\">\n
\n
\n \n
\n

\n Minter Admin Tools

\n

\n If you are a Minter Admin, you will need to know how to create and sign GROUP_APPROVAL transactions. You may do so here!

\n
\n
\n
2\">\n
\n
\n \n
\n

\n Start your Mission

\n

\n Every mission has a beginning. If your mission is to become a minter, or a Minter Admin, then you've landed at the correct launchpad!

\n
\n
\n
\n
\n
", - "_cid": "uufI05uMCB", - "_protectedParams": [], - "_global": false, - "_once": false, - "_params": {}, - "_anchor": "features1-5" - }, - { - "alias": false, - "_styles": { - "& when not (@fullScreen)": { - "padding-top": "(@paddingTop * 1rem)", - "padding-bottom": "(@paddingBottom * 1rem)" - }, - "& when (@bg-type = 'color')": { - "background-color": "@bg-value" - }, - "& when (@bg-type = 'image')": { - "background-image": "url(@bg-value)" - }, - ".mbr-fallback-image.disabled": { - "display": "none" - }, - ".mbr-fallback-image": { - "display": "block", - "background-size": "cover", - "background-position": "center center", - "width": "100%", - "height": "100%", - "position": "absolute", - "top": "0", - "& when (@bg-type = 'video')": { - "background-image": "url(@fallBackImage)" - } - }, - "& when (@fullWidth)": { - ".container-fluid": { - "margin": "0", - "padding": "0 60px", - "@media (max-width: 992px)": { - "padding": "0 25px" - }, - ".row": { - "padding": "0" - } - } - }, - "& when not (@fullWidth)": { - "@media (max-width: 992px)": { - ".row": { - "padding": "0 13px" - } - } - }, - ".row": { - "justify-content": "center" - }, - ".card": { - "display": "flex", - "flex-direction": "column", - "justify-content": "center" - }, - ".title-wrapper": { - "padding": "30px 54px 30px 0", - "& when (@reverseContent)": { - "padding": "30px 0 30px 54px", - "@media (max-width: 992px)": { - "padding": "0 0 40px" - } - }, - "@media (max-width: 992px)": { - "padding": "0 0 40px" - }, - ".mbr-section-title": { - "margin-bottom": "0" - } - }, - ".text-wrapper": { - "padding": "30px 0 30px 74px", - "border-left": "1px solid @border", - "& when (@reverseContent)": { - "padding": "30px 74px 30px 0", - "border-left": "none", - "border-right": "1px solid @border", - "@media (max-width: 992px)": { - "padding": "40px 0 0", - "border": "none", - "border-top": "1px solid @border" - } - }, - "@media (max-width: 992px)": { - "padding": "40px 0 0", - "border-left": "none", - "border-top": "1px solid @border" - }, - ".mbr-text": { - "margin-bottom": "0" - } - }, - ".mbr-section-title": { - "color": "#ffffff" - }, - ".mbr-text": { - "color": "#b6b6b6" - } - }, - "_name": "content7", - "_sourceTheme": "boldm5", - "_customHTML": "
\n \n
Size
\n \n \n \n \n \n
Show/Hide
\n \n \n \n
Background
\n
\n \n \n
\n
Fallback Image
\n \n \n \n \n
\n\n
\n
\n
\n\n
\n
\n
\n
\n

\n This is the beginning...

\n
\n
\n
\n
\n

\n This is the very start of the Q-Mintership app. It will be dramatically changing upon the beta release, and modification to the Q-Mintership Q-App. This initial version is a version that could be launched more quickly, and does not have nearly as much functionality as what will exist once the main app goes live. 

\n
\n
\n
\n
\n \n
", - "_cid": "uufIRKtXOO", - "_anchor": "content7-6", - "_PHPplaceholders": [], - "_JSplaceholders": [], - "_protectedParams": [], - "_global": false, - "_once": false, - "_params": {} - }, - { - "alias": false, - "_styles": { - "& when not (@fullScreen)": { - "padding-top": "(@paddingTop * 1rem)", - "padding-bottom": "(@paddingBottom * 1rem)" - }, - "& when (@bg-type = 'color')": { - "background-color": "@bg-value" - }, - "& when (@bg-type = 'image')": { - "background-image": "url(@bg-value)" - }, - ".mbr-fallback-image.disabled": { - "display": "none" - }, - ".mbr-fallback-image": { - "display": "block", - "background-size": "cover", - "background-position": "center center", - "width": "100%", - "height": "100%", - "position": "absolute", - "top": "0", - "& when (@bg-type = 'video')": { - "background-image": "url(@fallBackImage)" - } - }, - "& when (@fullWidth)": { - ".container-fluid": { - "padding": "0 50px", - "@media (max-width: 992px)": { - "padding": "0 30px" - } - } - }, - ".container": { - "@media (max-width: 992px)": { - "padding": "0 30px" - } - }, - ".title-wrapper": { - "margin-bottom": "20px", - ".title-wrap": { - "display": "inline-flex", - "align-items": "center", - "img": { - "height": "78px", - "width": "78px", - "border-radius": "20px", - "object-fit": "cover", - "margin-right": "16px" - }, - ".mbr-section-title": { - "margin-bottom": "0" - } - } - }, - ".mbr-desc": { - "margin-bottom": "0", - "color": "#a4a2a2", - "text-align": "left" - }, - ".link-wrap": { - "&:hover": { - ".mbr-link": { - "color": "@active" - } - }, - ".mbr-link": { - "margin-bottom": "0", - "transition": "all 0.3s ease-in-out", - "@media (max-width: 992px)": { - "margin-bottom": "20px" - } - } - }, - ".list": { - "margin": "0", - "padding": "0", - "list-style-type": "none", - "@media (max-width: 992px)": { - "text-align": "left" - }, - ".item-wrap": { - "text-decoration": "underline", - "margin-bottom": "16px", - "transition": "all 0.3s ease-in-out", - "&:hover": { - "color": "@active" - } - }, - "color": "#ffffff" - }, - ".copyright": { - "margin": "50px 0", - "@media (max-width: 992px)": { - "margin": "30px 0" - }, - "color": "#ffffff", - "text-align": "right" - }, - ".mbr-section-title": { - "color": "#ffffff" - }, - ".mbr-link": { - "color": "#ffffff", - "text-align": "left" - }, - ".mbr-section-title, .title-wrapper": { - "color": "#324c6d", - "text-align": "left" - } - }, - "_name": "footer1", - "_sourceTheme": "visualm5", - "_customHTML": "
\n\n \n
Size
\n \n \n \n \n \n
Show/Hide
\n \n \n \n \n \n \n \n
Background
\n
\n \n \n
\n
Fallback Image
\n \n \n \n \n
\n\n
\n
\n
\n\n
\n
\n
\n
\n
\n \"Mobirise\"\n

Q-Mintership Alpha

\n
\n
\n

\n Site Updates and Development Information

\n \n \n \n
\n
\n
    \n
  • TWITTER
  • \n
  • FACEBOOK
  • \n
  • LINKEDIN
  • \n
  • BEHANCE
  • \n
\n
\n
\n

\n 2024 Qortal Development Group

\n
\n
\n
\n
", - "_cid": "uhGd4SVHZK", - "_anchor": "footer1-1", - "_protectedParams": [], - "_global": true, - "_once": "footers", - "_params": {} - } - ] - } - } -} \ No newline at end of file