diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php index ebe7bce..1e8cb47 100644 --- a/app/Controllers/AuthController.php +++ b/app/Controllers/AuthController.php @@ -200,15 +200,26 @@ class AuthController extends Controller { public function requestWithdrawal() { if (!isset($_SESSION['user_id'])) { + if (is_ajax()) { + header('Content-Type: application/json'); + echo json_encode(['error' => 'Unauthorized']); + exit; + } $this->redirect('/login'); } - $amount = (float)$_POST['amount']; - $method = $_POST['method']; - $details = $_POST['details']; + $amount = (float)($_POST['amount'] ?? 0); + $method = $_POST['method'] ?? ''; + $details = $_POST['details'] ?? ''; if ($amount < 10000) { // Minimum WD - $_SESSION['error'] = __('error_min_withdraw'); + $error = __('error_min_withdraw'); + if (is_ajax()) { + header('Content-Type: application/json'); + echo json_encode(['error' => $error]); + exit; + } + $_SESSION['error'] = $error; $this->redirect('/profile'); } @@ -218,7 +229,13 @@ class AuthController extends Controller { $balance = $stmt->fetchColumn(); if ($balance < $amount) { - $_SESSION['error'] = __('error_insufficient_balance'); + $error = __('error_insufficient_balance'); + if (is_ajax()) { + header('Content-Type: application/json'); + echo json_encode(['error' => $error]); + exit; + } + $_SESSION['error'] = $error; $this->redirect('/profile'); } @@ -230,7 +247,14 @@ class AuthController extends Controller { $stmt = $db->prepare("INSERT INTO withdrawals (user_id, amount, method, account_details, status) VALUES (?, ?, ?, ?, 'pending')"); $stmt->execute([$_SESSION['user_id'], $amount, $method, $details]); - $_SESSION['success'] = __('success_withdraw_submitted'); + $success = __('success_withdraw_submitted'); + if (is_ajax()) { + header('Content-Type: application/json'); + echo json_encode(['success' => $success, 'new_balance' => $balance - $amount]); + exit; + } + + $_SESSION['success'] = $success; $this->redirect('/profile'); } -} +} \ No newline at end of file diff --git a/app/Controllers/ContactController.php b/app/Controllers/ContactController.php index 6853220..538f458 100644 --- a/app/Controllers/ContactController.php +++ b/app/Controllers/ContactController.php @@ -37,9 +37,43 @@ class ContactController extends Controller { $_SESSION['success'] = 'Your message has been sent successfully!'; } else { $_SESSION['error'] = 'Failed to send message. Please try again later.'; - // Log error if needed: error_log($res['error']); } $this->redirect('/contact'); } -} + + public function ajaxReport() { + if (!is_ajax()) { + $this->redirect('/'); + } + + header('Content-Type: application/json'); + + $email = $_POST['email'] ?? ''; + $subject = $_POST['subject'] ?? 'App Report'; + $message = $_POST['message'] ?? ''; + $apk_name = $_POST['apk_name'] ?? 'Unknown App'; + + if (empty($email) || empty($message)) { + echo json_encode(['error' => 'Email and message are required.']); + exit; + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + echo json_encode(['error' => 'Invalid email address.']); + exit; + } + + require_once __DIR__ . '/../../mail/MailService.php'; + + $full_message = "Report for App: $apk_name\n\nUser Email: $email\n\nMessage:\n$message"; + $res = \MailService::sendContactMessage('System Report', $email, $full_message, null, $subject); + + if (!empty($res['success'])) { + echo json_encode(['success' => 'Report submitted successfully!']); + } else { + echo json_encode(['error' => 'Failed to submit report.']); + } + exit; + } +} \ No newline at end of file diff --git a/app/Helpers/functions.php b/app/Helpers/functions.php index 1dec2e6..887893d 100644 --- a/app/Helpers/functions.php +++ b/app/Helpers/functions.php @@ -75,3 +75,7 @@ function get_client_ip() { return $ipaddress; } + +function is_ajax() { + return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; +} \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index f401f06..558256d 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -362,8 +362,234 @@ body.animated-bg { padding: 0.75rem 1.5rem; font-size: 1rem; } - - .card-title { - font-size: 0.85rem; +} + +/* Mobile Bottom Navigation */ +.mobile-bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 65px; + background: var(--card-bg); + display: flex; + align-items: center; + justify-content: space-around; + box-shadow: 0 -5px 20px rgba(0,0,0,0.1); + z-index: 1040; + border-top: 1px solid var(--border-color); + padding-bottom: env(safe-area-inset-bottom); +} + +.mobile-nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-decoration: none; + color: var(--muted-text); + font-size: 0.7rem; + font-weight: 500; + transition: all 0.2s ease; + flex: 1; +} + +.mobile-nav-item i { + font-size: 1.25rem; + margin-bottom: 4px; +} + +.mobile-nav-item.active { + color: var(--accent-color); +} + +.mobile-nav-item.active i { + transform: translateY(-2px); +} + +/* Sticky Download Button for Mobile */ +.sticky-download-bar { + position: fixed; + bottom: 65px; /* Above mobile-bottom-nav */ + left: 0; + right: 0; + background: var(--card-bg); + padding: 0.75rem 1rem; + box-shadow: 0 -5px 15px rgba(0,0,0,0.05); + z-index: 1030; + border-top: 1px solid var(--border-color); + transform: translateY(100%); + transition: transform 0.3s ease; +} + +.sticky-download-bar.show { + transform: translateY(0); +} + +/* Search Overlay */ +.search-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--bg-color); + z-index: 1100; + display: none; + padding: 2rem 1.5rem; +} + +.search-overlay.active { + display: block; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.search-overlay .btn-close-search { + position: absolute; + top: 1.5rem; + right: 1.5rem; + font-size: 1.5rem; + color: var(--text-color); + background: none; + border: none; +} + +/* Share FAB */ +.share-fab { + position: fixed; + bottom: 80px; + right: 20px; + width: 50px; + height: 50px; + background: var(--accent-color); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4); + z-index: 1020; + text-decoration: none; + transition: all 0.3s ease; +} + +.share-fab:hover { + color: white; + transform: scale(1.1); +} + +/* WhatsApp FAB */ +.whatsapp-fab { + position: fixed; + bottom: 80px; + left: 20px; + width: 50px; + height: 50px; + background: #25D366; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 15px rgba(37, 211, 102, 0.4); + z-index: 1020; + text-decoration: none; + transition: all 0.3s ease; +} + +.whatsapp-fab:hover { + color: white; + transform: scale(1.1); +} + +/* Back to Top */ +.back-to-top { + position: fixed; + bottom: 140px; + right: 20px; + width: 40px; + height: 40px; + background: var(--card-bg); + color: var(--text-color); + border: 1px solid var(--border-color); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + z-index: 1010; + text-decoration: none; + transition: all 0.3s ease; + opacity: 0; + visibility: hidden; +} + +.back-to-top.show { + opacity: 1; + visibility: visible; +} + +/* Category Chips Scroll */ +.category-scroll .btn { + border: none; + transition: all 0.2s ease; +} + +.category-scroll .btn-light { + background: var(--subtle-bg); + color: var(--muted-text); +} + +.category-scroll .btn-light:hover { + background: var(--border-color); + color: var(--text-color); +} + +/* Featured Scroll Hover */ +.featured-scroll .card:hover { + background: var(--subtle-bg); +} + +/* Hide desktop elements on mobile and vice-versa */ +@media (min-width: 992px) { + .mobile-bottom-nav, .sticky-download-bar, .search-overlay, .share-fab, .whatsapp-fab { + display: none !important; } } + +@media (max-width: 991.98px) { + body { + padding-bottom: 70px; /* Space for bottom nav */ + } + + .navbar-brand { + font-size: 1.1rem; + } + + .navbar-toggler { + padding: 0.25rem 0.5rem; + } + + /* Improve card layout on mobile for 3 columns */ + .card-body { + padding: 0.5rem !important; + } + + .card-title { + font-size: 0.75rem !important; + margin-bottom: 0.25rem !important; + } + + .badge { + padding: 0.2rem 0.4rem !important; + } + + #ai-chat-wrapper { + bottom: 75px !important; + } +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index 4a62eb3..5515d1a 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -15,7 +15,6 @@ document.addEventListener('DOMContentLoaded', () => { const html = document.documentElement; const updateIcons = (theme) => { - // Update all theme toggle icons const icons = document.querySelectorAll('#theme-toggle i, #theme-toggle-mobile i'); icons.forEach(icon => { if (theme === 'dark') { @@ -25,7 +24,6 @@ document.addEventListener('DOMContentLoaded', () => { } }); - // Update all theme status texts const textLabels = document.querySelectorAll('.theme-status-text'); textLabels.forEach(label => { label.textContent = theme === 'dark' ? 'Dark Mode' : 'Light Mode'; @@ -40,71 +38,140 @@ document.addEventListener('DOMContentLoaded', () => { themeToggle.addEventListener('click', () => { const currentTheme = html.getAttribute('data-theme') || 'light'; const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - - // Update UI html.setAttribute('data-theme', newTheme); - - // Update All Icons and Labels updateIcons(newTheme); - - // Save preference document.cookie = `theme=${newTheme}; path=/; max-age=${365 * 24 * 60 * 60}`; localStorage.setItem('theme', newTheme); }); }; + // Unified AJAX Content Loader + const loadContent = (url, updateUrl = true) => { + const gridContainer = document.getElementById('apk-grid-container'); + if (!gridContainer) return; + + gridContainer.style.opacity = '0.5'; + gridContainer.style.pointerEvents = 'none'; + + fetch(url, { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(response => response.text()) + .then(data => { + gridContainer.innerHTML = data; + gridContainer.style.opacity = '1'; + gridContainer.style.pointerEvents = 'all'; + + if (updateUrl) { + window.history.pushState({}, '', url); + } + + // Scroll to grid top on mobile if needed + if (window.innerWidth < 992) { + const latestSection = document.getElementById('latest'); + if (latestSection) { + window.scrollTo({ + top: latestSection.offsetTop - 100, + behavior: 'smooth' + }); + } + } + }) + .catch(err => { + console.error('Fetch error:', err); + gridContainer.style.opacity = '1'; + gridContainer.style.pointerEvents = 'all'; + }); + }; + // AJAX Category Filtering const initCategoryAjax = () => { - const filters = document.querySelectorAll('.category-filter'); - const gridContainer = document.getElementById('apk-grid-container'); - const dropdownBtn = document.getElementById('category-dropdown-btn'); - const latestTitle = document.getElementById('latest-title'); + document.addEventListener('click', (e) => { + const filter = e.target.closest('.category-filter, .ajax-cat-link'); + if (!filter) return; - if (!gridContainer || filters.length === 0) return; + e.preventDefault(); + const url = filter.getAttribute('href'); + const categoryName = filter.textContent.trim(); + const dropdownBtn = document.getElementById('category-dropdown-btn'); - filters.forEach(filter => { - filter.addEventListener('click', (e) => { - e.preventDefault(); - - const category = filter.getAttribute('data-category'); - const url = filter.getAttribute('href'); - const categoryName = filter.textContent; + if (dropdownBtn && filter.classList.contains('category-filter')) { + dropdownBtn.innerHTML = `${categoryName} `; + } - // Update UI state - gridContainer.style.opacity = '0.5'; - gridContainer.style.pointerEvents = 'none'; - - // Fetch data - fetch(url, { - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }) - .then(response => response.text()) - .then(data => { - gridContainer.innerHTML = data; - gridContainer.style.opacity = '1'; - gridContainer.style.pointerEvents = 'all'; - - // Update dropdown button text - if (dropdownBtn) { - dropdownBtn.innerHTML = `${categoryName} `; - } - - // Update URL without refreshing - window.history.pushState({ category: category }, '', url); - }) - .catch(err => { - console.error('Fetch error:', err); - gridContainer.style.opacity = '1'; - gridContainer.style.pointerEvents = 'all'; + // Update active state for chips if they are chips + if (filter.classList.contains('ajax-cat-link')) { + document.querySelectorAll('.ajax-cat-link').forEach(btn => { + btn.classList.remove('btn-success'); + btn.classList.add('btn-light'); }); - }); - }); + filter.classList.remove('btn-light'); + filter.classList.add('btn-success'); + } - // Handle browser back/forward - window.addEventListener('popstate', (e) => { - window.location.reload(); // Simple solution for now + loadContent(url); + + // Close search overlay if open + const searchOverlay = document.getElementById('search-overlay'); + if (searchOverlay) searchOverlay.classList.remove('active'); + }); + }; + + // AJAX Search + const initSearchAjax = () => { + const searchForm = document.getElementById('ajax-search-form'); + if (!searchForm) return; + + searchForm.addEventListener('submit', (e) => { + e.preventDefault(); + const formData = new FormData(searchForm); + const query = formData.get('search'); + const url = `/?search=${encodeURIComponent(query)}`; + + loadContent(url); + + const searchOverlay = document.getElementById('search-overlay'); + if (searchOverlay) searchOverlay.classList.remove('active'); + }); + }; + + // Newsletter AJAX + const initNewsletterAjax = () => { + const btn = document.getElementById('newsletter-btn'); + const emailInput = document.getElementById('newsletter-email'); + const msg = document.getElementById('newsletter-msg'); + + if (!btn || !emailInput) return; + + btn.addEventListener('click', () => { + const email = emailInput.value; + if (!email) return; + + btn.disabled = true; + const originalText = btn.innerHTML; + btn.innerHTML = ''; + + fetch('/api/newsletter/subscribe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email }) + }) + .then(res => res.json()) + .then(data => { + btn.disabled = false; + btn.innerHTML = originalText; + if (data.success) { + msg.innerHTML = `${data.success}`; + emailInput.value = ''; + } else { + msg.innerHTML = `${data.error}`; + } + }) + .catch(err => { + btn.disabled = false; + btn.innerHTML = originalText; + msg.innerHTML = 'An error occurred.'; + }); }); }; @@ -137,11 +204,8 @@ document.addEventListener('DOMContentLoaded', () => { const content = document.createElement('div'); content.className = (isUser ? 'bg-success text-white' : 'bg-white') + ' p-3 rounded-4 shadow-sm small'; content.style.maxWidth = '85%'; - if (isUser) { - content.style.borderBottomRightRadius = '0'; - } else { - content.style.borderBottomLeftRadius = '0'; - } + content.style.borderBottomRightRadius = isUser ? '0' : 'inherit'; + content.style.borderBottomLeftRadius = isUser ? 'inherit' : '0'; content.textContent = message; div.appendChild(content); @@ -156,7 +220,6 @@ document.addEventListener('DOMContentLoaded', () => { appendMessage(message, true); chatInput.value = ''; - // Loading state const loadingDiv = document.createElement('div'); loadingDiv.className = 'mb-3 d-flex'; loadingDiv.innerHTML = '
Thinking...
'; @@ -165,9 +228,7 @@ document.addEventListener('DOMContentLoaded', () => { fetch('/api/ai/chat', { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: message }) }) .then(response => response.json()) @@ -180,7 +241,7 @@ document.addEventListener('DOMContentLoaded', () => { } }) .catch(err => { - messagesContainer.removeChild(loadingDiv); + if (messagesContainer.contains(loadingDiv)) messagesContainer.removeChild(loadingDiv); appendMessage('Error connecting to AI assistant.'); console.error(err); }); @@ -192,24 +253,60 @@ document.addEventListener('DOMContentLoaded', () => { }); }; + // Mobile Overlays & Utils + const initMobileUtils = () => { + const searchTrigger = document.getElementById('mobile-search-trigger'); + const searchOverlay = document.getElementById('search-overlay'); + const closeSearch = document.getElementById('close-search-overlay'); + + if (searchTrigger && searchOverlay) { + searchTrigger.addEventListener('click', (e) => { + e.preventDefault(); + searchOverlay.classList.add('active'); + const input = searchOverlay.querySelector('input'); + if (input) setTimeout(() => input.focus(), 300); + }); + } + + if (closeSearch && searchOverlay) { + closeSearch.addEventListener('click', () => { + searchOverlay.classList.remove('active'); + }); + } + + // Back to Top + const backToTop = document.getElementById('back-to-top'); + if (backToTop) { + window.addEventListener('scroll', () => { + if (window.pageYOffset > 300) { + backToTop.classList.add('show'); + } else { + backToTop.classList.remove('show'); + } + }); + backToTop.addEventListener('click', (e) => { + e.preventDefault(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + } + }; + // Initial Sync const currentTheme = html.getAttribute('data-theme') || 'light'; updateIcons(currentTheme); - // Sync theme from localStorage if cookie is missing but localStorage has it - const savedTheme = localStorage.getItem('theme'); - const currentCookie = document.cookie.split('; ').find(row => row.startsWith('theme='))?.split('=')[1]; - - if (savedTheme && !currentCookie) { - html.setAttribute('data-theme', savedTheme); - updateIcons(savedTheme); - document.cookie = `theme=${savedTheme}; path=/; max-age=${365 * 24 * 60 * 60}`; - } - initThemeToggle('theme-toggle'); initThemeToggle('theme-toggle-mobile'); initCategoryAjax(); + initSearchAjax(); + initNewsletterAjax(); initAIChat(); + initMobileUtils(); - console.log('ApkNusa ready.'); + // Handle browser back/forward + window.addEventListener('popstate', () => { + loadContent(window.location.href, false); + }); + + console.log('ApkNusa AJAX Engine active.'); }); \ No newline at end of file diff --git a/index.php b/index.php index b740525..16a30a3 100644 --- a/index.php +++ b/index.php @@ -40,6 +40,7 @@ if (get_setting('maintenance_mode') === '1') { $router = new Router(); $router->post('/api/newsletter/subscribe', 'NewsletterController@subscribe'); +$router->post('/api/report', 'ContactController@ajaxReport'); $router->post('/api/ai/chat', 'AIController@chat'); // Sitemap diff --git a/views/apk_detail.php b/views/apk_detail.php index 9aadc6b..f0bfb32 100644 --- a/views/apk_detail.php +++ b/views/apk_detail.php @@ -2,14 +2,15 @@
-