Autosave: 20260225-175004
This commit is contained in:
parent
d44d584918
commit
a58c03c1f9
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
BIN
assets/pasted-20260225-124200-16a7c097.jpg
Normal file
BIN
assets/pasted-20260225-124200-16a7c097.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 619 KiB |
16
db/migrations/20260225_security_updates.sql
Normal file
16
db/migrations/20260225_security_updates.sql
Normal 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`)
|
||||
);
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
72
views/admin/users/index.php
Normal file
72
views/admin/users/index.php
Normal 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'; ?>
|
||||
@ -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'; ?>
|
||||
|
||||
@ -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">
|
||||
@ -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'; ?>
|
||||
Loading…
x
Reference in New Issue
Block a user