DynamicTable Pro: Interactive Data Table & List Component Generator

Table/List Configuration

Define Columns

Options

`; } downloadCodeBtn.addEventListener('click', () => { if (isUnlocked()) { downloadCode(); } else { incrementUsageCountAndCheck(); if (isUnlocked()) { downloadCode(); } } }); function downloadCode() { const code = generateCode(); if (code) { const blob = new Blob([code], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'dynamic-table-component.html'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); generatedCodePre.textContent = code; codeSection.style.display = 'block'; } } // --- WhatsApp Integration --- whatsappBtn.addEventListener('click', () => { if (isUnlocked()) { sendConfigViaWhatsApp(); } else { incrementUsageCountAndCheck(); if (isUnlocked()) { sendConfigViaWhatsApp(); } } }); function sendConfigViaWhatsApp() { updateColumnsArray(); if (columns.length === 0) { alert('Please define at least one column before sending to WhatsApp.'); return; } const currentTranslation = translations[currentLocale]; let message = `${currentTranslation['whatsappMessageHeader']}\n\n`; message += `${currentTranslation['whatsappMessageConfig']}\n`; message += `\n${currentTranslation['whatsappMessageColumns']}\n`; columns.forEach(col => { message += currentTranslation['whatsappMessageColumn'] .replace('{name}', col.name) .replace('{type}', col.type) + '\n'; }); message += `\n${currentTranslation['whatsappMessageOptions']}\n`; message += currentTranslation['whatsappMessageTableType'].replace('{type}', tableTypeSelect.value === 'table' ? currentTranslation['option_table'] : currentTranslation['option_list']) + '\n'; const sortableNames = Array.from(sortableColumnsSelect.selectedOptions).map(opt => columns[parseInt(opt.value)]?.name).filter(Boolean); message += currentTranslation['whatsappMessageSortable'].replace('{columns}', sortableNames.length > 0 ? sortableNames.join(', ') : 'None') + '\n'; message += currentTranslation['whatsappMessageSearch'].replace('{enabled}', searchEnabledSelect.value === 'true' ? currentTranslation['option_yes'] : currentTranslation['option_no']) + '\n'; message += currentTranslation['whatsappMessagePagination'].replace('{enabled}', paginationEnabledSelect.value === 'true' ? currentTranslation['option_yes'] : currentTranslation['option_no']) + '\n'; message += `\n${currentTranslation['whatsappMessageFooter']}`; const whatsappUrl = `https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent(message)}`; window.open(whatsappUrl, '_blank'); } // --- Monetization Logic --- function getUsageCount() { return parseInt(localStorage.getItem(`pv_actions_${WIDGET_SLUG}`)) || 0; } function incrementUsageCountAndCheck() { let count = getUsageCount(); count++; localStorage.setItem(`pv_actions_${WIDGET_SLUG}`, count); if (!isUnlocked() && count >= 3) { showPaymentOverlay(); } } function isUnlocked() { return localStorage.getItem(`pv_unlocked_${WIDGET_SLUG}`) === 'true'; } function unlockWidget() { localStorage.setItem(`pv_unlocked_${WIDGET_SLUG}`, 'true'); hidePaymentOverlay(); console.log('Widget unlocked!'); // Re-check button texts for logged-in user if payment overlay was shown if (auth.currentUser) { hubSubscribeBtn.textContent = translations[currentLocale]['paymentCard1_btn_loggedIn']; } showToast(translations[currentLocale]['cryptoStatusSuccess'], 'success'); } function showPaymentOverlay() { paymentOverlay.classList.add('active'); // Update button texts based on login status if (auth.currentUser) { hubSubscribeBtn.textContent = translations[currentLocale]['paymentCard1_btn_loggedIn']; } else { hubSubscribeBtn.textContent = translations[currentLocale]['paymentCard1_btn']; } } function hidePaymentOverlay() { paymentOverlay.classList.remove('active'); } function openAuthModal(isLogin = true) { isAuthLoginMode = isLogin; authModalTitle.textContent = isLogin ? translations[currentLocale]['authModalTitle_login'] : translations[currentLocale]['authModalTitle_register']; authSubmitBtn.textContent = isLogin ? translations[currentLocale]['authLoginBtn'] : translations[currentLocale]['authRegisterBtn']; toggleAuthModeBtn.textContent = isLogin ? translations[currentLocale]['authToggleToRegister'] : translations[currentLocale]['authToggleToLogin']; authErrorDisplay.style.display = 'none'; authModal.classList.add('active'); } function closeAuthModal() { authModal.classList.remove('active'); authForm.reset(); authErrorDisplay.style.display = 'none'; } async function handleAuthSubmit(event) { event.preventDefault(); const email = authEmailInput.value; const password = authPasswordInput.value; authErrorDisplay.style.display = 'none'; try { if (isAuthLoginMode) { await auth.signInWithEmailAndPassword(email, password); showToast(translations[currentLocale]['authSuccess_login'], 'success'); } else { await auth.createUserWithEmailAndPassword(email, password); showToast(translations[currentLocale]['authSuccess_register'], 'success'); } closeAuthModal(); // onAuthStateChanged will handle subscription check and unlock } catch (error) { console.error("Auth error:", error); let errorMessage = translations[currentLocale]['authError_generic']; if (error.code === 'auth/email-already-in-use') { errorMessage = translations[currentLocale]['authError_emailInUse']; } else if (error.code === 'auth/user-not-found' || error.code === 'auth/wrong-password') { errorMessage = translations[currentLocale]['authError_generic']; // General message to avoid leaking user info } authErrorDisplay.textContent = errorMessage; authErrorDisplay.style.display = 'block'; } } async function signInWithGoogle() { try { await auth.signInWithPopup(googleProvider); showToast(translations[currentLocale]['authSuccess_login'], 'success'); closeAuthModal(); // onAuthStateChanged will handle subscription check and unlock } catch (error) { console.error("Google Auth error:", error); authErrorDisplay.textContent = translations[currentLocale]['authError_generic']; authErrorDisplay.style.display = 'block'; } } function showToast(message, type = 'info', duration = 3000) { const toast = document.createElement('div'); toast.className = `message ${type}`; toast.textContent = message; Object.assign(toast.style, { position: 'fixed', bottom: '20px', right: '20px', zIndex: '10000', padding: '10px 20px', borderRadius: '8px', boxShadow: '0 4px 10px rgba(0,0,0,0.2)', opacity: '0', transition: 'opacity 0.3s ease-out, transform 0.3s ease-out', transform: 'translateY(20px)' }); document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translateY(0)'; }, 100); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(20px)'; toast.addEventListener('transitionend', () => toast.remove()); }, duration); } // Event Listeners for monetization buttons hubSubscribeBtn.addEventListener('click', async () => { if (!auth.currentUser) { openAuthModal(true); // Open login modal } else { // User is logged in, initiate subscription showToast("Initiating subscription...", 'info', 5000); try { const response = await fetch(`${API_BASE}/create-hub-subscription`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: auth.currentUser.email, userId: auth.currentUser.uid }) }); const data = await response.json(); if (data.url) { window.location.href = data.url; } else { showToast(data.message || translations[currentLocale]['error_fetch'], 'error'); } } catch (error) { console.error('Error creating Hub subscription:', error); showToast(translations[currentLocale]['error_fetch'], 'error'); } } }); oneTimePayBtn.addEventListener('click', async () => { showToast("Redirecting to Stripe...", 'info', 5000); try { const response = await fetch(`${API_BASE}/create-session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: WIDGET_SLUG, amount: 199, // $1.99 widgetName: translations['EN']['appName'] // Always use EN for backend comms }) }); const data = await response.json(); if (data.url) { window.location.href = data.url; } else { showToast(data.message || translations[currentLocale]['error_fetch'], 'error'); } } catch (error) { console.error('Error creating Stripe session:', error); showToast(translations[currentLocale]['error_fetch'], 'error'); } }); cryptoPayBtn.addEventListener('click', async () => { showToast("Generating crypto payment details...", 'info', 5000); try { const response = await fetch(`${API_BASE}/request-crypto`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: WIDGET_SLUG, amount: 199, // $1.99 widgetName: translations['EN']['appName'] }) }); const data = await response.json(); if (data.address && data.network && data.amount && data.paymentId) { openCryptoModal(data.address, data.network, data.amount, data.paymentId); } else { showToast(data.message || translations[currentLocale]['error_fetch'], 'error'); } } catch (error) { console.error('Error requesting crypto payment:', error); showToast(translations[currentLocale]['error_fetch'], 'error'); } }); authForm.addEventListener('submit', handleAuthSubmit); googleAuthBtn.addEventListener('click', signInWithGoogle); toggleAuthModeBtn.addEventListener('click', () => openAuthModal(!isAuthLoginMode)); function openCryptoModal(address, network, amount, paymentId) { cryptoQrCodeDiv.innerHTML = `QR Code for Crypto Payment`; cryptoAmountSpan.textContent = amount / 100; // Convert cents to dollars cryptoNetworkSpan.textContent = network; cryptoAddressSpan.textContent = address; cryptoTxIdSpan.textContent = paymentId; cryptoPaymentStatus.textContent = translations[currentLocale]['cryptoStatusPending']; cryptoPaymentStatus.style.color = 'var(--primary-accent)'; cryptoPaymentModal.classList.add('active'); // Start polling for payment status cryptoPollingInterval = setInterval(() => { checkCryptoPaymentStatus(paymentId); }, 5000); // Poll every 5 seconds } function closeCryptoModal() { cryptoPaymentModal.classList.remove('active'); clearInterval(cryptoPollingInterval); } async function checkCryptoPaymentStatus(paymentId) { cryptoPaymentStatus.textContent = translations[currentLocale]['cryptoStatusVerifying']; cryptoPaymentStatus.style.color = 'var(--secondary-accent)'; try { const response = await fetch(`${API_BASE}/verify-crypto?paymentId=${paymentId}`); const data = await response.json(); if (data.status === 'success') { clearInterval(cryptoPollingInterval); unlockWidget(); closeCryptoModal(); } else if (data.status === 'failed') { clearInterval(cryptoPollingInterval); cryptoPaymentStatus.textContent = translations[currentLocale]['cryptoStatusFailed']; cryptoPaymentStatus.style.color = 'var(--error-color)'; } // If status is 'pending', keep polling } catch (error) { console.error('Error verifying crypto payment:', error); // Continue polling, but show an error toast showToast(translations[currentLocale]['error_fetch'], 'error'); } } function copyToClipboard(elementId) { const element = document.getElementById(elementId); const text = element.textContent; navigator.clipboard.writeText(text).then(() => { showToast('Copied to clipboard!', 'success'); }).catch(err => { console.error('Failed to copy: ', err); showToast('Failed to copy.', 'error'); }); } // --- Initial Load Checks --- document.addEventListener('DOMContentLoaded', () => { updateTexts(); // Apply initial translations addColumn('ID', 'numeric'); addColumn('Name', 'text'); addColumn('Status', 'boolean'); addColumn('Date', 'date'); // Check for Stripe redirect success const urlParams = new URLSearchParams(window.location.search); const stripeStatus = urlParams.get('status'); const stripeSessionId = urlParams.get('session_id'); const subscriptionStatus = urlParams.get('subscription_status'); const checkStripeRedirect = async () => { if (stripeStatus === 'success' && stripeSessionId) { showToast('Verifying payment...', 'info', 10000); try { const response = await fetch(`${API_BASE}/verify-session?session_id=${stripeSessionId}`); const data = await response.json(); if (data.verified) { unlockWidget(); showToast('Payment successful! Widget unlocked.', 'success'); } else { showToast('Payment verification failed.', 'error'); } } catch (error) { console.error('Error verifying Stripe session:', error); showToast(translations[currentLocale]['error_fetch'], 'error'); } finally { // Clean up URL window.history.replaceState({}, document.title, window.location.pathname); } } else if (subscriptionStatus === 'success' && stripeSessionId) { showToast('Verifying subscription...', 'info', 10000); // Let Firebase auth listener handle the actual subscription check // The user needs to be logged in for this to work. // The onAuthStateChanged will handle the final unlock based on backend check. setTimeout(() => { window.history.replaceState({}, document.title, window.location.pathname); if (!isUnlocked()) { showToast("Subscription payment successful, but activation might be delayed or requires login. Please log in.", 'info'); } }, 3000); // Give some time for Firebase listener to catch up } }; checkStripeRedirect(); // Firebase Auth State Listener auth.onAuthStateChanged(async user => { if (user) { console.log("User is signed in:", user.email); hubSubscribeBtn.textContent = translations[currentLocale]['paymentCard1_btn_loggedIn']; // Check subscription status try { const response = await fetch(`${API_BASE}/check-subscription?email=${encodeURIComponent(user.email)}`); const data = await response.json(); if (data.active) { unlockWidget(); console.log("Subscription active. Widget unlocked."); } else { console.log("User is signed in, but no active subscription."); if (!isUnlocked() && getUsageCount() >= 3) { showPaymentOverlay(); } } } catch (error) { console.error('Error checking subscription:', error); showToast(translations[currentLocale]['error_fetch'], 'error'); if (!isUnlocked() && getUsageCount() >= 3) { showPaymentOverlay(); // Show overlay if API fails and user is over limit } } } else { console.log("No user signed in."); hubSubscribeBtn.textContent = translations[currentLocale]['paymentCard1_btn']; if (!isUnlocked() && getUsageCount() >= 3) { showPaymentOverlay(); } } }); // Initial check for unlock status and usage limit if (isUnlocked()) { console.log("Widget is permanently unlocked from localStorage."); hidePaymentOverlay(); } else if (getUsageCount() >= 3) { console.log("Free usage limit reached. Showing payment overlay."); showPaymentOverlay(); } });