Autosave: 20260225-175004

This commit is contained in:
Flatlogic Bot 2026-02-25 17:50:05 +00:00
parent d44d584918
commit a58c03c1f9
12 changed files with 330 additions and 128 deletions

View File

@ -64,6 +64,22 @@ class AdminController extends Controller {
return $db->query("SELECT SUM(total_downloads) FROM apks")->fetchColumn() ?: 0;
}
// Member Management
public function users() {
$this->checkAuth();
$db = db_pdo();
$users = $db->query("SELECT * FROM users ORDER BY created_at DESC")->fetchAll();
$this->view('admin/users/index', ['users' => $users]);
}
public function toggleBan($params) {
$this->checkAuth();
$db = db_pdo();
$stmt = $db->prepare("UPDATE users SET is_banned = NOT is_banned WHERE id = ? AND role != 'admin'");
$stmt->execute([$params['id']]);
$this->redirect('/admin/users');
}
// APK Management
public function apks() {
$this->checkAuth();
@ -345,4 +361,4 @@ class AdminController extends Controller {
$text = strtolower($text);
return empty($text) ? 'n-a' : $text;
}
}
}

View File

@ -25,18 +25,67 @@ class AuthController extends Controller {
public function login() {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$ip = get_client_ip();
$db = db_pdo();
$stmt = $db->prepare("SELECT * FROM users WHERE username = ? AND role = 'user'");
// Anti-Brute Force check by IP (for non-existent users)
$stmt = $db->prepare("SELECT attempts, last_attempt FROM login_logs WHERE ip_address = ?");
$stmt->execute([$ip]);
$ip_log = $stmt->fetch();
if ($ip_log && $ip_log['attempts'] >= 10 && (time() - strtotime($ip_log['last_attempt'])) < 900) {
$this->view('auth/login', ['error' => 'Too many failed attempts from this IP. Please try again in 15 minutes.']);
return;
}
$stmt = $db->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$this->redirect('/profile');
if ($user) {
// Check if account is banned
if ($user['is_banned']) {
$this->view('auth/login', ['error' => 'Your account has been banned. Please contact support.']);
return;
}
// Check brute force for specific user
if ($user['login_attempts'] >= 5 && (time() - strtotime($user['last_attempt_time'])) < 900) {
$this->view('auth/login', ['error' => 'Too many failed attempts. Please try again in 15 minutes.']);
return;
}
if (password_verify($password, $user['password'])) {
// Reset attempts
$stmt = $db->prepare("UPDATE users SET login_attempts = 0, last_ip = ? WHERE id = ?");
$stmt->execute([$ip, $user['id']]);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
if ($user['role'] === 'admin') {
$this->redirect('/admin');
} else {
$this->redirect('/profile');
}
} else {
// Increment attempts
$stmt = $db->prepare("UPDATE users SET login_attempts = login_attempts + 1, last_attempt_time = NOW() WHERE id = ?");
$stmt->execute([$user['id']]);
$this->view('auth/login', ['error' => __('error_invalid_login')]);
}
} else {
// Log failed attempt by IP
if ($ip_log) {
$stmt = $db->prepare("UPDATE login_logs SET attempts = attempts + 1, last_attempt = NOW() WHERE ip_address = ?");
$stmt->execute([$ip]);
} else {
$stmt = $db->prepare("INSERT INTO login_logs (ip_address, attempts) VALUES (?, 1)");
$stmt->execute([$ip]);
}
$this->view('auth/login', ['error' => __('error_invalid_login')]);
}
}
@ -46,6 +95,15 @@ class AuthController extends Controller {
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
$ref_code = $_POST['ref_code'] ?? '';
$honeypot = $_POST['full_name'] ?? ''; // Hidden field
$ip = get_client_ip();
// Bot protection (Honeypot)
if (!empty($honeypot)) {
// Silent fail or show error
$this->view('auth/register', ['error' => 'Bot detected.', 'ref' => $ref_code]);
return;
}
if ($password !== $confirm_password) {
$this->view('auth/register', ['error' => __('error_password_mismatch'), 'ref' => $ref_code]);
@ -54,6 +112,14 @@ class AuthController extends Controller {
$db = db_pdo();
// Multi-account check (Anti-bot/Anti-cheat)
$stmt = $db->prepare("SELECT COUNT(*) FROM users WHERE registration_ip = ? AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)");
$stmt->execute([$ip]);
if ($stmt->fetchColumn() >= 3) {
$this->view('auth/register', ['error' => 'Too many registrations from this IP. Please try again later.', 'ref' => $ref_code]);
return;
}
// Check if username exists
$stmt = $db->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
@ -72,15 +138,27 @@ class AuthController extends Controller {
$referrer = $stmt->fetch();
if ($referrer) {
$referred_by = $referrer['id'];
// Anti-self referral check
$stmt_ip = $db->prepare("SELECT registration_ip FROM users WHERE id = ?");
$stmt_ip->execute([$referred_by]);
$referrer_ip = $stmt_ip->fetchColumn();
if ($referrer_ip === $ip) {
// Possible self-referral, mark but allow or block?
// Let's block if IP matches exactly
$this->view('auth/register', ['error' => 'Self-referral is not allowed.', 'ref' => $ref_code]);
return;
}
}
}
$stmt = $db->prepare("INSERT INTO users (username, password, referral_code, referred_by, role, balance) VALUES (?, ?, ?, ?, 'user', 0)");
$stmt->execute([$username, $hashed_password, $referral_code, $referred_by]);
$stmt = $db->prepare("INSERT INTO users (username, password, referral_code, referred_by, role, balance, registration_ip, last_ip) VALUES (?, ?, ?, ?, 'user', 0, ?, ?)");
$stmt->execute([$username, $hashed_password, $referral_code, $referred_by, $ip, $ip]);
$userId = $db->lastInsertId();
if ($referred_by) {
// Reward referrer with points (not balance yet, as per previous logic)
// Reward referrer
$stmt = $db->prepare("UPDATE users SET points = points + 10, total_referrals = total_referrals + 1 WHERE id = ?");
$stmt->execute([$referred_by]);
}
@ -155,4 +233,4 @@ class AuthController extends Controller {
$_SESSION['success'] = __('success_withdraw_submitted');
$this->redirect('/profile');
}
}
}

View File

@ -47,4 +47,31 @@ function compress_image($source, $destination, $quality) {
imagejpeg($image, $destination, $quality);
return $destination;
}
}
function get_client_ip() {
$ipaddress = '';
if (isset($_SERVER['HTTP_CF_CONNECTING_IP']))
$ipaddress = $_SERVER['HTTP_CF_CONNECTING_IP'];
else if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
$ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
else if(isset($_SERVER['HTTP_X_FORWARDED']))
$ipaddress = $_SERVER['HTTP_X_FORWARDED'];
else if(isset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']))
$ipaddress = $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
else if(isset($_SERVER['HTTP_FORWARDED_FOR']))
$ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
else if(isset($_SERVER['HTTP_FORWARDED']))
$ipaddress = $_SERVER['HTTP_FORWARDED'];
else if(isset($_SERVER['REMOTE_ADDR']))
$ipaddress = $_SERVER['REMOTE_ADDR'];
else
$ipaddress = 'UNKNOWN';
if (strpos($ipaddress, ',') !== false) {
$ips = explode(',', $ipaddress);
$ipaddress = trim($ips[0]);
}
return $ipaddress;
}

View File

@ -1,9 +1,29 @@
body {
font-family: 'Inter', sans-serif;
background-color: #F8FAFC;
/* Animated gradient background for a subtle dynamic feel */
background: linear-gradient(-45deg, #ffffff, #f8fafc, #f1f5f9, #f8fafc);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
background-attachment: fixed;
color: #1E293B;
position: relative;
min-height: 100vh;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Background blobs are visible and animated */
.bg-blob {
display: block !important;
pointer-events: none;
}
.opacity-10 { opacity: 0.1; }
.hover-lift {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
@ -48,72 +68,56 @@ body {
background-color: #10B981 !important;
}
.bg-primary {
background-color: #3B82F6 !important;
}
.bg-warning {
background-color: #F59E0B !important;
}
.bg-success-subtle {
background-color: #ECFDF5 !important;
}
.btn-white {
background-color: #FFFFFF;
color: #10B981;
border: none;
}
.btn-white:hover {
background-color: #F8FAFC;
color: #059669;
}
.rounded-4 { border-radius: 1rem !important; }
.rounded-5 { border-radius: 1.5rem !important; }
.navbar-brand i {
font-size: 1.5rem;
vertical-align: middle;
/* Navbar styling with glassmorphism */
.navbar {
background: rgba(255, 255, 255, 0.8) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
/* Specific styles for APK Detail matching the image */
.breadcrumb-item + .breadcrumb-item::before {
content: "/";
.navbar-brand {
font-size: 1.25rem;
}
.x-small {
font-size: 0.75rem;
/* Card styling */
.card {
border: none;
background-color: #FFFFFF;
}
.bg-light {
background-color: #F1F5F9 !important;
}
.shadow-sm {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;
}
.badge {
font-weight: 500;
}
.border-success-subtle {
border-color: #A7F3D0 !important;
}
.border-info-subtle {
border-color: #BAE6FD !important;
}
/* Mobile adjustments */
/* Mobile adjustments to match the screenshot */
@media (max-width: 767.98px) {
.container {
padding-left: 1.25rem;
padding-right: 1.25rem;
.display-4 {
font-size: 2rem !important;
line-height: 1.2;
}
.display-5 {
font-size: 1.75rem;
.lead {
font-size: 1rem;
}
.btn-lg {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
.card-title {
font-size: 0.85rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

View File

@ -0,0 +1,16 @@
-- Add security and tracking columns to users table
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `is_banned` TINYINT(1) DEFAULT 0;
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `registration_ip` VARCHAR(45) DEFAULT NULL;
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `last_ip` VARCHAR(45) DEFAULT NULL;
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `login_attempts` INT DEFAULT 0;
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `last_attempt_time` TIMESTAMP NULL DEFAULT NULL;
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `registration_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
-- Create a table for brute force tracking by IP (for non-existent users)
CREATE TABLE IF NOT EXISTS `login_logs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`ip_address` VARCHAR(45) NOT NULL,
`attempts` INT DEFAULT 1,
`last_attempt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX (`ip_address`)
);

View File

@ -72,6 +72,10 @@ $router->get('/admin/dashboard', 'AdminController@dashboard');
$router->get('/admin/settings', 'AdminController@settingsForm');
$router->post('/admin/settings', 'AdminController@saveSettings');
// Admin Users
$router->get('/admin/users', 'AdminController@users');
$router->post('/admin/users/toggle-ban/:id', 'AdminController@toggleBan');
// Admin APKs
$router->get('/admin/apks', 'AdminController@apks');
$router->get('/admin/apks/mass-upload', 'AdminController@massUploadForm');
@ -93,4 +97,4 @@ $router->get('/admin/withdrawals', 'AdminController@withdrawals');
$router->get('/admin/withdrawals/approve/:id', 'AdminController@approveWithdrawal');
$router->get('/admin/withdrawals/reject/:id', 'AdminController@rejectWithdrawal');
$router->dispatch();
$router->dispatch();

View File

@ -56,6 +56,9 @@
<li class="nav-item">
<a class="nav-link px-3" href="/admin/dashboard"><i class="fas fa-tachometer-alt me-1"></i> Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link px-3" href="/admin/users"><i class="fas fa-users me-1"></i> Members</a>
</li>
<li class="nav-item">
<a class="nav-link px-3" href="/admin/apks"><i class="fas fa-mobile-alt me-1"></i> APKs</a>
</li>
@ -99,4 +102,4 @@
</div>
</div>
</div>
</nav>
</nav>

View File

@ -0,0 +1,72 @@
<?php require_once __DIR__ . '/../header.php'; ?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0">Member Management</h2>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">User</th>
<th>Status</th>
<th>IP Addresses</th>
<th>Balance</th>
<th>Joined</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 text-primary rounded-circle d-flex align-items-center justify-content-center fw-bold me-3" style="width: 40px; height: 40px;">
<?= strtoupper(substr($user['username'], 0, 1)) ?>
</div>
<div>
<div class="fw-bold"><?= htmlspecialchars($user['username']) ?></div>
<small class="text-muted"><?= $user['role'] ?></small>
</div>
</div>
</td>
<td>
<?php if ($user['is_banned']): ?>
<span class="badge bg-danger">Banned</span>
<?php else: ?>
<span class="badge bg-success">Active</span>
<?php endif; ?>
</td>
<td>
<div class="small">
<strong>Reg IP:</strong> <?= $user['registration_ip'] ?: 'N/A' ?>
<a href="https://ip-api.com/#<?= $user['registration_ip'] ?>" target="_blank" class="text-decoration-none ms-1"><i class="bi bi-geo-alt"></i></a>
<br>
<strong>Last IP:</strong> <?= $user['last_ip'] ?: 'N/A' ?>
<a href="https://ip-api.com/#<?= $user['last_ip'] ?>" target="_blank" class="text-decoration-none ms-1"><i class="bi bi-geo-alt"></i></a>
</div>
</td>
<td>Rp <?= number_format($user['balance'], 0, ',', '.') ?></td>
<td><?= date('d M Y', strtotime($user['created_at'])) ?></td>
<td class="text-end pe-4">
<?php if ($user['role'] !== 'admin'): ?>
<form action="/admin/users/toggle-ban/<?= $user['id'] ?>" method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
<button type="submit" class="btn btn-sm <?= $user['is_banned'] ? 'btn-outline-success' : 'btn-outline-danger' ?>">
<?= $user['is_banned'] ? 'Unban' : 'Ban' ?>
</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/../footer.php'; ?>

View File

@ -11,6 +11,11 @@
<?php endif; ?>
<form action="/register" method="POST">
<!-- Honeypot field - hidden from users -->
<div style="display:none;">
<input type="text" name="full_name" value="">
</div>
<div class="mb-3">
<label for="username" class="form-label"><?php echo __('username'); ?></label>
<input type="text" class="form-control rounded-3" id="username" name="username" required>
@ -40,4 +45,4 @@
</div>
</div>
<?php require_once __DIR__ . '/../footer.php'; ?>
<?php require_once __DIR__ . '/../footer.php'; ?>

View File

@ -12,7 +12,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="icon" type="image/x-icon" href="/<?php echo get_setting('site_favicon'); ?>">
<link rel="stylesheet" href="/assets/css/custom.css">
<link rel="stylesheet" href="/assets/css/custom.css?v=<?php echo time(); ?>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
@ -20,41 +20,47 @@
<?php echo get_setting('head_js'); ?>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom py-3 sticky-top">
<!-- Dynamic background blobs with color-shifting animation -->
<div class="bg-blob" style="position: fixed; top: -10%; left: -10%; width: 45%; height: 45%; border-radius: 50%; filter: blur(100px); z-index: -1; opacity: 0.15; animation: float-blob 25s infinite alternate, color-cycle 30s infinite;"></div>
<div class="bg-blob" style="position: fixed; bottom: -10%; right: -10%; width: 50%; height: 50%; border-radius: 50%; filter: blur(100px); z-index: -1; opacity: 0.15; animation: float-blob 30s infinite alternate-reverse, color-cycle 35s infinite reverse;"></div>
<div class="bg-blob" style="position: fixed; top: 40%; left: 30%; width: 35%; height: 35%; border-radius: 50%; filter: blur(100px); z-index: -1; opacity: 0.1; animation: float-blob 20s infinite alternate, color-cycle 25s infinite 5s;"></div>
<style>
@keyframes float-blob {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(15%, 15%) scale(1.2); }
}
@keyframes color-cycle {
0% { background-color: #10B981; } /* Success Green */
25% { background-color: #3B82F6; } /* Primary Blue */
50% { background-color: #F59E0B; } /* Warning Amber */
75% { background-color: #EC4899; } /* Pink */
100% { background-color: #10B981; }
}
</style>
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom py-3 sticky-top shadow-sm">
<div class="container">
<a class="navbar-brand fw-bold text-success d-flex align-items-center" href="/">
<?php if (get_setting('site_icon')): ?>
<img src="/<?php echo get_setting('site_icon'); ?>" alt="Logo" class="me-2" style="height: 30px;">
<?php else: ?>
<i class="fas fa-robot"></i>
<i class="fas fa-robot me-2"></i>
<?php endif; ?>
<?php echo htmlspecialchars(get_setting('site_name', 'ApkNusa')); ?>
</a>
<!-- Mobile search button -->
<button class="btn d-lg-none ms-auto me-2 text-muted" type="button" data-bs-toggle="collapse" data-bs-target="#headerSearchCollapse">
<i class="bi bi-search"></i>
</button>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<!-- Search bar in navigation -->
<form action="/" method="GET" class="d-none d-lg-flex ms-lg-4 me-auto position-relative" style="max-width: 400px; width: 100%;">
<div class="input-group input-group-sm bg-light rounded-pill px-2">
<span class="input-group-text bg-transparent border-0"><i class="bi bi-search text-muted"></i></span>
<input type="text" name="search" class="form-control bg-transparent border-0 py-2" placeholder="<?php echo __('search', 'Search...'); ?>" value="<?php echo htmlspecialchars($_GET['search'] ?? ''); ?>">
</div>
</form>
<ul class="navbar-nav ms-auto align-items-center">
<li class="nav-item">
<a class="nav-link px-3" href="/"><?php echo __('home'); ?></a>
</li>
<li class="nav-item">
<a class="nav-link px-3" href="#"><?php echo __('categories'); ?></a>
<a class="nav-link px-3" href="#latest"><?php echo __('categories'); ?></a>
</li>
<li class="nav-item dropdown px-3">
@ -75,7 +81,7 @@
</li>
<?php else: ?>
<li class="nav-item ms-lg-3">
<a class="btn btn-outline-dark rounded-pill px-4" href="/login"><?php echo __('login'); ?></a>
<a class="btn btn-outline-dark border-0 px-3" href="/login"><?php echo __('login'); ?></a>
</li>
<li class="nav-item ms-lg-2">
<a class="btn btn-success rounded-pill px-4" href="/register"><?php echo __('register'); ?></a>
@ -84,14 +90,5 @@
</ul>
</div>
</div>
<!-- Mobile Search Collapse -->
<div class="collapse d-lg-none w-100 px-3 mt-2" id="headerSearchCollapse">
<form action="/" method="GET" class="pb-3">
<div class="input-group bg-light rounded-pill px-2">
<span class="input-group-text bg-transparent border-0"><i class="bi bi-search text-muted"></i></span>
<input type="text" name="search" class="form-control bg-transparent border-0 py-2" placeholder="<?php echo __('search', 'Search...'); ?>" value="<?php echo htmlspecialchars($_GET['search'] ?? ''); ?>">
</div>
</form>
</div>
</nav>
<main class="min-vh-100">

View File

@ -1,32 +1,20 @@
<?php include 'header.php'; ?>
<div class="container">
<div class="row align-items-center mb-5 mt-4">
<div class="row align-items-center mb-5 mt-5">
<div class="col-lg-7">
<h1 class="display-4 fw-bold mb-3"><?php echo __('hero_title'); ?></h1>
<p class="lead text-muted mb-4"><?php echo __('hero_subtitle'); ?></p>
<!-- Search bar added here -->
<div class="mb-4">
<form action="/" method="GET" class="position-relative">
<div class="input-group input-group-lg shadow-sm rounded-pill overflow-hidden border">
<span class="input-group-text bg-white border-0 px-4">
<i class="bi bi-search text-success"></i>
</span>
<input type="text" name="search" class="form-control border-0 px-2 py-3 fs-5" placeholder="<?php echo __('search_placeholder', 'Search for apps and games...'); ?>" value="<?php echo htmlspecialchars($_GET['search'] ?? ''); ?>">
<button class="btn btn-success px-5 fw-bold" type="submit"><?php echo __('search', 'Search'); ?></button>
</div>
</form>
</div>
<div class="d-flex gap-2">
<a href="#latest" class="btn btn-success btn-lg px-4 rounded-pill"><?php echo __('explore_apps'); ?></a>
<a href="/register" class="btn btn-outline-dark btn-lg px-4 rounded-pill"><?php echo __('join_referral'); ?></a>
<a href="/register" class="btn btn-outline-dark btn-lg px-4 rounded-pill border-1"><?php echo __('join_referral'); ?></a>
</div>
</div>
<div class="col-lg-5 d-none d-lg-block text-center">
<div class="position-relative">
<div class="bg-success opacity-10 position-absolute rounded-circle" style="width: 400px; height: 400px; top: -50px; right: -50px; z-index: -1;"></div>
<!-- Dynamic Hero Circle -->
<div class="position-absolute rounded-circle" style="width: 400px; height: 400px; top: -50px; right: -50px; z-index: -1; filter: blur(60px); opacity: 0.15; animation: color-cycle 20s infinite, floating 10s infinite ease-in-out;"></div>
<img src="https://img.icons8.com/color/512/android-os.png" class="img-fluid floating-animation" alt="Android APKs" style="max-height: 350px;">
</div>
</div>
@ -42,10 +30,10 @@
<?php endif; ?>
</h2>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle rounded-pill" type="button" data-bs-toggle="dropdown">
<?php echo __('categories'); ?>
<button class="btn btn-white shadow-sm border rounded-pill dropdown-toggle" type="button" data-bs-toggle="dropdown">
<?php echo __('categories'); ?> <i class="bi bi-chevron-down ms-1 small"></i>
</button>
<ul class="dropdown-menu">
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
<li><a class="dropdown-item" href="/"><?php echo __('all_categories'); ?></a></li>
<?php
$db = db();
@ -65,8 +53,7 @@
<a href="/" class="btn btn-outline-success rounded-pill px-4 mt-2"><?php echo __('view_all_apks', 'View All APKs'); ?></a>
</div>
<?php else: ?>
<!-- Grid optimized for 3 columns on desktop and mobile as requested -->
<div class="row g-2 g-md-4">
<div class="row g-3 g-md-4">
<?php foreach ($apks as $apk): ?>
<div class="col-4 col-md-4">
<div class="card h-100 border-0 shadow-sm rounded-4 hover-lift">
@ -75,9 +62,11 @@
<?php
$icon = !empty($apk['icon_path']) ? '/'.$apk['icon_path'] : $apk['image_url'];
?>
<img src="<?php echo $icon; ?>" class="rounded-3 mx-auto mx-md-0 mb-2 mb-md-0 d-block d-md-inline-block" width="50" height="50" alt="<?php echo $apk['title']; ?>" style="object-fit: cover; width: 50px; height: 50px;">
<div class="ms-md-3">
<h6 class="card-title fw-bold mb-0 text-truncate mx-auto" style="max-width: 100%;"><?php echo $apk['title']; ?></h6>
<div class="mx-auto mx-md-0 mb-2 mb-md-0" style="width: 50px; height: 50px;">
<img src="<?php echo $icon; ?>" class="rounded-3" width="50" height="50" alt="<?php echo $apk['title']; ?>" style="object-fit: cover; width: 50px; height: 50px;">
</div>
<div class="ms-md-3 flex-grow-1 overflow-hidden">
<h6 class="card-title fw-bold mb-0 text-truncate"><?php echo $apk['title']; ?></h6>
<span class="badge bg-light text-dark fw-normal d-none d-md-inline-block">v<?php echo $apk['version']; ?></span>
</div>
</div>
@ -94,8 +83,11 @@
<?php endif; ?>
</section>
<div class="bg-dark text-white p-5 rounded-5 mt-5">
<div class="row align-items-center text-center text-lg-start">
<div class="bg-dark text-white p-5 rounded-5 mt-5 mb-5 shadow-lg position-relative overflow-hidden">
<!-- Subtle dark mode blob -->
<div class="position-absolute bg-success opacity-10 rounded-circle" style="width: 300px; height: 300px; bottom: -100px; left: -100px; filter: blur(50px);"></div>
<div class="row align-items-center text-center text-lg-start position-relative">
<div class="col-lg-8">
<h2 class="fw-bold mb-3"><?php echo __('referral_journey_title'); ?></h2>
<p class="mb-0 text-white-50"><?php echo __('referral_journey_text'); ?></p>
@ -107,16 +99,4 @@
</div>
</div>
<style>
@media (max-width: 767px) {
.card-title {
font-size: 0.8rem;
}
.btn-sm {
font-size: 0.7rem;
padding: 0.25rem 0.5rem;
}
}
</style>
<?php include 'footer.php'; ?>