Compare commits

...

27 Commits

Author SHA1 Message Date
Flatlogic Bot
f9fb1438f5 Update mail 2026-03-16 10:14:32 +00:00
Flatlogic Bot
1483602b82 Revert to version afb01a6 2026-02-25 23:45:20 +00:00
Flatlogic Bot
a2cdf745b4 Autosave: 20260225-233522 2026-02-25 23:35:22 +00:00
Flatlogic Bot
afb01a6973 Baru lagi ommm 2026-02-25 23:20:40 +00:00
Flatlogic Bot
b7baac6c0c Revert to version b67ab46 2026-02-25 23:12:07 +00:00
Flatlogic Bot
dc6297b93b Bagus ini 2026-02-25 22:41:29 +00:00
Flatlogic Bot
b67ab46d1e Autosave: 20260225-222606 2026-02-25 22:26:07 +00:00
Flatlogic Bot
3d02f25bbd V baru 2026-02-25 22:06:24 +00:00
Flatlogic Bot
01072acc88 Update blog 2026-02-25 21:10:47 +00:00
Flatlogic Bot
70b6d84e70 Update paling Baru 2026-02-25 21:02:06 +00:00
Flatlogic Bot
e342e6bc0e Autosave: 20260225-203753 2026-02-25 20:37:53 +00:00
Flatlogic Bot
24820a45de Ajaxdll 2026-02-25 19:25:22 +00:00
Flatlogic Bot
a58c03c1f9 Autosave: 20260225-175004 2026-02-25 17:50:05 +00:00
Flatlogic Bot
d44d584918 Andmin akses admin 2026-02-25 12:21:34 +00:00
Flatlogic Bot
e8c4b6fa90 Tampilan slug keren 2026-02-25 12:06:18 +00:00
Flatlogic Bot
777450559e 404 2026-02-25 01:45:26 +00:00
Flatlogic Bot
649064caeb Massal upload 2026-02-25 00:48:51 +00:00
Flatlogic Bot
b610ad13d6 Yang Paling Fix 2026-02-25 00:35:00 +00:00
Flatlogic Bot
00993cf283 Revert to version 612728f 2026-02-25 00:29:28 +00:00
Flatlogic Bot
e29fd2b9e8 Tema 2026-02-25 00:04:12 +00:00
Flatlogic Bot
612728f586 Good game 2026-02-24 23:55:07 +00:00
Flatlogic Bot
b535a47db4 Fix ini 2026-02-24 23:36:27 +00:00
Flatlogic Bot
5a3eb14edc Update bahasa 2026-02-24 23:19:12 +00:00
Flatlogic Bot
2fd69f4642 Googdame 2026-02-24 23:10:29 +00:00
Flatlogic Bot
7cb17c6136 New Vesion Aslam 2026-02-24 23:03:19 +00:00
Flatlogic Bot
4f61082b27 Aslam vbru 2026-02-24 22:45:41 +00:00
Flatlogic Bot
a887a75aed Aslam 2026-02-24 22:20:46 +00:00
81 changed files with 6322 additions and 896 deletions

10
.env Normal file
View File

@ -0,0 +1,10 @@
MAIL_TRANSPORT=smtp
SMTP_HOST=mail.yourdomain.com
SMTP_PORT=587
SMTP_SECURE=tls
SMTP_USER=email@yourdomain.com
SMTP_PASS=password
MAIL_FROM=email@yourdomain.com
MAIL_FROM_NAME="Admin"
MAIL_TO=admin@yourdomain.com
OPENAI_API_KEY=sk-...

9
.htaccess Normal file
View File

@ -0,0 +1,9 @@
RewriteEngine On
RewriteBase /
# Stop processing if file or directory exists
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Redirect all other requests to index.php
RewriteRule ^(.*)$ index.php [L,QSA]

View File

@ -1,58 +1,24 @@
<?php <?php
// LocalAIApi — proxy client for the Responses API. // LocalAIApi — proxy client, now with direct OpenAI support for Shared Hosting.
// Usage (async: auto-polls status until ready):
// require_once __DIR__ . '/ai/LocalAIApi.php';
// $response = LocalAIApi::createResponse([
// 'input' => [
// ['role' => 'system', 'content' => 'You are a helpful assistant.'],
// ['role' => 'user', 'content' => 'Tell me a bedtime story.'],
// ],
// ]);
// if (!empty($response['success'])) {
// // response['data'] contains full payload, e.g.:
// // {
// // "id": "resp_xxx",
// // "status": "completed",
// // "output": [
// // {"type": "reasoning", "summary": []},
// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
// // ]
// // }
// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...)
// }
// Poll settings override:
// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]);
class LocalAIApi class LocalAIApi
{ {
/** @var array<string,mixed>|null */
private static ?array $configCache = null; private static ?array $configCache = null;
/**
* Signature compatible with the OpenAI Responses API.
*
* @param array<string,mixed> $params Request body (model, input, text, reasoning, metadata, etc.).
* @param array<string,mixed> $options Extra options (timeout, verify_tls, headers, path, project_uuid).
* @return array{
* success:bool,
* status?:int,
* data?:mixed,
* error?:string,
* response?:mixed,
* message?:string
* }
*/
public static function createResponse(array $params, array $options = []): array public static function createResponse(array $params, array $options = []): array
{ {
// Check if local key exists (from workspace .env)
$localKey = getenv('OPENAI_API_KEY');
if ($localKey && str_starts_with($localKey, 'sk-')) {
return self::directRequest($params, $localKey);
}
// Default to original proxy flow
$cfg = self::config(); $cfg = self::config();
$payload = $params; $payload = $params;
if (empty($payload['input']) || !is_array($payload['input'])) { if (empty($payload['input']) || !is_array($payload['input'])) {
return [ return ['success' => false, 'error' => 'input_missing', 'message' => 'Parameter "input" is required.'];
'success' => false,
'error' => 'input_missing',
'message' => 'Parameter "input" is required and must be an array.',
];
} }
if (!isset($payload['model']) || $payload['model'] === '') { if (!isset($payload['model']) || $payload['model'] === '') {
@ -60,434 +26,132 @@ class LocalAIApi
} }
$initial = self::request($options['path'] ?? null, $payload, $options); $initial = self::request($options['path'] ?? null, $payload, $options);
if (empty($initial['success'])) { if (empty($initial['success'])) return $initial;
return $initial;
}
// Async flow: if backend returns ai_request_id, poll status until ready
$data = $initial['data'] ?? null; $data = $initial['data'] ?? null;
if (is_array($data) && isset($data['ai_request_id'])) { if (is_array($data) && isset($data['ai_request_id'])) {
$aiRequestId = $data['ai_request_id']; return self::awaitResponse($data['ai_request_id'], $options);
$pollTimeout = isset($options['poll_timeout']) ? (int) $options['poll_timeout'] : 300; // seconds
$pollInterval = isset($options['poll_interval']) ? (int) $options['poll_interval'] : 5; // seconds
return self::awaitResponse($aiRequestId, [
'timeout' => $pollTimeout,
'interval' => $pollInterval,
'headers' => $options['headers'] ?? [],
'timeout_per_call' => $options['timeout'] ?? null,
]);
} }
return $initial; return $initial;
} }
/** private static function directRequest(array $params, string $apiKey): array
* Snake_case alias for createResponse (matches the provided example).
*
* @param array<string,mixed> $params
* @param array<string,mixed> $options
* @return array<string,mixed>
*/
public static function create_response(array $params, array $options = []): array
{ {
return self::createResponse($params, $options); $url = 'https://api.openai.com/v1/chat/completions';
// Map input to messages
$messages = [];
foreach ($params['input'] as $msg) {
$messages[] = ['role' => $msg['role'] ?? 'user', 'content' => $msg['content'] ?? ''];
}
$payload = [
'model' => $params['model'] ?? 'gpt-4o',
'messages' => $messages
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
$result = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) return ['success' => false, 'error' => $err];
$data = json_decode($result, true);
// Wrap to match existing structure
return [
'success' => true,
'data' => [
'choices' => $data['choices'] ?? []
]
];
} }
/** public static function create_response(array $params, array $options = []): array { return self::createResponse($params, $options); }
* Perform a raw request to the AI proxy.
*
* @param string $path Endpoint (may be an absolute URL).
* @param array<string,mixed> $payload JSON payload.
* @param array<string,mixed> $options Additional request options.
* @return array<string,mixed>
*/
public static function request(?string $path = null, array $payload = [], array $options = []): array public static function request(?string $path = null, array $payload = [], array $options = []): array
{ {
$cfg = self::config(); $cfg = self::config();
$projectUuid = $cfg['project_uuid']; $projectUuid = $cfg['project_uuid'];
if (empty($projectUuid)) {
return [
'success' => false,
'error' => 'project_uuid_missing',
'message' => 'PROJECT_UUID is not defined; aborting AI request.',
];
}
$defaultPath = $cfg['responses_path'] ?? null; $defaultPath = $cfg['responses_path'] ?? null;
$resolvedPath = $path ?? ($options['path'] ?? $defaultPath); $resolvedPath = $path ?? ($options['path'] ?? $defaultPath);
if (empty($resolvedPath)) {
return [
'success' => false,
'error' => 'project_id_missing',
'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.',
];
}
$url = self::buildUrl($resolvedPath, $cfg['base_url']); $url = self::buildUrl($resolvedPath, $cfg['base_url']);
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30; $timeout = isset($options['timeout']) ? (int) $options['timeout'] : 30;
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
if ($timeout <= 0) {
$timeout = 30;
}
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true; $headers = ['Content-Type: application/json', 'Accept: application/json', ($cfg['project_header'] ?? 'Project-UUID') . ': ' . $projectUuid];
$verifyTls = array_key_exists('verify_tls', $options) $payload['project_uuid'] = $projectUuid;
? (bool) $options['verify_tls']
: $baseVerifyTls;
$projectHeader = $cfg['project_header']; return self::sendCurl($url, 'POST', json_encode($payload), $headers, $timeout, true);
$headers = [
'Content-Type: application/json',
'Accept: application/json',
];
$headers[] = $projectHeader . ': ' . $projectUuid;
if (!empty($options['headers']) && is_array($options['headers'])) {
foreach ($options['headers'] as $header) {
if (is_string($header) && $header !== '') {
$headers[] = $header;
}
}
}
if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) {
$payload['project_uuid'] = $projectUuid;
}
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
if ($body === false) {
return [
'success' => false,
'error' => 'json_encode_failed',
'message' => 'Failed to encode request body to JSON.',
];
}
return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls);
} }
/**
* Poll AI request status until ready or timeout.
*
* @param int|string $aiRequestId
* @param array<string,mixed> $options
* @return array<string,mixed>
*/
public static function awaitResponse($aiRequestId, array $options = []): array public static function awaitResponse($aiRequestId, array $options = []): array
{ {
$cfg = self::config(); $deadline = time() + 300;
while (time() < $deadline) {
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds $statusResp = self::fetchStatus($aiRequestId);
$interval = isset($options['interval']) ? (int) $options['interval'] : 5; // seconds
if ($interval <= 0) {
$interval = 5;
}
$perCallTimeout = isset($options['timeout_per_call']) ? (int) $options['timeout_per_call'] : null;
$deadline = time() + max($timeout, $interval);
$headers = $options['headers'] ?? [];
while (true) {
$statusResp = self::fetchStatus($aiRequestId, [
'headers' => $headers,
'timeout' => $perCallTimeout,
]);
if (!empty($statusResp['success'])) { if (!empty($statusResp['success'])) {
$data = $statusResp['data'] ?? []; $data = $statusResp['data'];
if (is_array($data)) { if (($data['status'] ?? '') === 'success') return ['success' => true, 'data' => $data['response'] ?? $data];
$statusValue = $data['status'] ?? null; if (($data['status'] ?? '') === 'failed') return ['success' => false, 'error' => 'AI request failed'];
if ($statusValue === 'success') {
return [
'success' => true,
'status' => 200,
'data' => $data['response'] ?? $data,
];
}
if ($statusValue === 'failed') {
return [
'success' => false,
'status' => 500,
'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed',
'data' => $data,
];
}
}
} else {
return $statusResp;
} }
sleep(5);
if (time() >= $deadline) {
return [
'success' => false,
'error' => 'timeout',
'message' => 'Timed out waiting for AI response.',
];
}
sleep($interval);
} }
return ['success' => false, 'error' => 'timeout'];
} }
/** public static function fetchStatus($aiRequestId): array
* Fetch status for queued AI request.
*
* @param int|string $aiRequestId
* @param array<string,mixed> $options
* @return array<string,mixed>
*/
public static function fetchStatus($aiRequestId, array $options = []): array
{ {
$cfg = self::config(); $cfg = self::config();
$projectUuid = $cfg['project_uuid']; $url = self::buildUrl('/ai-request/' . rawurlencode((string)$aiRequestId) . '/status', $cfg['base_url']);
if (empty($projectUuid)) { $headers = [($cfg['project_header'] ?? 'Project-UUID') . ': ' . $cfg['project_uuid']];
return [ return self::sendCurl($url, 'GET', null, $headers, 30, true);
'success' => false,
'error' => 'project_uuid_missing',
'message' => 'PROJECT_UUID is not defined; aborting status check.',
];
}
$statusPath = self::resolveStatusPath($aiRequestId, $cfg);
$url = self::buildUrl($statusPath, $cfg['base_url']);
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
if ($timeout <= 0) {
$timeout = 30;
}
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
$verifyTls = array_key_exists('verify_tls', $options)
? (bool) $options['verify_tls']
: $baseVerifyTls;
$projectHeader = $cfg['project_header'];
$headers = [
'Accept: application/json',
$projectHeader . ': ' . $projectUuid,
];
if (!empty($options['headers']) && is_array($options['headers'])) {
foreach ($options['headers'] as $header) {
if (is_string($header) && $header !== '') {
$headers[] = $header;
}
}
}
return self::sendCurl($url, 'GET', null, $headers, $timeout, $verifyTls);
} }
/**
* Extract plain text from a Responses API payload.
*
* @param array<string,mixed> $response Result of LocalAIApi::createResponse|request.
* @return string
*/
public static function extractText(array $response): string public static function extractText(array $response): string
{ {
$payload = $response['data'] ?? $response; $payload = $response['data'] ?? $response;
if (!is_array($payload)) { if (!empty($payload['choices'][0]['message']['content'])) return (string) $payload['choices'][0]['message']['content'];
return '';
}
if (!empty($payload['output']) && is_array($payload['output'])) {
$combined = '';
foreach ($payload['output'] as $item) {
if (!isset($item['content']) || !is_array($item['content'])) {
continue;
}
foreach ($item['content'] as $block) {
if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) {
$combined .= $block['text'];
}
}
}
if ($combined !== '') {
return $combined;
}
}
if (!empty($payload['choices'][0]['message']['content'])) {
return (string) $payload['choices'][0]['message']['content'];
}
return ''; return '';
} }
/**
* Attempt to decode JSON emitted by the model (handles markdown fences).
*
* @param array<string,mixed> $response
* @return array<string,mixed>|null
*/
public static function decodeJsonFromResponse(array $response): ?array
{
$text = self::extractText($response);
if ($text === '') {
return null;
}
$decoded = json_decode($text, true);
if (is_array($decoded)) {
return $decoded;
}
$stripped = preg_replace('/^```json|```$/m', '', trim($text));
if ($stripped !== null && $stripped !== $text) {
$decoded = json_decode($stripped, true);
if (is_array($decoded)) {
return $decoded;
}
}
return null;
}
/**
* Load configuration from ai/config.php.
*
* @return array<string,mixed>
*/
private static function config(): array private static function config(): array
{ {
if (self::$configCache === null) { if (self::$configCache === null) self::$configCache = require __DIR__ . '/config.php';
$configPath = __DIR__ . '/config.php';
if (!file_exists($configPath)) {
throw new RuntimeException('AI config file not found: ai/config.php');
}
$cfg = require $configPath;
if (!is_array($cfg)) {
throw new RuntimeException('Invalid AI config format: expected array');
}
self::$configCache = $cfg;
}
return self::$configCache; return self::$configCache;
} }
/**
* Build an absolute URL from base_url and a path.
*/
private static function buildUrl(string $path, string $baseUrl): string private static function buildUrl(string $path, string $baseUrl): string
{ {
$trimmed = trim($path); return str_starts_with($path, 'http') ? $path : $baseUrl . '/' . ltrim($path, '/');
if ($trimmed === '') {
return $baseUrl;
}
if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) {
return $trimmed;
}
if ($trimmed[0] === '/') {
return $baseUrl . $trimmed;
}
return $baseUrl . '/' . $trimmed;
} }
/**
* Resolve status path based on configured responses_path and ai_request_id.
*
* @param int|string $aiRequestId
* @param array<string,mixed> $cfg
* @return string
*/
private static function resolveStatusPath($aiRequestId, array $cfg): string
{
$basePath = $cfg['responses_path'] ?? '';
$trimmed = rtrim($basePath, '/');
if ($trimmed === '') {
return '/ai-request/' . rawurlencode((string)$aiRequestId) . '/status';
}
if (substr($trimmed, -11) !== '/ai-request') {
$trimmed .= '/ai-request';
}
return $trimmed . '/' . rawurlencode((string)$aiRequestId) . '/status';
}
/**
* Shared CURL sender for GET/POST requests.
*
* @param string $url
* @param string $method
* @param string|null $body
* @param array<int,string> $headers
* @param int $timeout
* @param bool $verifyTls
* @return array<string,mixed>
*/
private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array
{ {
if (!function_exists('curl_init')) {
return [
'success' => false,
'error' => 'curl_missing',
'message' => 'PHP cURL extension is missing. Install or enable it on the VM.',
];
}
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt_array($ch, [
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); CURLOPT_HTTPHEADER => $headers,
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); CURLOPT_RETURNTRANSFER => true,
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); CURLOPT_TIMEOUT => $timeout,
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls); CURLOPT_SSL_VERIFYPEER => $verifyTls,
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0); ]);
curl_setopt($ch, CURLOPT_FAILONERROR, false); if ($method === 'POST') {
$upper = strtoupper($method);
if ($upper === 'POST') {
curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? ''); curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} else {
curl_setopt($ch, CURLOPT_HTTPGET, true);
} }
$resp = curl_exec($ch);
$responseBody = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($responseBody === false) {
$error = curl_error($ch) ?: 'Unknown cURL error';
curl_close($ch);
return [
'success' => false,
'error' => 'curl_error',
'message' => $error,
];
}
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
return ['success' => ($status >= 200 && $status < 300), 'data' => json_decode($resp, true)];
$decoded = null;
if ($responseBody !== '' && $responseBody !== null) {
$decoded = json_decode($responseBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$decoded = null;
}
}
if ($status >= 200 && $status < 300) {
return [
'success' => true,
'status' => $status,
'data' => $decoded ?? $responseBody,
];
}
$errorMessage = 'AI proxy request failed';
if (is_array($decoded)) {
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage;
} elseif (is_string($responseBody) && $responseBody !== '') {
$errorMessage = $responseBody;
}
return [
'success' => false,
'status' => $status,
'error' => $errorMessage,
'response' => $decoded ?? $responseBody,
];
} }
} }
// Legacy alias for backward compatibility with the previous class name.
if (!class_exists('OpenAIService')) {
class_alias(LocalAIApi::class, 'OpenAIService');
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
class AIController extends Controller {
public function chat() {
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$userMessage = $input['message'] ?? '';
if (empty($userMessage)) {
echo json_encode(['error' => 'Message is empty']);
return;
}
require_once __DIR__ . '/../../ai/LocalAIApi.php';
$systemPrompt = "You are a helpful assistant for " . get_setting('site_name', 'ApkNusa') . ", an APK downloader and tech blog site. Provide concise and accurate information about Android apps, games, and technology. Be youthful and professional.";
$resp = \LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $userMessage],
],
]);
if (!empty($resp['success'])) {
$text = \LocalAIApi::extractText($resp);
echo json_encode(['reply' => $text]);
} else {
echo json_encode(['error' => 'AI Assistant is currently unavailable.']);
}
}
}

View File

@ -0,0 +1,430 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Services\ApkService;
class AdminController extends Controller {
private function checkAuth() {
if (!isset($_SESSION['user_id']) || ($_SESSION['role'] ?? '') !== 'admin') {
$this->redirect('/admin/login');
}
}
public function loginForm() {
if (isset($_SESSION['user_id']) && ($_SESSION['role'] ?? '') === 'admin') {
$this->redirect('/admin/dashboard');
}
$this->view('admin/login');
}
public function login() {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$db = db_pdo();
$stmt = $db->prepare("SELECT * FROM users WHERE username = ? AND role = 'admin'");
$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('/admin/dashboard');
} else {
$error = "Invalid username or password, or you are not an admin";
$this->view('admin/login', ['error' => $error]);
}
}
public function logout() {
session_destroy();
$this->redirect('/admin/login');
}
public function dashboard() {
$this->checkAuth();
$apkService = new ApkService();
$db = db_pdo();
$stats = [
'total_apks' => count($apkService->getAllApks()),
'total_downloads' => $this->getTotalDownloads(),
'total_users' => $db->query("SELECT COUNT(*) FROM users")->fetchColumn(),
'pending_withdrawals' => $db->query("SELECT COUNT(*) FROM withdrawals WHERE status = 'pending'")->fetchColumn(),
'recent_apks' => array_slice($apkService->getAllApks(), 0, 5),
'referral_stats' => $this->getReferralStats()
];
$this->view('admin/dashboard', $stats);
}
private function getReferralStats() {
$db = db_pdo();
$stmt = $db->query("SELECT DATE(created_at) as date, COUNT(*) as count FROM referral_downloads WHERE created_at > DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY DATE(created_at) ORDER BY date ASC");
return $stmt->fetchAll();
}
private function getTotalDownloads() {
$db = db_pdo();
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();
$search = $_GET['search'] ?? null;
$apkService = new ApkService();
$apks = $apkService->getAllApks(null, $search);
$this->view('admin/apks/index', ['apks' => $apks]);
}
public function addApkForm() {
$this->checkAuth();
$db = db_pdo();
$categories = $db->query("SELECT * FROM categories")->fetchAll();
$this->view('admin/apks/form', ['action' => 'add', 'categories' => $categories]);
}
public function addApk() {
$this->checkAuth();
$title = $_POST['title'];
$slug = $this->slugify($title);
$description = $_POST['description'];
$version = $_POST['version'];
$image_url = $_POST['image_url'];
$download_url = $_POST['download_url'];
$category_id = !empty($_POST['category_id']) ? $_POST['category_id'] : null;
$status = $_POST['status'] ?? 'published';
$is_vip = isset($_POST['is_vip']) ? 1 : 0;
$icon_path = $this->handleUpload('icon_file', 'icons');
$db = db_pdo();
$stmt = $db->prepare("INSERT INTO apks (title, slug, description, version, image_url, icon_path, download_url, category_id, status, is_vip, display_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)");
$stmt->execute([$title, $slug, $description, $version, $image_url, $icon_path, $download_url, $category_id, $status, $is_vip]);
$this->redirect('/admin/apks');
}
public function massUploadForm() {
$this->checkAuth();
$db = db_pdo();
$categories = $db->query("SELECT * FROM categories")->fetchAll();
$this->view('admin/apks/mass_upload', ['categories' => $categories]);
}
public function massUpload() {
$this->checkAuth();
$titles = $_POST['titles'] ?? [];
$versions = $_POST['versions'] ?? [];
$download_urls = $_POST['download_urls'] ?? [];
$category_id = !empty($_POST['category_id']) ? $_POST['category_id'] : null;
$status = $_POST['status'] ?? 'published';
$db = db_pdo();
$stmt = $db->prepare("INSERT INTO apks (title, slug, description, version, icon_path, download_url, category_id, status, is_vip, display_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0)");
foreach ($titles as $index => $title) {
if (empty($title)) continue;
$slug = $this->slugify($title);
$version = $versions[$index] ?? '';
$download_url = $download_urls[$index] ?? '';
$description = $title; // Default description to title for mass upload
$icon_path = $this->handleMassUploadFile('icon_files', $index, 'icons');
$stmt->execute([$title, $slug, $description, $version, $icon_path, $download_url, $category_id, $status]);
}
$this->redirect('/admin/apks');
}
private function handleMassUploadFile($field, $index, $dir = 'icons') {
if (!isset($_FILES[$field]['name'][$index]) || $_FILES[$field]['error'][$index] !== UPLOAD_ERR_OK) {
return null;
}
$uploadDir = 'assets/uploads/' . $dir . '/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$ext = pathinfo($_FILES[$field]['name'][$index], PATHINFO_EXTENSION);
$fileName = uniqid() . '.' . $ext;
$targetPath = $uploadDir . $fileName;
if (compress_image($_FILES[$field]['tmp_name'][$index], $targetPath, 75)) {
return $targetPath;
}
return null;
}
public function editApkForm($params) {
$this->checkAuth();
$apkService = new ApkService();
$apk = $apkService->getApkById($params['id']);
$db = db_pdo();
$categories = $db->query("SELECT * FROM categories")->fetchAll();
$this->view('admin/apks/form', ['action' => 'edit', 'apk' => $apk, 'categories' => $categories]);
}
public function editApk($params) {
$this->checkAuth();
$title = $_POST['title'];
$description = $_POST['description'];
$version = $_POST['version'];
$image_url = $_POST['image_url'];
$download_url = $_POST['download_url'];
$category_id = !empty($_POST['category_id']) ? $_POST['category_id'] : null;
$status = $_POST['status'];
$is_vip = isset($_POST['is_vip']) ? 1 : 0;
$db = db_pdo();
$apk = $db->query("SELECT * FROM apks WHERE id = " . $params['id'])->fetch();
$icon_path = $this->handleUpload('icon_file', 'icons') ?: $apk['icon_path'];
$stmt = $db->prepare("UPDATE apks SET title = ?, description = ?, version = ?, image_url = ?, icon_path = ?, download_url = ?, category_id = ?, status = ?, is_vip = ? WHERE id = ?");
$stmt->execute([$title, $description, $version, $image_url, $icon_path, $download_url, $category_id, $status, $is_vip, $params['id']]);
$this->redirect('/admin/apks');
}
public function updateOrder() {
$this->checkAuth();
$order = $_POST['order'] ?? [];
$db = db_pdo();
foreach ($order as $index => $id) {
$stmt = $db->prepare("UPDATE apks SET display_order = ? WHERE id = ?");
$stmt->execute([$index, $id]);
}
header('Content-Type: application/json');
echo json_encode(['success' => true]);
}
private function handleUpload($field, $dir = 'icons') {
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] !== UPLOAD_ERR_OK) {
return null;
}
$uploadDir = 'assets/uploads/' . $dir . '/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$ext = pathinfo($_FILES[$field]['name'], PATHINFO_EXTENSION);
$fileName = uniqid() . '.' . $ext;
$targetPath = $uploadDir . $fileName;
if (compress_image($_FILES[$field]['tmp_name'], $targetPath, 75)) {
return $targetPath;
}
return null;
}
// Settings Management
public function settingsForm() {
$this->checkAuth();
$settings = [
'site_name' => get_setting('site_name'),
'contact_email' => get_setting('contact_email'),
'site_icon' => get_setting('site_icon'),
'site_favicon' => get_setting('site_favicon'),
'meta_description' => get_setting('meta_description'),
'meta_keywords' => get_setting('meta_keywords'),
'head_js' => get_setting('head_js'),
'body_js' => get_setting('body_js'),
'facebook_url' => get_setting('facebook_url'),
'twitter_url' => get_setting('twitter_url'),
'instagram_url' => get_setting('instagram_url'),
'github_url' => get_setting('github_url'),
'telegram_url' => get_setting('telegram_url'),
'whatsapp_url' => get_setting('whatsapp_url'),
'maintenance_mode' => get_setting('maintenance_mode'),
];
$this->view('admin/settings', ['settings' => $settings]);
}
public function saveSettings() {
$this->checkAuth();
$db = db_pdo();
$fields = [
'site_name', 'contact_email', 'meta_description', 'meta_keywords', 'head_js', 'body_js',
'facebook_url', 'twitter_url', 'instagram_url', 'github_url', 'telegram_url', 'whatsapp_url'
];
foreach ($fields as $field) {
if (isset($_POST[$field])) {
$stmt = $db->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = ?");
$stmt->execute([$_POST[$field], $field]);
}
}
$site_icon = $this->handleUpload('site_icon_file', 'settings');
if ($site_icon) {
$stmt = $db->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'site_icon'");
$stmt->execute([$site_icon]);
}
$site_favicon = $this->handleUpload('site_favicon_file', 'settings');
if ($site_favicon) {
$stmt = $db->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'site_favicon'");
$stmt->execute([$site_favicon]);
}
$this->redirect('/admin/settings');
}
// Blog Management
public function posts() {
$this->checkAuth();
$db = db_pdo();
$posts = $db->query("SELECT * FROM posts ORDER BY created_at DESC")->fetchAll();
$this->view('admin/posts/index', ['posts' => $posts]);
}
public function addPostForm() {
$this->checkAuth();
$this->view('admin/posts/form', ['action' => 'add']);
}
public function addPost() {
$this->checkAuth();
$title = $_POST['title'];
$slug = $this->slugify($title);
$content = $_POST['content'];
$status = $_POST['status'] ?? 'published';
$image_path = $this->handleUpload('image_file', 'blog');
$db = db_pdo();
$stmt = $db->prepare("INSERT INTO posts (title, slug, content, image_path, status) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$title, $slug, $content, $image_path, $status]);
$this->redirect('/admin/posts');
}
public function editPostForm($params) {
$this->checkAuth();
$db = db_pdo();
$post = $db->query("SELECT * FROM posts WHERE id = " . $params['id'])->fetch();
$this->view('admin/posts/form', ['action' => 'edit', 'post' => $post]);
}
public function editPost($params) {
$this->checkAuth();
$title = $_POST['title'];
$content = $_POST['content'];
$status = $_POST['status'];
$db = db_pdo();
$post = $db->query("SELECT * FROM posts WHERE id = " . $params['id'])->fetch();
$image_path = $this->handleUpload('image_file', 'blog') ?: $post['image_path'];
$stmt = $db->prepare("UPDATE posts SET title = ?, content = ?, image_path = ?, status = ? WHERE id = ?");
$stmt->execute([$title, $content, $image_path, $status, $params['id']]);
$this->redirect('/admin/posts');
}
public function deletePost($params) {
$this->checkAuth();
$db = db_pdo();
$stmt = $db->prepare("DELETE FROM posts WHERE id = ?");
$stmt->execute([$params['id']]);
$this->redirect('/admin/posts');
}
// Category Management
public function categories() {
$this->checkAuth();
$db = db_pdo();
$categories = $db->query("SELECT * FROM categories")->fetchAll();
$this->view('admin/categories/index', ['categories' => $categories]);
}
public function addCategory() {
$this->checkAuth();
$name = $_POST['name'];
$slug = $this->slugify($name);
$db = db_pdo();
$stmt = $db->prepare("INSERT INTO categories (name, slug) VALUES (?, ?)");
$stmt->execute([$name, $slug]);
$this->redirect('/admin/categories');
}
public function deleteCategory($params) {
$this->checkAuth();
$db = db_pdo();
$stmt = $db->prepare("DELETE FROM categories WHERE id = ?");
$stmt->execute([$params['id']]);
$this->redirect('/admin/categories');
}
// Withdrawal Management
public function withdrawals() {
$this->checkAuth();
$db = db_pdo();
$withdrawals = $db->query("SELECT w.*, u.username FROM withdrawals w JOIN users u ON w.user_id = u.id ORDER BY w.created_at DESC")->fetchAll();
$this->view('admin/withdrawals/index', ['withdrawals' => $withdrawals]);
}
public function approveWithdrawal($params) {
$this->checkAuth();
$db = db_pdo();
$stmt = $db->prepare("UPDATE withdrawals SET status = 'approved' WHERE id = ?");
$stmt->execute([$params['id']]);
$this->redirect('/admin/withdrawals');
}
public function rejectWithdrawal($params) {
$this->checkAuth();
$db = db_pdo();
$wd = $db->query("SELECT * FROM withdrawals WHERE id = " . $params['id'])->fetch();
if ($wd && $wd['status'] === 'pending') {
$stmt = $db->prepare("UPDATE users SET balance = balance + ? WHERE id = ?");
$stmt->execute([$wd['amount'], $wd['user_id']]);
$stmt = $db->prepare("UPDATE withdrawals SET status = 'rejected' WHERE id = ?");
$stmt->execute([$params['id']]);
}
$this->redirect('/admin/withdrawals');
}
public function deleteApk($params) {
$this->checkAuth();
$db = db_pdo();
$stmt = $db->prepare("DELETE FROM apks WHERE id = ?");
$stmt->execute([$params['id']]);
$this->redirect('/admin/apks');
}
private function slugify($text) {
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
$text = preg_replace('~[^-\w]+~', '', $text);
$text = trim($text, '-');
$text = preg_replace('~-+~', '-', $text);
$text = strtolower($text);
return empty($text) ? 'n-a' : $text;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Services\ApkService;
class ApkController extends Controller {
protected $apkService;
public function __construct() {
$this->apkService = new ApkService();
}
public function detail($params) {
$apk = $this->apkService->getBySlug($params['slug']);
if (!$apk) {
header("HTTP/1.0 404 Not Found");
$this->view("404");
return;
}
$siteName = get_setting('site_name', 'ApkNusa');
return $this->view('apk_detail', [
'apk' => $apk,
'title' => sprintf(__('apk_detail_title'), $apk['title'], $apk['version'], $siteName)
]);
}
public function download($params) {
$apk = $this->apkService->getBySlug($params['slug']);
if (!$apk) {
header("HTTP/1.0 404 Not Found");
$this->view("404");
return;
}
// Increment download counter
$this->apkService->incrementDownload($apk['id']);
// Get the download URL
$downloadUrl = $apk['download_url'];
// If URL is empty or #, redirect back to detail with a message
if (empty($downloadUrl) || $downloadUrl === '#') {
$this->redirect('/apk/' . $apk['slug'] . '?error=no_url');
return;
}
// Redirect to the actual download link (External URL)
$this->redirect($downloadUrl);
}
}

View File

@ -0,0 +1,260 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
class AuthController extends Controller {
public function loginForm() {
if (isset($_SESSION['user_id'])) {
$this->redirect('/profile');
}
$this->view('auth/login');
}
public function registerForm() {
if (isset($_SESSION['user_id'])) {
$this->redirect('/profile');
}
// Check GET first, then Session
$ref = $_GET['ref'] ?? ($_SESSION['global_ref'] ?? '');
$this->view('auth/register', ['ref' => $ref]);
}
public function login() {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$ip = get_client_ip();
$db = db_pdo();
// 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) {
// 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')]);
}
}
public function register() {
$username = $_POST['username'] ?? '';
$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]);
return;
}
$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]);
if ($stmt->fetch()) {
$this->view('auth/register', ['error' => __('error_username_exists'), 'ref' => $ref_code]);
return;
}
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$referral_code = substr(md5(uniqid($username, true)), 0, 8);
$referred_by = null;
if (!empty($ref_code)) {
$stmt = $db->prepare("SELECT id FROM users WHERE referral_code = ?");
$stmt->execute([$ref_code]);
$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, 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
$stmt = $db->prepare("UPDATE users SET points = points + 10, total_referrals = total_referrals + 1 WHERE id = ?");
$stmt->execute([$referred_by]);
}
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $username;
$_SESSION['role'] = 'user';
$this->redirect('/profile');
}
public function logout() {
session_destroy();
$this->redirect('/');
}
public function profile() {
if (!isset($_SESSION['user_id'])) {
$this->redirect('/login');
}
$db = db_pdo();
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
$stmt = $db->prepare("SELECT * FROM withdrawals WHERE user_id = ? ORDER BY created_at DESC");
$stmt->execute([$user['id']]);
$withdrawals = $stmt->fetchAll();
$this->view('auth/profile', [
'user' => $user,
'withdrawals' => $withdrawals,
'success' => $_SESSION['success'] ?? null,
'error' => $_SESSION['error'] ?? null
]);
unset($_SESSION['success'], $_SESSION['error']);
}
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'] ?? 0);
$method = $_POST['method'] ?? '';
$details = $_POST['details'] ?? '';
if ($amount < 10000) { // Minimum WD
$error = __('error_min_withdraw');
if (is_ajax()) {
header('Content-Type: application/json');
echo json_encode(['error' => $error]);
exit;
}
$_SESSION['error'] = $error;
$this->redirect('/profile');
}
$db = db_pdo();
$stmt = $db->prepare("SELECT balance FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$balance = $stmt->fetchColumn();
if ($balance < $amount) {
$error = __('error_insufficient_balance');
if (is_ajax()) {
header('Content-Type: application/json');
echo json_encode(['error' => $error]);
exit;
}
$_SESSION['error'] = $error;
$this->redirect('/profile');
}
// Deduct balance
$stmt = $db->prepare("UPDATE users SET balance = balance - ? WHERE id = ?");
$stmt->execute([$amount, $_SESSION['user_id']]);
// Create WD request
$stmt = $db->prepare("INSERT INTO withdrawals (user_id, amount, method, account_details, status) VALUES (?, ?, ?, ?, 'pending')");
$stmt->execute([$_SESSION['user_id'], $amount, $method, $details]);
$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');
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use PDO;
class BlogController extends Controller {
public function index() {
$db = db_pdo();
$stmt = $db->prepare("SELECT * FROM posts ORDER BY created_at DESC");
$stmt->execute();
$blogPosts = $stmt->fetchAll(PDO::FETCH_ASSOC);
$this->view('blog/index', ['blogPosts' => $blogPosts]);
}
public function detail($params) {
$slug = $params['slug'];
$db = db_pdo();
$stmt = $db->prepare("SELECT * FROM posts WHERE slug = ?");
$stmt->execute([$slug]);
$post = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$post) {
$this->view('404');
return;
}
$this->view('blog/detail', ['post' => $post]);
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use MailService;
class ContactController extends Controller {
public function index() {
$this->view('contact', [
'title' => __('contact_us') . ' - ' . get_setting('site_name', 'ApkNusa')
]);
}
public function submit() {
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
$subject = $_POST['subject'] ?? 'New Contact Message';
$message = $_POST['message'] ?? '';
if (empty($name) || empty($email) || empty($message)) {
$_SESSION['error'] = 'All fields are required.';
$this->redirect('/contact');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$_SESSION['error'] = 'Invalid email address.';
$this->redirect('/contact');
}
require_once __DIR__ . '/../../mail/MailService.php';
$res = \MailService::sendContactMessage($name, $email, $message, null, $subject);
if (!empty($res['success'])) {
$_SESSION['success'] = 'Your message has been sent successfully!';
} else {
$_SESSION['error'] = 'Failed to send message. Please try again later.';
}
$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;
}
}

View File

@ -0,0 +1,147 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Services\ApkService;
class HomeController extends Controller {
public function index() {
$category = $_GET['category'] ?? null;
$search = $_GET['search'] ?? null;
// Store referral code if present in landing
if (isset($_GET['ref'])) {
$_SESSION['global_ref'] = $_GET['ref'];
}
$apkService = new ApkService();
$apks = $apkService->getAllApks($category, $search);
// Handle AJAX requests for filtering/searching
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
$this->view('partials/apk_list', [
'apks' => $apks
]);
return;
}
$this->view('home', [
'apks' => $apks,
'title' => get_setting('site_name', 'ApkNusa') . __('home_title_suffix')
]);
}
public function apkDetail($params) {
$slug = $params['slug'];
$db = db_pdo();
$stmt = $db->prepare("SELECT * FROM apks WHERE slug = ?");
$stmt->execute([$slug]);
$apk = $stmt->fetch();
if (!$apk) {
if (ob_get_level() > 0) ob_clean();
header("HTTP/1.0 404 Not Found");
$this->view('404');
return;
}
// Store referral code if present specifically for this APK or take from global session
if (isset($_GET['ref'])) {
$_SESSION['ref_download_' . $apk['id']] = $_GET['ref'];
} elseif (isset($_SESSION['global_ref'])) {
$_SESSION['ref_download_' . $apk['id']] = $_SESSION['global_ref'];
}
$this->view('apk_detail', [
'apk' => $apk,
'title' => $apk['title'] . ' - ' . get_setting('site_name', 'ApkNusa')
]);
}
public function download($params) {
$slug = $params['slug'];
$db = db_pdo();
$stmt = $db->prepare("SELECT * FROM apks WHERE slug = ?");
$stmt->execute([$slug]);
$apk = $stmt->fetch();
if (!$apk) {
if (ob_get_level() > 0) ob_clean();
header("HTTP/1.0 404 Not Found");
$this->view('404');
return;
}
// Increment download count
$stmt = $db->prepare("UPDATE apks SET total_downloads = total_downloads + 1 WHERE id = ?");
$stmt->execute([$apk['id']]);
// Referral logic & Anti-Cheat
$ref_key = 'ref_download_' . $apk['id'];
// If not set for this specific APK, try global referral
if (!isset($_SESSION[$ref_key]) && isset($_SESSION['global_ref'])) {
$_SESSION[$ref_key] = $_SESSION['global_ref'];
}
if (isset($_SESSION[$ref_key])) {
$ref_code = $_SESSION[$ref_key];
$ip_address = $_SERVER['REMOTE_ADDR'];
// Find the user who owns this referral code
$stmt = $db->prepare("SELECT * FROM users WHERE referral_code = ?");
$stmt->execute([$ref_code]);
$referrer = $stmt->fetch();
if ($referrer) {
// Anti-Cheat: Check if this IP has already downloaded this APK for THIS referrer today
$stmt = $db->prepare("SELECT COUNT(*) FROM referral_downloads WHERE referrer_id = ? AND apk_id = ? AND ip_address = ? AND created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)");
$stmt->execute([$referrer['id'], $apk['id'], $ip_address]);
$already_downloaded = $stmt->fetchColumn();
// Anti-Cheat: Check general download frequency from this IP (max 10 rewarded downloads per IP per 24h across all APKs)
$stmt = $db->prepare("SELECT COUNT(*) FROM referral_downloads WHERE ip_address = ? AND created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)");
$stmt->execute([$ip_address]);
$ip_total_daily = $stmt->fetchColumn();
if ($already_downloaded == 0 && $ip_total_daily < 10) {
// Reward amount
$reward_amount = 500.00;
// Record the referral download
$stmt = $db->prepare("INSERT INTO referral_downloads (referrer_id, apk_id, ip_address, amount) VALUES (?, ?, ?, ?)");
$stmt->execute([$referrer['id'], $apk['id'], $ip_address, $reward_amount]);
// Award balance to referrer
$stmt = $db->prepare("UPDATE users SET balance = balance + ? WHERE id = ?");
$stmt->execute([$reward_amount, $referrer['id']]);
}
}
unset($_SESSION[$ref_key]);
}
header('Location: ' . $apk['download_url']);
exit;
}
public function helpCenter() {
$this->view('help_center', [
'title' => __('help_center') . ' - ' . get_setting('site_name', 'ApkNusa')
]);
}
public function privacyPolicy() {
$this->view('privacy_policy', [
'title' => __('privacy_policy') . ' - ' . get_setting('site_name', 'ApkNusa')
]);
}
public function termsOfService() {
$this->view('terms_of_service', [
'title' => __('terms_of_service') . ' - ' . get_setting('site_name', 'ApkNusa')
]);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
class NewsletterController extends Controller {
public function subscribe() {
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$email = $input['email'] ?? '';
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['error' => __('newsletter_invalid_email', 'Please provide a valid email address.')]);
return;
}
$db = db_pdo();
try {
$stmt = $db->prepare("INSERT INTO newsletter_subscribers (email) VALUES (?)");
$stmt->execute([$email]);
echo json_encode(['success' => __('newsletter_success', 'Thank you for subscribing!')]);
} catch (\PDOException $e) {
if ($e->getCode() == 23000) { // Duplicate entry
echo json_encode(['success' => __('newsletter_already_subscribed', 'You are already subscribed!')]);
} else {
echo json_encode(['error' => __('newsletter_error', 'An error occurred. Please try again.')]);
}
}
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Services\ApkService;
class SitemapController extends Controller {
public function index() {
$apkService = new ApkService();
$apks = $apkService->getAllApks();
$db = db_pdo();
$categories = $db->query("SELECT * FROM categories")->fetchAll();
header("Content-Type: application/xml; charset=utf-8");
$baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]";
echo '<?xml version="1.0" encoding="UTF-8"?>';
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
// Home
echo '<url>';
echo '<loc>' . $baseUrl . '/</loc>';
echo '<priority>1.0</priority>';
echo '<changefreq>daily</changefreq>';
echo '</url>';
// APKs
foreach ($apks as $apk) {
echo '<url>';
echo '<loc>' . $baseUrl . '/apk/' . htmlspecialchars($apk['slug']) . '</loc>';
echo '<lastmod>' . date('Y-m-d', strtotime($apk['created_at'] ?? 'now')) . '</lastmod>';
echo '<priority>0.8</priority>';
echo '<changefreq>weekly</changefreq>';
echo '</url>';
}
// Blog Posts
$posts = $db->query("SELECT * FROM posts WHERE status = 'published'")->fetchAll();
foreach ($posts as $post) {
echo '<url>';
echo '<loc>' . $baseUrl . '/blog/' . htmlspecialchars($post['slug']) . '</loc>';
echo '<lastmod>' . date('Y-m-d', strtotime($post['created_at'] ?? 'now')) . '</lastmod>';
echo '<priority>0.7</priority>';
echo '<changefreq>monthly</changefreq>';
echo '</url>';
}
// Categories (if you have category pages, assuming /category/slug)
foreach ($categories as $category) {
echo '<url>';
echo '<loc>' . $baseUrl . '/?category=' . htmlspecialchars($category['slug']) . '</loc>';
echo '<priority>0.6</priority>';
echo '<changefreq>weekly</changefreq>';
echo '</url>';
}
echo '</urlset>';
}
}

25
app/Core/Controller.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App\Core;
class Controller {
protected function view($name, $data = []) {
extract($data);
$viewFile = __DIR__ . "/../../views/{$name}.php";
if (file_exists($viewFile)) {
require $viewFile;
} else {
echo "View {$name} not found";
}
}
protected function json($data) {
header('Content-Type: application/json');
echo json_encode($data);
}
protected function redirect($url) {
header("Location: {$url}");
exit;
}
}

55
app/Core/Router.php Normal file
View File

@ -0,0 +1,55 @@
<?php
namespace App\Core;
class Router {
private $routes = [];
public function get($path, $handler) {
$this->add('GET', $path, $handler);
}
public function post($path, $handler) {
$this->add('POST', $path, $handler);
}
private function add($method, $path, $handler) {
$path = preg_replace('/:([^\/]+)/', '(?P<$1>[^/]+)', $path);
$path = '#^' . $path . '$#';
$this->routes[] = [
'method' => $method,
'path' => $path,
'handler' => $handler
];
}
public function dispatch() {
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
foreach ($this->routes as $route) {
if ($route['method'] === $method && preg_match($route['path'], $uri, $matches)) {
$handler = $route['handler'];
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
if (is_string($handler) && strpos($handler, '@') !== false) {
[$controllerName, $methodName] = explode('@', $handler);
$controllerClass = "App\\Controllers\\" . $controllerName;
$controller = new $controllerClass();
return $controller->$methodName($params);
}
if (is_callable($handler)) {
return call_user_func_array($handler, [$params]);
}
}
}
// Handle 404
if (ob_get_level() > 0) {
ob_clean();
}
header("HTTP/1.0 404 Not Found");
require __DIR__ . "/../../views/404.php";
}
}

81
app/Helpers/functions.php Normal file
View File

@ -0,0 +1,81 @@
<?php
use App\Services\LanguageService;
function db_pdo() {
return db();
}
function view($view, $data = []) {
extract($data);
$viewPath = __DIR__ . '/../../views/' . $view . '.php';
if (file_exists($viewPath)) {
require $viewPath;
} else {
echo "View $view not found";
}
}
function redirect($path) {
header("Location: $path");
exit;
}
function __($key, $default = null) {
return LanguageService::translate($key, $default);
}
function get_setting($key, $default = '') {
$db = db();
$stmt = $db->prepare("SELECT setting_value FROM settings WHERE setting_key = ?");
$stmt->execute([$key]);
$result = $stmt->fetch();
return $result ? $result['setting_value'] : $default;
}
function compress_image($source, $destination, $quality) {
$info = getimagesize($source);
if ($info['mime'] == 'image/jpeg') {
$image = imagecreatefromjpeg($source);
} elseif ($info['mime'] == 'image/gif') {
$image = imagecreatefromgif($source);
} elseif ($info['mime'] == 'image/png') {
$image = imagecreatefrompng($source);
} else {
return false;
}
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;
}
function is_ajax() {
return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Services;
require_once __DIR__ . '/../../db/config.php';
class ApkService {
protected $db;
public function __construct() {
$this->db = db();
}
public function getLatest($limit = 10) {
$stmt = $this->db->prepare("SELECT * FROM apks WHERE status = 'published' ORDER BY display_order ASC, created_at DESC LIMIT :limit");
$stmt->bindValue(':limit', (int)$limit, \PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
public function getBySlug($slug) {
$stmt = $this->db->prepare("SELECT * FROM apks WHERE slug = :slug AND status = 'published' LIMIT 1");
$stmt->execute(['slug' => $slug]);
return $stmt->fetch();
}
public function getAllApks($category_slug = null, $search = null) {
$query = "SELECT a.* FROM apks a";
$params = [];
$where = [];
if ($category_slug) {
$query .= " JOIN categories c ON a.category_id = c.id";
$where[] = "c.slug = :category_slug";
$params['category_slug'] = $category_slug;
}
if ($search) {
$where[] = "a.title LIKE :search";
$params['search'] = "%$search%";
}
if (!empty($where)) {
$query .= " WHERE " . implode(" AND ", $where);
}
$query .= " ORDER BY a.display_order ASC, a.created_at DESC";
$stmt = $this->db->prepare($query);
$stmt->execute($params);
return $stmt->fetchAll();
}
public function getApkById($id) {
$stmt = $this->db->prepare("SELECT * FROM apks WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch();
}
public function incrementDownload($apkId) {
$stmt = $this->db->prepare("UPDATE apks SET total_downloads = total_downloads + 1 WHERE id = :id");
$stmt->execute(['id' => $apkId]);
$stmt = $this->db->prepare("INSERT INTO downloads (apk_id, ip_address) VALUES (:apk_id, :ip)");
$stmt->execute([
'apk_id' => $apkId,
'ip' => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'
]);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Services;
class LanguageService {
private static $translations = [];
private static $lang = 'id';
public static function init() {
if (isset($_SESSION['lang']) && is_string($_SESSION['lang'])) {
self::$lang = $_SESSION['lang'];
} elseif (isset($_COOKIE['lang']) && is_string($_COOKIE['lang'])) {
self::$lang = $_COOKIE['lang'];
}
if (!is_string(self::$lang)) {
self::$lang = 'id';
}
$langFile = __DIR__ . '/../../lang/' . self::$lang . '.php';
if (file_exists($langFile)) {
self::$translations = require $langFile;
} else {
// Default to English if the translation file doesn't exist
self::$lang = 'en';
$langFile = __DIR__ . '/../../lang/' . self::$lang . '.php';
if (file_exists($langFile)) {
self::$translations = require $langFile;
}
}
}
public static function translate($key, $default = null) {
if (empty(self::$translations)) {
self::init();
}
return self::$translations[$key] ?? ($default ?? $key);
}
public static function setLang($lang) {
if (!is_string($lang)) {
return;
}
self::$lang = $lang;
$_SESSION['lang'] = $lang;
setcookie('lang', $lang, time() + (86400 * 30), "/");
}
public static function getLang() {
return self::$lang;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Services;
class ThemeService {
public static function getCurrent() {
return $_COOKIE['theme'] ?? 'light';
}
}

View File

@ -1,302 +1,595 @@
:root {
--bg-color: #ffffff;
--text-color: #1E293B;
--card-bg: #FFFFFF;
--navbar-bg: rgba(255, 255, 255, 0.8);
--border-color: rgba(0, 0, 0, 0.05);
--subtle-bg: #f8fafc;
--muted-text: #64748b;
--footer-bg: #ffffff;
--accent-color: #10B981;
}
[data-theme="dark"] {
--bg-color: #0f172a;
--text-color: #f1f5f9;
--card-bg: #1e293b;
--navbar-bg: rgba(15, 23, 42, 0.8);
--border-color: rgba(255, 255, 255, 0.1);
--subtle-bg: #1e293b;
--muted-text: #94a3b8;
--footer-bg: #0f172a;
--accent-color: #34D399;
}
body { body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); font-family: 'Inter', sans-serif;
background: var(--bg-color);
background-size: 400% 400%; background-size: 400% 400%;
animation: gradient 15s ease infinite; background-attachment: fixed;
color: #212529; color: var(--text-color);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
margin: 0;
min-height: 100vh;
}
.main-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
padding: 20px;
box-sizing: border-box;
position: relative; position: relative;
z-index: 1; min-height: 100vh;
transition: background-color 0.3s ease, color 0.3s ease;
} }
@keyframes gradient { h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
0% { color: var(--text-color) !important;
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
} }
.chat-container { /* Ensure headings inside light-text containers are visible */
width: 100%; .text-white h1, .text-white h2, .text-white h3, .text-white h4, .text-white h5, .text-white h6,
max-width: 600px; .bg-dark h1, .bg-dark h2, .bg-dark h3, .bg-dark h4, .bg-dark h5, .bg-dark h6 {
background: rgba(255, 255, 255, 0.85); color: #ffffff !important;
border: 1px solid rgba(255, 255, 255, 0.3); }
border-radius: 20px;
display: flex; body.animated-bg {
flex-direction: column; background: linear-gradient(-45deg, var(--bg-color), var(--subtle-bg), var(--bg-color), var(--subtle-bg));
height: 85vh; animation: gradientBG 15s ease infinite;
box-shadow: 0 20px 40px rgba(0,0,0,0.2); }
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px); @keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Background blobs */
.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;
}
.hover-lift:hover {
transform: translateY(-5px);
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.1) !important;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
.chat-header { .floating-animation {
padding: 1.5rem; animation: floating 3s ease-in-out infinite;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
} }
.chat-messages { @keyframes floating {
flex: 1; 0% { transform: translate(0, 0px); }
overflow-y: auto; 50% { transform: translate(0, 15px); }
padding: 1.5rem; 100% { transform: translate(0, -0px); }
}
.btn-success {
background-color: #10B981;
border-color: #10B981;
}
.btn-success:hover {
background-color: #059669;
border-color: #059669;
}
.text-success {
color: #10B981 !important;
}
.bg-success {
background-color: #10B981 !important;
}
.bg-primary {
background-color: #3B82F6 !important;
}
.bg-warning {
background-color: #F59E0B !important;
}
.bg-success-subtle {
background-color: #ECFDF5 !important;
}
[data-theme="dark"] .bg-success-subtle {
background-color: rgba(16, 185, 129, 0.1) !important;
}
.rounded-4 { border-radius: 1rem !important; }
.rounded-5 { border-radius: 1.5rem !important; }
/* Navbar styling */
.navbar {
background: var(--navbar-bg) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-color) !important;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.navbar-brand {
font-size: 1.25rem;
}
/* Card styling */
.card {
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--text-color);
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}
.card-title {
color: var(--text-color);
}
.text-muted {
color: var(--muted-text) !important;
}
/* Theme toggle button styling */
.theme-toggle-btn {
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border-color);
background: var(--subtle-bg);
color: var(--text-color);
position: relative;
overflow: hidden;
}
.theme-toggle-btn:hover {
background-color: var(--border-color);
transform: rotate(15deg) scale(1.05);
}
.theme-toggle-btn i {
font-size: 1.1rem;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
[data-theme="dark"] .theme-toggle-btn {
box-shadow: 0 0 15px rgba(245, 158, 11, 0.2);
border-color: rgba(245, 158, 11, 0.3);
}
[data-theme="dark"] .theme-toggle-btn i {
color: #F59E0B;
}
/* Language selector polish */
.lang-selector-btn {
background: var(--subtle-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 0.5rem 1rem;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-color);
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
}
.lang-selector-btn:hover {
background-color: var(--border-color);
border-color: var(--muted-text);
}
.lang-selector-btn i {
color: var(--accent-color);
}
/* Override Bootstrap utilities for dark mode */
[data-theme="dark"] .bg-white,
[data-theme="dark"] .btn-white {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
}
[data-theme="dark"] .bg-light {
background-color: var(--subtle-bg) !important;
}
[data-theme="dark"] .border-top,
[data-theme="dark"] .border-bottom,
[data-theme="dark"] .border {
border-color: var(--border-color) !important;
}
[data-theme="dark"] footer.bg-white {
background-color: var(--footer-bg) !important;
}
[data-theme="dark"] .list-unstyled a.text-muted:hover {
color: var(--text-color) !important;
}
[data-theme="dark"] .form-control {
background-color: var(--subtle-bg);
border-color: var(--border-color);
color: var(--text-color);
}
[data-theme="dark"] .form-control::placeholder {
color: var(--muted-text);
}
[data-theme="dark"] .dropdown-menu {
background-color: var(--card-bg);
border-color: var(--border-color);
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.4) !important;
padding: 0.5rem;
border-radius: 1rem;
}
[data-theme="dark"] .dropdown-item {
color: var(--text-color);
border-radius: 0.5rem;
}
[data-theme="dark"] .dropdown-item:hover {
background-color: var(--subtle-bg);
color: var(--text-color);
}
[data-theme="dark"] .nav-link {
color: var(--text-color);
}
[data-theme="dark"] .nav-link:hover {
color: #10B981;
}
[data-theme="dark"] .btn-outline-dark {
border-color: var(--text-color);
color: var(--text-color);
}
[data-theme="dark"] .btn-outline-dark:hover {
background-color: var(--text-color);
color: var(--bg-color);
}
[data-theme="dark"] .badge.bg-light {
background-color: rgba(255,255,255,0.1) !important;
color: var(--text-color) !important;
}
[data-theme="dark"] .breadcrumb-item.active {
color: var(--muted-text) !important;
}
[data-theme="dark"] .navbar-toggler-icon {
filter: invert(1) grayscale(1) brightness(2);
}
/* Mobile adjustments */
@media (max-width: 991.98px) {
.navbar-collapse {
background-color: var(--card-bg);
margin: 0 -1rem;
padding: 1rem;
border-radius: 0 0 1.5rem 1.5rem;
border: 1px solid var(--border-color);
border-top: none;
box-shadow: 0 1.5rem 3rem rgba(0,0,0,0.1);
}
[data-theme="dark"] .navbar-collapse {
box-shadow: 0 1.5rem 3rem rgba(0,0,0,0.4);
}
.navbar-nav .nav-item {
width: 100%;
padding: 0.15rem 0;
}
.navbar-nav .nav-link {
padding: 0.85rem 1.25rem !important;
border-radius: 0.75rem;
font-weight: 500;
}
.navbar-nav .nav-link:hover {
background-color: var(--subtle-bg);
}
.navbar-nav .dropdown-menu {
background-color: var(--subtle-bg);
border: none;
box-shadow: none !important;
margin: 0.5rem 1rem;
padding: 0.5rem;
}
.mobile-controls-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
background: var(--subtle-bg);
border-radius: 1rem;
margin-top: 0.75rem;
border: 1px solid var(--border-color);
}
.display-4 {
font-size: 2rem !important;
line-height: 1.2;
}
.lead {
font-size: 1rem;
}
.btn-lg {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
}
/* 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; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.25rem; align-items: center;
} justify-content: center;
text-decoration: none;
/* Custom Scrollbar */ color: var(--muted-text);
::-webkit-scrollbar { font-size: 0.7rem;
width: 6px; font-weight: 500;
} transition: all 0.2s ease;
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.message {
max-width: 85%;
padding: 0.85rem 1.1rem;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
color: #fff;
border-bottom-right-radius: 4px;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
}
.chat-input-area input {
flex: 1; flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 0.75rem 1rem;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
} }
.chat-input-area input:focus { .mobile-nav-item i {
border-color: #23a6d5; font-size: 1.25rem;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); margin-bottom: 4px;
} }
.chat-input-area button { .mobile-nav-item.active {
background: #212529; color: var(--accent-color);
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
} }
.chat-input-area button:hover { .mobile-nav-item.active i {
background: #000;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
} }
/* Background Animations */ /* Sticky Download Button for Mobile */
.bg-animations { .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; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 0; background: var(--bg-color);
overflow: hidden; z-index: 1100;
pointer-events: none; display: none;
padding: 2rem 1.5rem;
} }
.blob { .search-overlay.active {
position: absolute;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
filter: blur(80px);
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@keyframes move {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
}
.admin-link {
font-size: 14px;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.admin-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
}
/* Admin Styles */
.admin-container {
max-width: 900px;
margin: 3rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 1;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block; display: block;
margin-bottom: 0.5rem; animation: fadeIn 0.3s ease;
font-weight: 600;
font-size: 0.9rem;
} }
.form-control { @keyframes fadeIn {
width: 100%; from { opacity: 0; }
padding: 0.75rem 1rem; to { opacity: 1; }
border: 1px solid rgba(0, 0, 0, 0.1); }
border-radius: 12px;
background: #fff; .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; transition: all 0.3s ease;
box-sizing: border-box;
} }
.form-control:focus { .share-fab:hover {
outline: none; color: white;
border-color: #23a6d5; transform: scale(1.1);
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.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;
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1,39 +1,326 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form'); // Basic interaction for toasts
const chatInput = document.getElementById('chat-input'); try {
const chatMessages = document.getElementById('chat-messages'); const toasts = document.querySelectorAll('.toast');
toasts.forEach(toastEl => {
if (window.bootstrap && bootstrap.Toast) {
const toast = new bootstrap.Toast(toastEl, { delay: 5000 });
toast.show();
}
});
} catch (e) {
console.error('Toast error:', e);
}
const appendMessage = (text, sender) => { const html = document.documentElement;
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender); const updateIcons = (theme) => {
msgDiv.textContent = text; const icons = document.querySelectorAll('#theme-toggle i, #theme-toggle-mobile i');
chatMessages.appendChild(msgDiv); icons.forEach(icon => {
chatMessages.scrollTop = chatMessages.scrollHeight; if (theme === 'dark') {
icon.className = 'fa-solid fa-sun';
} else {
icon.className = 'fa-solid fa-moon';
}
});
const textLabels = document.querySelectorAll('.theme-status-text');
textLabels.forEach(label => {
label.textContent = theme === 'dark' ? 'Dark Mode' : 'Light Mode';
});
}; };
chatForm.addEventListener('submit', async (e) => { // Theme Toggle Logic
e.preventDefault(); const initThemeToggle = (btnId) => {
const message = chatInput.value.trim(); const themeToggle = document.getElementById(btnId);
if (!message) return; if (!themeToggle) return;
appendMessage(message, 'visitor'); themeToggle.addEventListener('click', () => {
chatInput.value = ''; const currentTheme = html.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
updateIcons(newTheme);
document.cookie = `theme=${newTheme}; path=/; max-age=${365 * 24 * 60 * 60}`;
localStorage.setItem('theme', newTheme);
});
};
try { // Unified AJAX Content Loader
const response = await fetch('api/chat.php', { 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 = () => {
document.addEventListener('click', (e) => {
const filter = e.target.closest('.category-filter, .ajax-cat-link');
if (!filter) return;
e.preventDefault();
const url = filter.getAttribute('href');
const categoryName = filter.textContent.trim();
const dropdownBtn = document.getElementById('category-dropdown-btn');
if (dropdownBtn && filter.classList.contains('category-filter')) {
dropdownBtn.innerHTML = `${categoryName} <i class="bi bi-chevron-down ms-1 small"></i>`;
}
// 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');
}
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 = () => {
document.addEventListener('click', (e) => {
const btn = e.target.closest('.newsletter-submit-btn');
if (!btn) return;
const form = btn.closest('.newsletter-form') || btn.parentElement;
const emailInput = form.querySelector('.newsletter-email');
const msg = form.querySelector('.newsletter-msg') || form.nextElementSibling;
if (!emailInput) return;
const email = emailInput.value;
if (!email || !email.includes('@')) return;
btn.disabled = true;
const originalText = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
fetch('/api/newsletter/subscribe', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }) body: JSON.stringify({ email: email })
})
.then(res => res.json())
.then(data => {
btn.disabled = false;
btn.innerHTML = originalText;
if (data.success) {
if (msg) msg.innerHTML = `<span class="text-success">${data.success}</span>`;
else alert(data.success);
emailInput.value = '';
} else {
if (msg) msg.innerHTML = `<span class="text-danger">${data.error}</span>`;
else alert(data.error);
}
})
.catch(err => {
btn.disabled = false;
btn.innerHTML = originalText;
if (msg) msg.innerHTML = '<span class="text-danger">An error occurred.</span>';
}); });
const data = await response.json(); });
// Also handle form submit for the one in home.php
document.addEventListener('submit', (e) => {
if (e.target.classList.contains('newsletter-form')) {
e.preventDefault();
const btn = e.target.querySelector('.newsletter-submit-btn');
if (btn) btn.click();
}
});
};
// AI Chat Assistant Logic
const initAIChat = () => {
const toggleBtn = document.getElementById('toggle-ai-chat');
const closeBtn = document.getElementById('close-ai-chat');
const chatWindow = document.getElementById('ai-chat-window');
const chatInput = document.getElementById('ai-chat-input');
const sendBtn = document.getElementById('send-ai-chat');
const messagesContainer = document.getElementById('ai-chat-messages');
if (!toggleBtn || !chatWindow) return;
toggleBtn.addEventListener('click', () => {
chatWindow.classList.toggle('d-none');
if (!chatWindow.classList.contains('d-none')) {
chatInput.focus();
}
});
closeBtn.addEventListener('click', () => {
chatWindow.classList.add('d-none');
});
const appendMessage = (message, isUser = false) => {
const div = document.createElement('div');
div.className = 'mb-3 d-flex ' + (isUser ? 'justify-content-end' : '');
// Artificial delay for realism const content = document.createElement('div');
setTimeout(() => { content.className = (isUser ? 'bg-success text-white' : 'bg-white') + ' p-3 rounded-4 shadow-sm small';
appendMessage(data.reply, 'bot'); content.style.maxWidth = '85%';
}, 500); content.style.borderBottomRightRadius = isUser ? '0' : 'inherit';
} catch (error) { content.style.borderBottomLeftRadius = isUser ? 'inherit' : '0';
console.error('Error:', error); content.textContent = message;
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
div.appendChild(content);
messagesContainer.appendChild(div);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
};
const sendMessage = () => {
const message = chatInput.value.trim();
if (!message) return;
appendMessage(message, true);
chatInput.value = '';
const loadingDiv = document.createElement('div');
loadingDiv.className = 'mb-3 d-flex';
loadingDiv.innerHTML = '<div class="bg-white p-3 rounded-4 shadow-sm small" style="border-bottom-left-radius: 0 !important;"><div class="spinner-border spinner-border-sm text-success" role="status"></div> Thinking...</div>';
messagesContainer.appendChild(loadingDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
fetch('/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: message })
})
.then(response => response.json())
.then(data => {
messagesContainer.removeChild(loadingDiv);
if (data.reply) {
appendMessage(data.reply);
} else {
appendMessage(data.error || 'Sorry, something went wrong.');
}
})
.catch(err => {
if (messagesContainer.contains(loadingDiv)) messagesContainer.removeChild(loadingDiv);
appendMessage('Error connecting to AI assistant.');
console.error(err);
});
};
sendBtn.addEventListener('click', sendMessage);
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
};
// 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);
initThemeToggle('theme-toggle');
initThemeToggle('theme-toggle-mobile');
initCategoryAjax();
initSearchAjax();
initNewsletterAjax();
initAIChat();
initMobileUtils();
// Handle browser back/forward
window.addEventListener('popstate', () => {
loadContent(window.location.href, false);
}); });
console.log('ApkNusa AJAX Engine active.');
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,5 @@
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
('meta_description', 'Download the latest APKs for free.'),
('meta_keywords', 'apk, download, android, games, apps'),
('head_js', ''),
('body_js', '');

View File

@ -0,0 +1,40 @@
-- Create categories table
CREATE TABLE IF NOT EXISTS categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE
);
-- Add categories if not exist
INSERT IGNORE INTO categories (name, slug) VALUES ('Games', 'games'), ('Apps', 'apps'), ('Tools', 'tools');
-- Update apks table
ALTER TABLE apks ADD COLUMN icon_path VARCHAR(255) DEFAULT NULL;
ALTER TABLE apks ADD COLUMN display_order INT DEFAULT 0;
-- Update users table
ALTER TABLE users ADD COLUMN balance DECIMAL(15, 2) DEFAULT 0.00;
-- Create withdrawals table
CREATE TABLE IF NOT EXISTS withdrawals (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
amount DECIMAL(15, 2) NOT NULL,
method VARCHAR(50) NOT NULL,
account_details TEXT NOT NULL,
status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Create referral_downloads table to track earnings per download
CREATE TABLE IF NOT EXISTS referral_downloads (
id INT AUTO_INCREMENT PRIMARY KEY,
referrer_id INT NOT NULL,
apk_id INT NOT NULL,
ip_address VARCHAR(45) NOT NULL,
amount DECIMAL(15, 2) DEFAULT 500.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (referrer_id) REFERENCES users(id),
FOREIGN KEY (apk_id) REFERENCES apks(id)
);

View File

@ -0,0 +1,18 @@
-- Create posts table for Blog
CREATE TABLE IF NOT EXISTS `posts` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`title` VARCHAR(255) NOT NULL,
`slug` VARCHAR(255) NOT NULL UNIQUE,
`content` TEXT NOT NULL,
`image_path` VARCHAR(255) DEFAULT NULL,
`status` ENUM('published', 'draft') DEFAULT 'published',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Add Social Media Settings
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES
('facebook_url', ''),
('twitter_url', ''),
('instagram_url', ''),
('github_url', '');

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

@ -0,0 +1,4 @@
-- Add Telegram and WhatsApp Social Media Settings
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES
('telegram_url', ''),
('whatsapp_url', '');

5
debug_blog.log Normal file
View File

@ -0,0 +1,5 @@
Count: 3
Count: 3
Count: 3
Count: 3
Count: 3

46
fill_blog.php Normal file
View File

@ -0,0 +1,46 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/ai/LocalAIApi.php';
$prompt = "Generate 3 professional blog posts for an APK/Android app store website.
Return ONLY a JSON array of objects. Each object must have:
- 'title': A catchy title.
- 'slug': A URL-friendly slug.
- 'content': Detailed HTML content (using <p>, <h2>, <ul>, etc.) at least 300 words.
Topics:
1. Top 5 Productivity Apps for 2026.
2. How to Safely Install APKs on Your Android Device.
3. The Future of Mobile Gaming: Trends in 2026.
Language: Indonesian.";
$response = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => 'You are a professional tech blogger and SEO expert.'],
['role' => 'user', 'content' => $prompt],
],
]);
if (!empty($response['success'])) {
$posts = LocalAIApi::decodeJsonFromResponse($response);
if (is_array($posts)) {
$db = db();
foreach ($posts as $post) {
try {
$stmt = $db->prepare("INSERT INTO posts (title, slug, content, status) VALUES (?, ?, ?, 'published')");
$stmt->execute([$post['title'], $post['slug'], $post['content']]);
echo "Inserted: " . $post['title'] . "\n";
} catch (Exception $e) {
echo "Error inserting " . $post['title'] . ": " . $e->getMessage() . "\n";
}
}
} else {
echo "Failed to decode JSON from AI response.\n";
// Attempt to extract text if JSON decoding fails directly
$text = LocalAIApi::extractText($response);
echo "Raw AI Text: " . substr($text, 0, 500) . "...\n";
}
} else {
echo "AI Request failed: " . ($response['error'] ?? 'Unknown error') . "\n";
}

108
full_schema.sql Normal file
View File

@ -0,0 +1,108 @@
-- Full Schema for APK Portal
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";
SET FOREIGN_KEY_CHECKS=0;
-- --------------------------------------------------------
-- Table structure for table `settings`
CREATE TABLE IF NOT EXISTS `settings` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`setting_key` VARCHAR(255) UNIQUE,
`setting_value` TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Table structure for table `users`
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`referral_code` varchar(20) DEFAULT NULL,
`referred_by` int(11) DEFAULT NULL,
`role` varchar(20) DEFAULT 'user',
`points` int(11) DEFAULT 0,
`total_referrals` int(11) DEFAULT 0,
`balance` decimal(15,2) DEFAULT 0.00,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `referral_code` (`referral_code`),
KEY `referred_by` (`referred_by`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Table structure for table `categories`
CREATE TABLE IF NOT EXISTS `categories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`slug` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Table structure for table `apks`
CREATE TABLE IF NOT EXISTS `apks` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`slug` varchar(255) NOT NULL,
`version` varchar(50) DEFAULT NULL,
`description` text DEFAULT NULL,
`image_url` varchar(255) DEFAULT NULL,
`icon_path` varchar(255) DEFAULT NULL,
`download_url` varchar(255) DEFAULT NULL,
`category_id` int(11) DEFAULT NULL,
`total_downloads` int(11) DEFAULT 0,
`is_vip` tinyint(1) DEFAULT 0,
`status` enum('published','draft') DEFAULT 'published',
`display_order` int(11) DEFAULT 0,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`),
KEY `category_id` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Table structure for table `withdrawals`
CREATE TABLE IF NOT EXISTS `withdrawals` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`amount` decimal(15,2) NOT NULL,
`method` varchar(50) NOT NULL,
`account_details` text NOT NULL,
`status` enum('pending','approved','rejected') DEFAULT 'pending',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `withdrawals_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Table structure for table `referral_downloads`
CREATE TABLE IF NOT EXISTS `referral_downloads` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`referrer_id` int(11) NOT NULL,
`apk_id` int(11) NOT NULL,
`ip_address` varchar(45) NOT NULL,
`amount` decimal(15,2) DEFAULT 500.00,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `referrer_id` (`referrer_id`),
KEY `apk_id` (`apk_id`),
CONSTRAINT `referral_downloads_ibfk_1` FOREIGN KEY (`referrer_id`) REFERENCES `users` (`id`),
CONSTRAINT `referral_downloads_ibfk_2` FOREIGN KEY (`apk_id`) REFERENCES `apks` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default Data
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES
('site_name', 'ApkNusa'),
('site_icon', ''),
('site_favicon', ''),
('meta_description', 'Download the latest APKs for free.'),
('meta_keywords', 'apk, download, android, games, apps'),
('head_js', ''),
('body_js', '');
INSERT IGNORE INTO `users` (`username`, `password`, `role`) VALUES ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin'); -- password: admin123
INSERT IGNORE INTO `categories` (`name`, `slug`) VALUES ('Games', 'games'), ('Apps', 'apps'), ('Tools', 'tools');
SET FOREIGN_KEY_CHECKS=1;
COMMIT;

267
index.php
View File

@ -1,150 +1,127 @@
<?php <?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; // Autoloader
$now = date('Y-m-d H:i:s'); spl_autoload_register(function ($class) {
?> $prefix = 'App\\';
<!doctype html> $base_dir = __DIR__ . '/app/';
<html lang="en">
<head> $len = strlen($prefix);
<meta charset="utf-8" /> if (strncmp($prefix, $class, $len) !== 0) {
<meta name="viewport" content="width=device-width, initial-scale=1" /> return;
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<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;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
} }
body {
margin: 0; $relative_class = substr($class, $len);
font-family: 'Inter', sans-serif; $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color); if (file_exists($file)) {
display: flex; require $file;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
} }
body::before { });
content: '';
position: absolute; require_once 'app/Helpers/functions.php';
top: 0; require_once 'db/config.php';
left: 0;
width: 100%; session_start();
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>'); // Initialize Language Service
animation: bg-pan 20s linear infinite; \App\Services\LanguageService::init();
z-index: -1;
use App\Core\Router;
// Maintenance Mode Check
if (get_setting('maintenance_mode') === '1') {
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$isAdmin = strpos($uri, '/admin') === 0 || strpos($uri, '/login') === 0 || strpos($uri, '/logout') === 0;
if (!$isAdmin && !isset($_SESSION['admin_id'])) {
require_once 'views/maintenance.php';
exit;
} }
@keyframes bg-pan { }
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; } $router = new Router();
} $router->post('/api/newsletter/subscribe', 'NewsletterController@subscribe');
main { $router->post('/api/report', 'ContactController@ajaxReport');
padding: 2rem; $router->post('/api/ai/chat', 'AIController@chat');
}
.card { // Sitemap
background: var(--card-bg-color); $router->get('/sitemap.xml', 'SitemapController@index');
border: 1px solid var(--card-border-color);
border-radius: 16px; // Language Switch
padding: 2rem; $router->get('/lang/:code', function($params) {
backdrop-filter: blur(20px); $code = $params['code'];
-webkit-backdrop-filter: blur(20px); \App\Services\LanguageService::setLang($code);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1); header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/'));
} exit;
.loader { });
margin: 1.25rem auto 1.25rem;
width: 48px; // Home & APKs
height: 48px; $router->get('/', 'HomeController@index');
border: 3px solid rgba(255, 255, 255, 0.25); $router->get('/apk/:slug', 'HomeController@apkDetail');
border-top-color: #fff; $router->get('/download/:slug', 'HomeController@download');
border-radius: 50%;
animation: spin 1s linear infinite; // Blog
} $router->get('/blog', 'BlogController@index');
@keyframes spin { $router->get('/blog/:slug', 'BlogController@detail');
from { transform: rotate(0deg); }
to { transform: rotate(360deg); } // Static Pages
} $router->get('/contact', 'ContactController@index');
.hint { $router->post('/contact', 'ContactController@submit');
opacity: 0.9; $router->get('/help-center', 'HomeController@helpCenter');
} $router->get('/privacy-policy', 'HomeController@privacyPolicy');
.sr-only { $router->get('/terms-of-service', 'HomeController@termsOfService');
position: absolute;
width: 1px; height: 1px; // Auth
padding: 0; margin: -1px; $router->get('/login', 'AuthController@loginForm');
overflow: hidden; $router->post('/login', 'AuthController@login');
clip: rect(0, 0, 0, 0); $router->get('/register', 'AuthController@registerForm');
white-space: nowrap; border: 0; $router->post('/register', 'AuthController@register');
} $router->get('/logout', 'AuthController@logout');
h1 { $router->get('/profile', 'AuthController@profile');
font-size: 3rem; $router->post('/withdraw', 'AuthController@requestWithdrawal');
font-weight: 700;
margin: 0 0 1rem; // Admin Auth
letter-spacing: -1px; $router->get('/admin/login', 'AdminController@loginForm');
} $router->post('/admin/login', 'AdminController@login');
p { $router->get('/admin/logout', 'AdminController@logout');
margin: 0.5rem 0;
font-size: 1.1rem; // Admin Dashboard
} $router->get('/admin/dashboard', 'AdminController@dashboard');
code {
background: rgba(0,0,0,0.2); // Admin Settings
padding: 2px 6px; $router->get('/admin/settings', 'AdminController@settingsForm');
border-radius: 4px; $router->post('/admin/settings', 'AdminController@saveSettings');
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
} // Admin Users
footer { $router->get('/admin/users', 'AdminController@users');
position: absolute; $router->post('/admin/users/toggle-ban/:id', 'AdminController@toggleBan');
bottom: 1rem;
font-size: 0.8rem; // Admin APKs
opacity: 0.7; $router->get('/admin/apks', 'AdminController@apks');
} $router->get('/admin/apks/mass-upload', 'AdminController@massUploadForm');
</style> $router->post('/admin/apks/mass-upload', 'AdminController@massUpload');
</head> $router->get('/admin/apks/add', 'AdminController@addApkForm');
<body> $router->post('/admin/apks/add', 'AdminController@addApk');
<main> $router->get('/admin/apks/edit/:id', 'AdminController@editApkForm');
<div class="card"> $router->post('/admin/apks/edit/:id', 'AdminController@editApk');
<h1>Analyzing your requirements and generating your website…</h1> $router->get('/admin/apks/delete/:id', 'AdminController@deleteApk');
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> $router->post('/admin/apks/reorder', 'AdminController@updateOrder');
<span class="sr-only">Loading…</span>
</div> // Admin Posts (Blog)
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p> $router->get('/admin/posts', 'AdminController@posts');
<p class="hint">This page will update automatically as the plan is implemented.</p> $router->get('/admin/posts/add', 'AdminController@addPostForm');
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p> $router->post('/admin/posts/add', 'AdminController@addPost');
</div> $router->get('/admin/posts/edit/:id', 'AdminController@editPostForm');
</main> $router->post('/admin/posts/edit/:id', 'AdminController@editPost');
<footer> $router->get('/admin/posts/delete/:id', 'AdminController@deletePost');
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer> // Admin Categories
</body> $router->get('/admin/categories', 'AdminController@categories');
</html> $router->post('/admin/categories/add', 'AdminController@addCategory');
$router->get('/admin/categories/delete/:id', 'AdminController@deleteCategory');
// Admin Withdrawals
$router->get('/admin/withdrawals', 'AdminController@withdrawals');
$router->get('/admin/withdrawals/approve/:id', 'AdminController@approveWithdrawal');
$router->get('/admin/withdrawals/reject/:id', 'AdminController@rejectWithdrawal');
$router->dispatch();

174
install.php Normal file
View File

@ -0,0 +1,174 @@
<?php
session_start();
if (file_exists('db/config.php') && !isset($_GET['force'])) {
// Check if DB is already connected
require_once 'db/config.php';
try {
db();
die("Application already installed. Delete db/config.php if you want to reinstall.");
} catch (Exception $e) {
// Continue to installation
}
}
$step = $_GET['step'] ?? 1;
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($step == 2) {
$host = $_POST['db_host'];
$name = $_POST['db_name'];
$user = $_POST['db_user'];
$pass = $_POST['db_pass'];
try {
$pdo = new PDO("mysql:host=$host;charset=utf8mb4", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$pdo->exec("CREATE DATABASE IF NOT EXISTS `$name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$pdo->exec("USE `$name`");
// Save config
$configContent = "<?php\ndefine('DB_HOST', '$host');\ndefine('DB_NAME', '$name');\ndefine('DB_USER', '$user');\ndefine('DB_PASS', '$pass');\n\nfunction db() {\n static \$pdo;\n if (!\$pdo) {\n \$pdo = new PDO('mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);\n }\n return \$pdo;
}\n";
file_put_contents('db/config.php', $configContent);
$_SESSION['install_pdo'] = ['host' => $host, 'name' => $name, 'user' => $user, 'pass' => $pass];
header("Location: install.php?step=3");
exit;
} catch (PDOException $e) {
$error = "Database Error: " . $e->getMessage();
}
}
if ($step == 3) {
require_once 'db/config.php';
$db = db();
// Import Schema
$schemaFile = 'full_schema.sql';
if (file_exists($schemaFile)) {
$sql = file_get_contents($schemaFile);
$db->exec($sql);
} else {
// Basic tables if full_schema.sql is missing
$db->exec("CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE, password VARCHAR(255), balance DECIMAL(10,2) DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
$db->exec("CREATE TABLE IF NOT EXISTS settings (id INT AUTO_INCREMENT PRIMARY KEY, setting_key VARCHAR(255) UNIQUE, setting_value TEXT)");
$db->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('site_name', 'My APK Store'), ('site_icon', ''), ('site_favicon', '')");
}
// Create Admin
$admin_user = $_POST['admin_user'];
$admin_pass = password_hash($_POST['admin_pass'], PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute([$admin_user, $admin_pass]);
$success = "Installation complete!";
$step = 4;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Installer</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #f4f7f6; }
.install-box { max-width: 500px; margin: 100px auto; background: #fff; padding: 40px; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
.step-indicator { display: flex; justify-content: space-between; margin-bottom: 30px; }
.step { width: 30px; height: 30px; border-radius: 50%; background: #ddd; text-align: center; line-height: 30px; color: #fff; }
.step.active { background: #0d6efd; }
</style>
</head>
<body>
<div class="install-box">
<h3 class="text-center mb-4">Web Installer</h3>
<div class="step-indicator">
<div class="step <?php echo $step >= 1 ? 'active' : ''; ">1</div>
<div class="step <?php echo $step >= 2 ? 'active' : ''; ">2</div>
<div class="step <?php echo $step >= 3 ? 'active' : ''; ">3</div>
<div class="step <?php echo $step >= 4 ? 'active' : ''; ">4</div>
</div>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<?php if ($step == 1): ?>
<h5>Welcome</h5>
<p>This wizard will help you install the application on your server.</p>
<div class="d-grid">
<a href="?step=2" class="btn btn-primary">Start Installation</a>
</div>
<?php endif; ?>
<?php if ($step == 2): ?>
<h5>Database Configuration</h5>
<form method="POST">
<div class="mb-3">
<label>DB Host</label>
<input type="text" name="db_host" class="form-control" value="localhost" required>
</div>
<div class="mb-3">
<label>DB Name</label>
<input type="text" name="db_name" class="form-control" required>
</div>
<div class="mb-3">
<label>DB User</label>
<input type="text" name="db_user" class="form-control" required>
</div>
<div class="mb-3">
<label>DB Password</label>
<input type="password" name="db_pass" class="form-control">
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Connect & Continue</button>
</div>
</form>
<?php endif; ?>
<?php if ($step == 3): ?>
<h5>Admin Account</h5>
<form method="POST">
<div class="mb-3">
<label>Admin Username</label>
<input type="text" name="admin_user" class="form-control" required>
</div>
<div class="mb-3">
<label>Admin Password</label>
<input type="password" name="admin_pass" class="form-control" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Finish Installation</button>
</div>
</form>
<?php endif; ?>
<?php if ($step == 4): ?>
<div class="text-center">
<h1 class="text-success"><i class="fas fa-check-circle"></i></h1>
<h5>Installation Successful!</h5>
<p>You can now log in to your admin panel.</p>
<div class="alert alert-warning">
<strong>Important:</strong> Please delete <code>install.php</code> file from your server.
</div>
<div class="d-grid">
<a href="/admin/login" class="btn btn-primary">Go to Admin Panel</a>
</div>
</div>
<?php endif; ?>
</div>
</body>
</html>

152
lang/en.php Normal file
View File

@ -0,0 +1,152 @@
<?php
return array (
'home' => 'Home',
'search' => 'Search',
'search_placeholder' => 'Search for apps and games...',
'search_results_for' => 'Search results for',
'no_apks_found' => 'No APKs found',
'try_another_search' => 'Try another search term or browse categories.',
'view_all_apks' => 'View All APKs',
'login' => 'Login',
'register' => 'Register',
'logout' => 'Logout',
'profile' => 'Profile',
'admin' => 'Admin',
'download' => 'Download',
'share' => 'Share',
'categories' => 'Categories',
'all_categories' => 'All Categories',
'latest_apks' => 'Latest APKs',
'featured_apks' => 'Featured APKs',
'download_now' => 'Download Now',
'share_link' => 'Share Link',
'balance' => 'Balance',
'referral_link' => 'Referral Link',
'withdraw' => 'Withdraw',
'withdrawal_history' => 'Withdrawal History',
'settings' => 'Settings',
'site_name' => 'Site Name',
'site_icon' => 'Site Icon',
'site_favicon' => 'Site Favicon',
'save_settings' => 'Save Settings',
'select_language' => 'Select Language',
'language_indonesia' => 'Indonesian',
'language_english' => 'English',
'admin_dashboard' => 'Admin Dashboard',
'manage_apks' => 'Manage APKs',
'manage_categories' => 'Manage Categories',
'manage_withdrawals' => 'Manage Withdrawals',
'general_settings' => 'General Settings',
'footer_about' => 'is your premier source for professional APK downloads, offering the latest and safest Android applications and games.',
'popular' => 'Popular',
'top_games' => 'Top Games',
'top_apps' => 'Top Apps',
'new_releases' => 'New Releases',
'resources' => 'Resources',
'support_center' => 'Support Center',
'terms_of_service' => 'Terms of Service',
'privacy_policy' => 'Privacy Policy',
'subscribe' => 'Subscribe',
'subscribe_text' => 'Stay updated with the latest APK releases.',
'email_placeholder' => 'Your email address',
'all_rights_reserved' => 'All rights reserved.',
'hero_title' => 'Download the Best <span class="text-success">Android APKs</span> Professionally',
'hero_subtitle' => 'Fast, safe, and secure downloads for your favorite mobile apps and games. No registration required to browse.',
'explore_apps' => 'Explore Apps',
'join_referral' => 'Join Referral',
'details' => 'Details',
'referral_journey_title' => 'Start your referral journey today',
'referral_journey_text' => 'Earn <b>Rp 500</b> for every download via your link. Join our community and share your favorite APKs.',
'get_started' => 'Get Started',
'home_title_suffix' => ' - Professional APK Download Portal',
'official_version_text' => 'Official and original version. Verified safe for Android device.',
'downloads' => 'Downloads',
'verified_safe' => 'Verified Safe',
'agree_terms_text' => 'By clicking Download, you agree to our terms of service.',
'description' => 'Description',
'main_features' => 'Main Features',
'feature_original' => 'Original APK from developer',
'feature_no_extra' => 'No extra files needed',
'feature_fast' => 'Fast and direct download',
'feature_regular' => 'Regular updates included',
'system_requirements' => 'System Requirements',
'req_android' => 'Android 6.0+ (Marshmallow)',
'req_ram' => '2GB RAM minimum recommended',
'req_internet' => 'Stable internet connection',
'req_cpu' => 'ARMv8 or newer processor',
'safe_question' => 'Is this safe to download?',
'safe_answer' => 'Yes, every app on ApkNusa is scanned and verified to ensure it is original and safe from the official developers.',
'share_earn' => 'Share & Earn',
'share_earn_text' => 'Share this link and earn <b>Rp 500</b> for every download!',
'copy' => 'Copy',
'login_to_earn' => 'Login to earn money',
'referral_program' => 'Referral Program',
'referral_program_text' => 'Join our community, share APKs, and get paid directly to your e-wallet or bank account.',
'copy_success_js' => 'Share link copied! Send this to your friends to earn money.',
'apk_detail_title' => 'Download %s %s - %s',
'apk_detail_meta_desc' => 'Download %s %s APK for free. %s',
'apk_detail_meta_keywords' => '%s, %s apk, download %s',
'login_title' => 'Login to ApkNusa',
'username' => 'Username',
'password' => 'Password',
'dont_have_account' => 'Don\'t have an account?',
'register_here' => 'Register here',
'register_title' => 'Register for ApkNusa',
'confirm_password' => 'Confirm Password',
'referral_code_optional' => 'Referral Code (Optional)',
'create_account' => 'Create Account',
'already_have_account' => 'Already have an account?',
'login_here' => 'Login here',
'error_invalid_login' => 'Invalid username or password',
'error_password_mismatch' => 'Passwords do not match',
'error_username_exists' => 'Username already exists',
'member_since' => 'Member since',
'points' => 'Points',
'referrals' => 'Referrals',
'min_withdraw' => 'Min. withdraw',
'referral_share_text' => 'Share your referral link to earn <b>Rp 500</b> for every download.',
'copy_link' => 'Copy Link',
'example_ref_link' => 'Example APK referral link:',
'recent_activities' => 'Recent activities',
'date' => 'Date',
'amount' => 'Amount',
'method' => 'Method',
'status' => 'Status',
'no_history' => 'No withdrawal history yet.',
'request_withdrawal' => 'Request Withdrawal',
'amount_to_withdraw' => 'Amount to Withdraw (IDR)',
'payment_method' => 'Payment Method',
'select_method' => 'Select method...',
'account_details' => 'Account Details',
'account_details_placeholder' => 'Enter phone number or bank account number with name',
'cancel' => 'Cancel',
'submit_request' => 'Submit Request',
'ref_copy_success_js' => 'Referral link copied to clipboard!',
'error_min_withdraw' => 'Minimum withdrawal is Rp 10.000',
'error_insufficient_balance' => 'Insufficient balance',
'success_withdraw_submitted' => 'Withdrawal request submitted successfully',
'meta_description_default' => 'Download Professional APKs.',
'meta_keywords_default' => 'apk, android, download',
'help_center_title' => 'Help Center',
'faq_title' => 'Frequently Asked Questions',
'faq_q1' => 'How to download APK from our site?',
'faq_a1' => 'Simply browse for the app you want, click on it, and then click the green "Download Now" button. Your download will start immediately.',
'faq_q2' => 'Are the APKs safe?',
'faq_a2' => 'Yes, all our APKs are sourced from original developers and verified to be safe and clean from any malware.',
'faq_q3' => 'How does the referral program work?',
'faq_a3' => 'Register for an account, copy your referral link from an APK page or your profile, and share it. You will earn Rp 500 for every unique download made through your link.',
'contact_us' => 'Contact Us',
'contact_text' => 'Still have questions? Our support team is here to help you.',
'send_email' => 'Send Email',
'privacy_policy_title' => 'Privacy Policy',
'privacy_policy_content' => '<h3>1. Information We Collect</h3><p>We collect information that you provide directly to us, such as when you create an account, participate in our referral program, or communicate with us.</p><h3>2. How We Use Information</h3><p>We use the information we collect to provide, maintain, and improve our services, including processing your referral earnings and withdrawal requests.</p><h3>3. Data Security</h3><p>We take reasonable measures to help protect information about you from loss, theft, misuse and unauthorized access.</p>',
'terms_of_service_title' => 'Terms of Service',
'terms_of_service_content' => '<h3>1. Acceptance of Terms</h3><p>By accessing or using our website, you agree to be bound by these terms of service.</p><h3>2. User Conduct</h3><p>You agree not to use the website for any unlawful purpose or in any way that could damage, disable, or impair the website.</p><h3>3. Referral Program</h3><p>Abuse of the referral program, including but not limited to self-referrals or using bots, will result in account suspension and forfeiture of earnings.</p>',
'404_title' => 'Page Not Found',
'404_text' => 'The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.',
'back_to_home' => 'Back to Home',
'newsletter_invalid_email' => 'Please provide a valid email address.',
'newsletter_success' => 'Thank you for subscribing!',
'newsletter_already_subscribed' => 'You are already subscribed!',
'newsletter_error' => 'An error occurred. Please try again.',
);

152
lang/id.php Normal file
View File

@ -0,0 +1,152 @@
<?php
return array (
'home' => 'Beranda',
'search' => 'Cari',
'search_placeholder' => 'Cari aplikasi dan game...',
'search_results_for' => 'Hasil pencarian untuk',
'no_apks_found' => 'APK tidak ditemukan',
'try_another_search' => 'Coba kata kunci lain atau telusuri kategori.',
'view_all_apks' => 'Lihat Semua APK',
'login' => 'Masuk',
'register' => 'Daftar',
'logout' => 'Keluar',
'profile' => 'Profil',
'admin' => 'Admin',
'download' => 'Unduh',
'share' => 'Bagikan',
'categories' => 'Kategori',
'all_categories' => 'Semua Kategori',
'latest_apks' => 'APK Terbaru',
'featured_apks' => 'APK Unggulan',
'download_now' => 'Unduh Sekarang',
'share_link' => 'Bagikan Link',
'balance' => 'Saldo',
'referral_link' => 'Link Referral',
'withdraw' => 'Tarik Saldo',
'withdrawal_history' => 'Riwayat Penarikan',
'settings' => 'Pengaturan',
'site_name' => 'Nama Website',
'site_icon' => 'Ikon Website',
'site_favicon' => 'Favicon Website',
'save_settings' => 'Simpan Pengaturan',
'select_language' => 'Pilih Bahasa',
'language_indonesia' => 'Indonesia',
'language_english' => 'English',
'admin_dashboard' => 'Dashboard Admin',
'manage_apks' => 'Kelola APK',
'manage_categories' => 'Kelola Kategori',
'manage_withdrawals' => 'Kelola Penarikan',
'general_settings' => 'Pengaturan Umum',
'footer_about' => 'adalah sumber utama Anda untuk unduhan APK profesional, menawarkan aplikasi dan game Android terbaru dan teraman.',
'popular' => 'Populer',
'top_games' => 'Game Teratas',
'top_apps' => 'Aplikasi Teratas',
'new_releases' => 'Rilis Baru',
'resources' => 'Sumber Daya',
'support_center' => 'Pusat Bantuan',
'terms_of_service' => 'Ketentuan Layanan',
'privacy_policy' => 'Kebijakan Privasi',
'subscribe' => 'Berlangganan',
'subscribe_text' => 'Tetap update dengan rilis APK terbaru.',
'email_placeholder' => 'Alamat email Anda',
'all_rights_reserved' => 'Hak cipta dilindungi undang-undang.',
'hero_title' => 'Unduh <span class="text-success">APK Android</span> Terbaik Secara Profesional',
'hero_subtitle' => 'Unduhan cepat, aman, dan terpercaya untuk aplikasi dan game seluler favorit Anda. Tanpa perlu registrasi untuk menjelajah.',
'explore_apps' => 'Jelajahi Aplikasi',
'join_referral' => 'Gabung Referral',
'details' => 'Detail',
'referral_journey_title' => 'Mulai perjalanan referral Anda hari ini',
'referral_journey_text' => 'Dapatkan <b>Rp 500</b> untuk setiap unduhan melalui link Anda. Bergabunglah dengan komunitas kami dan bagikan APK favorit Anda.',
'get_started' => 'Mulai Sekarang',
'home_title_suffix' => ' - Portal Unduh APK Profesional',
'official_version_text' => 'Versi resmi dan asli. Diverifikasi aman untuk perangkat Android.',
'downloads' => 'Unduhan',
'verified_safe' => 'Terverifikasi Aman',
'agree_terms_text' => 'Dengan mengklik Unduh, Anda menyetujui ketentuan layanan kami.',
'description' => 'Deskripsi',
'main_features' => 'Fitur Utama',
'feature_original' => 'APK asli dari pengembang',
'feature_no_extra' => 'Tidak perlu file tambahan',
'feature_fast' => 'Unduhan cepat dan langsung',
'feature_regular' => 'Termasuk pembaruan rutin',
'system_requirements' => 'Persyaratan Sistem',
'req_android' => 'Android 6.0+ (Marshmallow)',
'req_ram' => 'Minimal RAM 2GB direkomendasikan',
'req_internet' => 'Koneksi internet stabil',
'req_cpu' => 'Prosesor ARMv8 atau yang lebih baru',
'safe_question' => 'Apakah ini aman untuk diunduh?',
'safe_answer' => 'Ya, setiap aplikasi di ApkNusa dipindai dan diverifikasi untuk memastikan keaslian dan keamanannya dari pengembang resmi.',
'share_earn' => 'Bagikan & Hasilkan',
'share_earn_text' => 'Bagikan link ini dan dapatkan <b>Rp 500</b> untuk setiap unduhan!',
'copy' => 'Salin',
'login_to_earn' => 'Masuk untuk menghasilkan uang',
'referral_program' => 'Program Referral',
'referral_program_text' => 'Bergabunglah dengan komunitas kami, bagikan APK, dan dapatkan bayaran langsung ke e-wallet atau rekening bank Anda.',
'copy_success_js' => 'Link bagikan berhasil disalin! Kirimkan ini ke teman Anda untuk menghasilkan uang.',
'apk_detail_title' => 'Unduh %s %s - %s',
'apk_detail_meta_desc' => 'Unduh APK %s %s secara gratis. %s',
'apk_detail_meta_keywords' => '%s, %s apk, unduh %s',
'login_title' => 'Masuk ke ApkNusa',
'username' => 'Username',
'password' => 'Kata Sandi',
'dont_have_account' => 'Belum punya akun?',
'register_here' => 'Daftar di sini',
'register_title' => 'Daftar di ApkNusa',
'confirm_password' => 'Konfirmasi Kata Sandi',
'referral_code_optional' => 'Kode Referral (Opsional)',
'create_account' => 'Buat Akun',
'already_have_account' => 'Sudah punya akun?',
'login_here' => 'Masuk di sini',
'error_invalid_login' => 'Username atau kata sandi salah',
'error_password_mismatch' => 'Kata sandi tidak cocok',
'error_username_exists' => 'Username sudah terdaftar',
'member_since' => 'Anggota sejak',
'points' => 'Poin',
'referrals' => 'Referral',
'min_withdraw' => 'Min. penarikan',
'referral_share_text' => 'Bagikan link referral Anda untuk mendapatkan <b>Rp 500</b> untuk setiap unduhan.',
'copy_link' => 'Salin Link',
'example_ref_link' => 'Contoh link referral APK:',
'recent_activities' => 'Aktivitas terbaru',
'date' => 'Tanggal',
'amount' => 'Jumlah',
'method' => 'Metode',
'status' => 'Status',
'no_history' => 'Belum ada riwayat penarikan.',
'request_withdrawal' => 'Ajukan Penarikan',
'amount_to_withdraw' => 'Jumlah yang Ditarik (IDR)',
'payment_method' => 'Metode Pembayaran',
'select_method' => 'Pilih metode...',
'account_details' => 'Detail Akun',
'account_details_placeholder' => 'Masukkan nomor telepon atau nomor rekening bank dengan nama',
'cancel' => 'Batal',
'submit_request' => 'Ajukan Permintaan',
'ref_copy_success_js' => 'Link referral berhasil disalin!',
'error_min_withdraw' => 'Penarikan minimum adalah Rp 10.000',
'error_insufficient_balance' => 'Saldo tidak mencukupi',
'success_withdraw_submitted' => 'Permintaan penarikan berhasil diajukan',
'meta_description_default' => 'Unduh APK Profesional.',
'meta_keywords_default' => 'apk, android, unduh',
'help_center_title' => 'Pusat Bantuan',
'faq_title' => 'Pertanyaan yang Sering Diajukan',
'faq_q1' => 'Bagaimana cara mengunduh APK dari situs kami?',
'faq_a1' => 'Cukup cari aplikasi yang Anda inginkan, klik aplikasi tersebut, lalu klik tombol hijau "Unduh Sekarang". Unduhan Anda akan segera dimulai.',
'faq_q2' => 'Apakah APK di sini aman?',
'faq_a2' => 'Ya, semua APK kami bersumber dari pengembang asli dan diverifikasi aman serta bersih dari malware.',
'faq_q3' => 'Bagaimana cara kerja program referral?',
'faq_a3' => 'Daftar akun, salin link referral Anda dari halaman APK atau profil Anda, dan bagikan. Anda akan mendapatkan Rp 500 untuk setiap unduhan unik yang dilakukan melalui link Anda.',
'contact_us' => 'Hubungi Kami',
'contact_text' => 'Masih punya pertanyaan? Tim dukungan kami siap membantu Anda.',
'send_email' => 'Kirim Email',
'privacy_policy_title' => 'Kebijakan Privasi',
'privacy_policy_content' => '<h3>1. Informasi yang Kami Kumpulkan</h3><p>Kami mengumpulkan informasi yang Anda berikan langsung kepada kami, seperti saat Anda membuat akun, berpartisipasi dalam program referral kami, atau berkomunikasi dengan kami.</p><h3>2. Bagaimana Kami Menggunakan Informasi</h3><p>Kami menggunakan informasi yang kami kumpulkan untuk menyediakan, memelihara, dan meningkatkan layanan kami, termasuk memproses pendapatan referral dan permintaan penarikan Anda.</p><h3>3. Data Security</h3><p>Kami mengambil langkah-langkah yang wajar untuk membantu melindungi informasi tentang Anda dari kehilangan, pencurian, penyalahgunaan, dan akses yang tidak sah.</p>',
'terms_of_service_title' => 'Ketentuan Layanan',
'terms_of_service_content' => '<h3>1. Penerimaan Ketentuan</h3><p>Dengan mengakses atau menggunakan situs web kami, Anda setuju untuk terikat oleh ketentuan layanan ini.</p><h3>2. Perilaku Pengguna</h3><p>Anda setuju untuk tidak menggunakan situs web untuk tujuan yang melanggar hukum atau dengan cara apa pun yang dapat merusak, melumpuhkan, atau mengganggu situs web.</p><h3>3. Program Referral</h3><p>Penyalahgunaan program referral, termasuk namun tidak terbatas pada referral diri sendiri atau menggunakan bot, akan mengakibatkan penangguhan akun dan penghapusan pendapatan.</p>',
'404_title' => 'Halaman Tidak Ditemukan',
'404_text' => 'Halaman yang Anda cari mungkin telah dihapus, namanya diubah, atau sementara tidak tersedia.',
'back_to_home' => 'Kembali ke Beranda',
'newsletter_invalid_email' => 'Harap berikan alamat email yang valid.',
'newsletter_success' => 'Terima kasih telah berlangganan!',
'newsletter_already_subscribed' => 'Anda sudah berlangganan!',
'newsletter_error' => 'Terjadi kesalahan. Silakan coba lagi.',
);

View File

@ -1,48 +1,39 @@
<?php <?php
// Mail configuration sourced from environment variables. // Mail configuration sourced from environment variables.
// No secrets are stored here; the file just maps env -> config array for MailService. // Updated to prioritize local .env in workspace root.
function env_val(string $key, $default = null) { function env_val(string $key, $default = null) {
$v = getenv($key); $v = getenv($key);
return ($v === false || $v === null || $v === '') ? $default : $v; return ($v === false || $v === null || $v === '') ? $default : $v;
} }
// Fallback: if critical vars are missing from process env, try to parse executor/.env // Loads .env files (either executor/.env or local .env)
// This helps in web/Apache contexts where .env is not exported. function load_env(): void {
// Supports simple KEY=VALUE lines; ignores quotes and comments. $paths = [__DIR__ . '/../../.env', __DIR__ . '/../.env']; // executor/.env and workspace/.env
function load_dotenv_if_needed(array $keys): void { foreach ($paths as $envPath) {
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === ''); $envPath = realpath($envPath);
if (empty($missing)) return; if ($envPath && is_readable($envPath)) {
static $loaded = false; $lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
if ($loaded) return; foreach ($lines as $line) {
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env if ($line[0] === '#' || trim($line) === '') continue;
if ($envPath && is_readable($envPath)) { if (!str_contains($line, '=')) continue;
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; [$k, $v] = array_map('trim', explode('=', $line, 2));
foreach ($lines as $line) { $v = trim($v, "' ");
if ($line[0] === '#' || trim($line) === '') continue; // Set env if not set
if (!str_contains($line, '=')) continue; if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
[$k, $v] = array_map('trim', explode('=', $line, 2)); putenv("{$k}={$v}");
// Strip potential surrounding quotes }
$v = trim($v, "\"' ");
// Do not override existing env
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
putenv("{$k}={$v}");
} }
} }
$loaded = true;
} }
} }
load_dotenv_if_needed([ load_env();
'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS',
'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO',
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
]);
$transport = env_val('MAIL_TRANSPORT', 'smtp'); $transport = env_val('MAIL_TRANSPORT', 'smtp');
$smtp_host = env_val('SMTP_HOST'); $smtp_host = env_val('SMTP_HOST');
$smtp_port = (int) env_val('SMTP_PORT', 587); $smtp_port = (int) env_val('SMTP_PORT', 587);
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null $smtp_secure = env_val('SMTP_SECURE', 'tls');
$smtp_user = env_val('SMTP_USER'); $smtp_user = env_val('SMTP_USER');
$smtp_pass = env_val('SMTP_PASS'); $smtp_pass = env_val('SMTP_PASS');
@ -50,27 +41,14 @@ $from_email = env_val('MAIL_FROM', 'no-reply@localhost');
$from_name = env_val('MAIL_FROM_NAME', 'App'); $from_name = env_val('MAIL_FROM_NAME', 'App');
$reply_to = env_val('MAIL_REPLY_TO'); $reply_to = env_val('MAIL_REPLY_TO');
$dkim_domain = env_val('DKIM_DOMAIN');
$dkim_selector = env_val('DKIM_SELECTOR');
$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH');
return [ return [
'transport' => $transport, 'transport' => $transport,
// SMTP
'smtp_host' => $smtp_host, 'smtp_host' => $smtp_host,
'smtp_port' => $smtp_port, 'smtp_port' => $smtp_port,
'smtp_secure' => $smtp_secure, 'smtp_secure' => $smtp_secure,
'smtp_user' => $smtp_user, 'smtp_user' => $smtp_user,
'smtp_pass' => $smtp_pass, 'smtp_pass' => $smtp_pass,
// From / Reply-To
'from_email' => $from_email, 'from_email' => $from_email,
'from_name' => $from_name, 'from_name' => $from_name,
'reply_to' => $reply_to, 'reply_to' => $reply_to,
];
// DKIM (optional)
'dkim_domain' => $dkim_domain,
'dkim_selector' => $dkim_selector,
'dkim_private_key_path' => $dkim_private_key_path,
];

54
update_blog_images.php Normal file
View File

@ -0,0 +1,54 @@
<?php
require_once __DIR__ . '/db/config.php';
// Helper functions from guidelines
function pexels_key() {
$k = getenv('PEXELS_KEY');
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
}
function pexels_get($url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
return null;
}
function download_to($srcUrl, $destPath) {
$data = file_get_contents($srcUrl);
if ($data === false) return false;
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
return file_put_contents($destPath, $data) !== false;
}
$db = db();
$posts = $db->query("SELECT id, title FROM posts WHERE image_path IS NULL OR image_path = ''")->fetchAll();
foreach ($posts as $post) {
$q = urlencode($post['title'] . " android tech");
$url = 'https://api.pexels.com/v1/search?query=' . $q . '&orientation=landscape&per_page=1&page=1';
$data = pexels_get($url);
if ($data && !empty($data['photos'])) {
$photo = $data['photos'][0];
$src = $photo['src']['large'] ?? $photo['src']['original'];
$filename = 'assets/images/pexels/' . $photo['id'] . '.jpg';
$target = __DIR__ . '/' . $filename;
if (download_to($src, $target)) {
$db->prepare("UPDATE posts SET image_path = ? WHERE id = ?")->execute([$filename, $post['id']]);
echo "Added image to: " . $post['title'] . "\n";
} else {
echo "Failed to download image for: " . $post['title'] . "\n";
}
} else {
echo "No image found for: " . $post['title'] . "\n";
}
}

76
views/404.php Normal file
View File

@ -0,0 +1,76 @@
<?php include 'header.php'; ?>
<div class="container overflow-hidden">
<div class="row min-vh-100 align-items-center justify-content-center">
<div class="col-md-10 col-lg-8 text-center position-relative">
<!-- Large Decorative background 404 -->
<div class="position-absolute top-50 start-50 translate-middle z-0 opacity-5 d-none d-md-block" style="font-size: 20rem; font-weight: 900; letter-spacing: -0.5rem; pointer-events: none; user-select: none;">
404
</div>
<div class="position-relative z-1 py-5">
<div class="mb-5 floating-animation">
<!-- Custom SVG Illustration instead of icons for better reliability -->
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" class="mx-auto shadow-sm p-4 rounded-circle bg-white border border-light">
<path d="M100 0C44.7715 0 0 44.7715 0 100C0 155.228 44.7715 200 100 200C155.228 200 200 155.228 200 100C200 44.7715 155.228 0 100 0ZM100 185C53.0558 185 15 146.944 15 100C15 53.0558 53.0558 15 100 15C146.944 15 185 53.0558 185 100C185 146.944 146.944 185 100 185Z" fill="#00C853" fill-opacity="0.1"/>
<circle cx="100" cy="100" r="70" fill="#00C853" fill-opacity="0.05"/>
<path d="M100 60V110" stroke="#00C853" stroke-width="12" stroke-linecap="round"/>
<circle cx="100" cy="135" r="8" fill="#00C853"/>
<path d="M60 85L50 95M140 85L150 95" stroke="#00C853" stroke-width="8" stroke-linecap="round"/>
</svg>
</div>
<h1 class="display-3 fw-bold mb-3 text-dark"><?php echo __('404_title'); ?></h1>
<p class="text-muted fs-5 mb-5 max-width-600 mx-auto px-3">
<?php echo __('404_text'); ?>
</p>
<div class="d-flex flex-column flex-sm-row gap-3 justify-content-center align-items-center mb-5 px-3">
<a href="/" class="btn btn-success btn-lg rounded-pill px-5 py-3 shadow-sm hover-lift d-flex align-items-center w-100 w-sm-auto">
<i class="bi bi-house-door-fill me-2"></i>
<span class="fw-bold"><?php echo __('back_to_home'); ?></span>
</a>
<button onclick="window.history.back()" class="btn btn-outline-dark btn-lg rounded-pill px-5 py-3 hover-lift d-flex align-items-center w-100 w-sm-auto">
<i class="bi bi-arrow-left me-2"></i>
<span class="fw-bold"><?php echo __('Kembali', 'Kembali'); ?></span>
</button>
</div>
<div class="mt-5 p-4 bg-white rounded-5 shadow-sm border border-light-subtle max-width-500 mx-auto mx-3">
<p class="text-muted small fw-medium mb-3"><?php echo __('search_placeholder'); ?></p>
<form action="/" method="GET">
<div class="input-group bg-light rounded-pill p-1 border">
<span class="input-group-text bg-transparent border-0 ps-3">
<i class="bi bi-search text-muted small"></i>
</span>
<input type="text" name="search" class="form-control bg-transparent border-0 py-2 fs-6" placeholder="<?php echo __('search', 'Search...'); ?>">
<button class="btn btn-success rounded-pill px-4 fw-bold" type="submit"><?php echo __('search'); ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.max-width-600 {
max-width: 600px;
}
.max-width-500 {
max-width: 500px;
}
.bg-light {
background-color: #f8f9fa !important;
}
.shadow-sm {
box-shadow: 0 .125rem .25rem rgba(0,0,0,.075) !important;
}
@media (max-width: 768px) {
.display-3 {
font-size: 2.5rem;
}
}
</style>
<?php include 'footer.php'; ?>

98
views/admin/apks/form.php Normal file
View File

@ -0,0 +1,98 @@
<?php include __DIR__ . '/../header.php'; ?>
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<!-- Search bar at the top of the form for quick access -->
<div class="mb-4">
<form action="/admin/apks" method="GET">
<div class="input-group shadow-sm">
<span class="input-group-text bg-white border-0"><i class="fas fa-search text-primary"></i></span>
<input type="text" name="search" class="form-control border-0" placeholder="Search for existing APKs to edit..." aria-label="Search APKs">
<button class="btn btn-primary" type="submit">Search</button>
</div>
</form>
</div>
<div class="card shadow-lg border-0 rounded-4">
<div class="card-header bg-white py-3">
<h5 class="m-0 fw-bold"><?php echo $action === 'add' ? 'Add New APK' : 'Edit APK: ' . htmlspecialchars($apk['title']); ?></h5>
</div>
<div class="card-body p-4">
<form action="<?php echo $action === 'add' ? '/admin/apks/add' : '/admin/apks/edit/' . $apk['id']; ?>" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label fw-medium">APK Title</label>
<input type="text" class="form-control" id="title" name="title" value="<?php echo htmlspecialchars($apk['title'] ?? ''); ?>" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="version" class="form-label fw-medium">Version</label>
<input type="text" class="form-control" id="version" name="version" value="<?php echo htmlspecialchars($apk['version'] ?? ''); ?>" required>
</div>
<div class="col-md-6 mb-3">
<label for="category_id" class="form-label fw-medium"><?php echo __('categories'); ?></label>
<select class="form-select" id="category_id" name="category_id">
<option value="">Uncategorized</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat['id']; ?>" <?php echo (isset($apk['category_id']) && $apk['category_id'] == $cat['id']) ? 'selected' : ''; ?>><?php echo htmlspecialchars($cat['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label fw-medium">Description</label>
<textarea class="form-control" id="description" name="description" rows="4" required><?php echo htmlspecialchars($apk['description'] ?? ''); ?></textarea>
</div>
<div class="mb-4">
<label class="form-label fw-medium">APK Icon</label>
<div class="d-flex align-items-center mb-2">
<?php if (!empty($apk['icon_path'])): ?>
<img src="/<?php echo $apk['icon_path']; ?>" class="rounded me-3 border shadow-sm" width="64" height="64">
<?php endif; ?>
<div class="flex-grow-1">
<input type="file" class="form-control" id="icon_file" name="icon_file" accept="image/*">
<div class="form-text text-muted"><i class="fas fa-info-circle me-1"></i> Icons are automatically compressed to optimize speed.</div>
</div>
</div>
<div class="mb-3">
<label for="image_url" class="form-label fw-medium">External Icon URL (Optional)</label>
<input type="url" class="form-control" id="image_url" name="image_url" value="<?php echo htmlspecialchars($apk['image_url'] ?? ''); ?>" placeholder="https://example.com/icon.png">
</div>
</div>
<div class="mb-3">
<label for="download_url" class="form-label fw-medium">Download URL</label>
<input type="url" class="form-control" id="download_url" name="download_url" value="<?php echo htmlspecialchars($apk['download_url'] ?? ''); ?>" required>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label for="status" class="form-label fw-medium">Status</label>
<select class="form-select" id="status" name="status">
<option value="published" <?php echo (isset($apk['status']) && $apk['status'] === 'published') ? 'selected' : ''; ?>>Published</option>
<option value="draft" <?php echo (isset($apk['status']) && $apk['status'] === 'draft') ? 'selected' : ''; ?>>Draft</option>
</select>
</div>
<div class="col-md-6 d-flex align-items-center mt-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="is_vip" name="is_vip" <?php echo (isset($apk['is_vip']) && $apk['is_vip']) ? 'checked' : ''; ?>>
<label class="form-check-label fw-medium" for="is_vip">VIP APK</label>
</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end border-top pt-4">
<a href="/admin/apks" class="btn btn-light px-4 fw-bold">Cancel</a>
<button type="submit" class="btn btn-primary px-5 fw-bold"><i class="fas fa-save me-2"></i>Save APK</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/../footer.php'; ?>

137
views/admin/apks/index.php Normal file
View File

@ -0,0 +1,137 @@
<?php include __DIR__ . '/../header.php'; ?>
<div class="container-fluid py-4">
<div class="d-flex align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">Manage APKs</h1>
<form action="/admin/apks" method="GET" class="d-flex gap-2 ms-auto me-3">
<input type="text" name="search" class="form-control form-control-sm" placeholder="Search APKs..." value="<?php echo htmlspecialchars($_GET['search'] ?? ''); ?>">
<button type="submit" class="btn btn-sm btn-outline-secondary"><i class="bi bi-search"></i></button>
</form>
<a href="/admin/apks/add" class="btn btn-primary shadow-sm">
<i class="bi bi-plus-lg me-1"></i> Add New APK
</a>
</div>
<div id="reorderAlert" class="alert alert-success alert-dismissible fade show" style="display: none;" role="alert">
Order updated successfully!
<button type="button" class="btn-close" onclick="this.parentElement.style.display='none'"></button>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">APK List (Drag to Reorder)</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" id="apkTable">
<thead class="table-light">
<tr>
<th width="50"></th>
<th>Icon</th>
<th>Title</th>
<th>Version</th>
<th>Category</th>
<th>Downloads</th>
<th>Status</th>
<th>VIP</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="sortableList">
<?php foreach ($apks as $apk): ?>
<tr data-id="<?php echo $apk['id']; ?>">
<td class="align-middle text-center cursor-move">
<i class="bi bi-grip-vertical text-muted fs-5"></i>
</td>
<td class="align-middle">
<?php
$icon = !empty($apk['icon_path']) ? '/'.$apk['icon_path'] : $apk['image_url'];
?>
<img src="<?php echo $icon; ?>" class="rounded" width="40" height="40" alt="">
</td>
<td class="align-middle fw-bold"><?php echo $apk['title']; ?></td>
<td class="align-middle"><?php echo $apk['version']; ?></td>
<td class="align-middle">
<?php
$db = db_pdo();
$cat = $db->query("SELECT name FROM categories WHERE id = " . ($apk['category_id'] ?: 0))->fetchColumn();
echo $cat ?: 'Uncategorized';
?>
</td>
<td class="align-middle"><?php echo number_format($apk['total_downloads']); ?></td>
<td class="align-middle">
<span class="badge bg-<?php echo $apk['status'] === 'published' ? 'success' : 'secondary'; ?>">
<?php echo ucfirst($apk['status']); ?>
</span>
</td>
<td class="align-middle">
<?php if ($apk['is_vip']): ?>
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill"></i> VIP</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td class="align-middle">
<div class="btn-group btn-group-sm">
<a href="/admin/apks/edit/<?php echo $apk['id']; ?>" class="btn btn-outline-primary">
<i class="bi bi-pencil"></i>
</a>
<a href="/admin/apks/delete/<?php echo $apk['id']; ?>" class="btn btn-outline-danger" onclick="return confirm('Are you sure?')">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
const el = document.getElementById('sortableList');
const sortable = Sortable.create(el, {
animation: 150,
handle: '.cursor-move',
onEnd: function (evt) {
const rows = el.querySelectorAll('tr');
const order = Array.from(rows).map(row => row.dataset.id);
const alertBox = document.getElementById('reorderAlert');
fetch('/admin/apks/reorder', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'order[]=' + order.join('&order[]=')
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
if (data.success) {
console.log('Order updated');
alertBox.style.display = 'block';
setTimeout(() => {
alertBox.style.display = 'none';
}, 3000);
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to update order. Please check your connection.');
});
}
});
</script>
<style>
.cursor-move { cursor: move; }
.sortable-ghost { opacity: 0.4; background-color: #f8f9fa; }
</style>
<?php include __DIR__ . '/../footer.php'; ?>

View File

@ -0,0 +1,96 @@
<?php include __DIR__ . '/../header.php'; ?>
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card shadow-lg border-0 rounded-4">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h5 class="m-0 fw-bold">Mass Upload APKs</h5>
<button type="button" class="btn btn-outline-primary btn-sm fw-bold" id="add-row">
<i class="fas fa-plus me-1"></i> Add Row
</button>
</div>
<div class="card-body p-4">
<form action="/admin/apks/mass-upload" method="POST" enctype="multipart/form-data">
<div class="row mb-3">
<div class="col-md-6">
<label for="category_id" class="form-label fw-medium">Common Category</label>
<select class="form-select" id="category_id" name="category_id">
<option value="">Uncategorized</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat['id']; ?>"><?php echo htmlspecialchars($cat['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label for="status" class="form-label fw-medium">Common Status</label>
<select class="form-select" id="status" name="status">
<option value="published">Published</option>
<option value="draft">Draft</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered align-middle" id="mass-upload-table">
<thead class="table-light">
<tr>
<th style="width: 25%;">Title</th>
<th style="width: 15%;">Version</th>
<th style="width: 25%;">Icon File</th>
<th style="width: 30%;">Download URL</th>
<th style="width: 5%;"></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text" name="titles[]" class="form-control" required></td>
<td><input type="text" name="versions[]" class="form-control" placeholder="1.0.0"></td>
<td><input type="file" name="icon_files[]" class="form-control" accept="image/*"></td>
<td><input type="url" name="download_urls[]" class="form-control" required placeholder="https://..."></td>
<td><button type="button" class="btn btn-outline-danger btn-sm remove-row"><i class="fas fa-times"></i></button></td>
</tr>
</tbody>
</table>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end border-top pt-4 mt-3">
<a href="/admin/apks" class="btn btn-light px-4 fw-bold">Cancel</a>
<button type="submit" class="btn btn-primary px-5 fw-bold"><i class="fas fa-upload me-2"></i>Start Mass Upload</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tableBody = document.querySelector('#mass-upload-table tbody');
const addRowBtn = document.querySelector('#add-row');
addRowBtn.addEventListener('click', function() {
const newRow = document.createElement('tr');
newRow.innerHTML = `
<td><input type="text" name="titles[]" class="form-control" required></td>
<td><input type="text" name="versions[]" class="form-control" placeholder="1.0.0"></td>
<td><input type="file" name="icon_files[]" class="form-control" accept="image/*"></td>
<td><input type="url" name="download_urls[]" class="form-control" required placeholder="https://..."></td>
<td><button type="button" class="btn btn-outline-danger btn-sm remove-row"><i class="fas fa-times"></i></button></td>
`;
tableBody.appendChild(newRow);
});
tableBody.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-row') || e.target.parentElement.classList.contains('remove-row')) {
const row = e.target.closest('tr');
if (tableBody.querySelectorAll('tr').length > 1) {
row.remove();
}
}
});
});
</script>
<?php include __DIR__ . '/../footer.php'; ?>

View File

@ -0,0 +1,60 @@
<?php include __DIR__ . '/../header.php'; ?>
<div class="container py-4">
<div class="row">
<div class="col-md-5 mb-4">
<div class="card shadow border-0 rounded-4">
<div class="card-header bg-white py-3">
<h5 class="m-0 fw-bold">Add New Category</h5>
</div>
<div class="card-body p-4">
<form action="/admin/categories/add" method="POST">
<div class="mb-3">
<label for="name" class="form-label fw-medium">Category Name</label>
<input type="text" class="form-control" id="name" name="name" placeholder="e.g. Games, Social Media" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary rounded-pill py-2">Create Category</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-7">
<div class="card shadow border-0 rounded-4">
<div class="card-header bg-white py-3">
<h5 class="m-0 fw-bold">All Categories</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="ps-4">Name</th>
<th>Slug</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $cat): ?>
<tr>
<td class="ps-4 align-middle fw-medium"><?php echo $cat['name']; ?></td>
<td class="align-middle text-muted"><?php echo $cat['slug']; ?></td>
<td class="text-end pe-4 align-middle">
<a href="/admin/categories/delete/<?php echo $cat['id']; ?>" class="btn btn-outline-danger btn-sm rounded-pill px-3" onclick="return confirm('Are you sure you want to delete this category?')">
<i class="bi bi-trash"></i> Delete
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/../footer.php'; ?>

213
views/admin/dashboard.php Normal file
View File

@ -0,0 +1,213 @@
<?php include __DIR__ . '/header.php'; ?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?php echo __('admin_dashboard'); ?></h1>
<div class="d-flex gap-2">
<a href="/admin/apks/add" class="btn btn-primary shadow-sm rounded-pill px-4 fw-bold">
<i class="fas fa-plus me-1"></i> Add APK
</a>
<a href="/admin/settings" class="btn btn-outline-secondary shadow-sm rounded-pill px-4 fw-bold">
<i class="fas fa-cog me-1"></i> <?php echo __('settings'); ?>
</a>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm rounded-4 h-100 py-2 border-start border-primary border-4 bg-white">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1 small fw-bold">Total APKs</div>
<div class="h4 mb-0 font-weight-bold text-gray-800"><?php echo number_format($total_apks); ?></div>
</div>
<div class="col-auto">
<i class="fas fa-mobile-alt fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm rounded-4 h-100 py-2 border-start border-success border-4 bg-white">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1 small fw-bold">Total Downloads</div>
<div class="h4 mb-0 font-weight-bold text-gray-800"><?php echo number_format($total_downloads); ?></div>
</div>
<div class="col-auto">
<i class="fas fa-download fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm rounded-4 h-100 py-2 border-start border-info border-4 bg-white">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1 small fw-bold">Total Users</div>
<div class="h4 mb-0 font-weight-bold text-gray-800"><?php echo number_format($total_users); ?></div>
</div>
<div class="col-auto">
<i class="fas fa-users fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm rounded-4 h-100 py-2 border-start border-warning border-4 bg-white">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1 small fw-bold">Pending Withdrawals</div>
<div class="h4 mb-0 font-weight-bold text-gray-800"><?php echo number_format($pending_withdrawals); ?></div>
</div>
<div class="col-auto">
<i class="fas fa-wallet fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm border-0 rounded-4 bg-white">
<div class="card-header bg-white py-3 border-bottom border-light d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary fw-bold">Referral Downloads (Last 7 Days)</h6>
</div>
<div class="card-body">
<canvas id="referralChart" style="height: 300px; width: 100%;"></canvas>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('referralChart').getContext('2d');
const stats = <?php echo json_encode($referral_stats); ?>;
const labels = stats.map(s => s.date);
const counts = stats.map(s => s.count);
new Chart(ctx, {
type: 'line',
data: {
labels: labels.length ? labels : ['No Data'],
datasets: [{
label: 'Referral Downloads',
data: counts.length ? counts : [0],
borderColor: '#10B981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 5,
pointBackgroundColor: '#10B981'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)'
}
},
x: {
grid: {
display: false
}
}
}
}
});
});
</script>
<div class="row g-4">
<div class="col-lg-8">
<div class="card shadow-sm border-0 rounded-4 mb-4 bg-white">
<div class="card-header bg-white py-3 border-bottom border-light">
<h6 class="m-0 font-weight-bold text-primary fw-bold">Recent APKs</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th class="border-0">Title</th>
<th class="border-0">Version</th>
<th class="border-0">Downloads</th>
<th class="border-0">Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent_apks as $apk): ?>
<tr>
<td>
<div class="fw-bold text-dark"><?php echo htmlspecialchars($apk['title']); ?></div>
<small class="text-muted"><?php echo htmlspecialchars($apk['slug']); ?></small>
</td>
<td>v<?php echo htmlspecialchars($apk['version']); ?></td>
<td><i class="fas fa-download me-1 text-muted"></i> <?php echo number_format($apk['total_downloads']); ?></td>
<td>
<span class="badge rounded-pill bg-<?php echo $apk['status'] === 'published' ? 'success' : 'secondary'; ?> px-3 py-2">
<?php echo ucfirst($apk['status']); ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0 rounded-4 bg-white">
<div class="card-header bg-white py-3 border-bottom border-light">
<h6 class="m-0 font-weight-bold text-primary fw-bold">Quick Navigation</h6>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush rounded-bottom-4">
<a href="/admin/apks" class="list-group-item list-group-item-action py-3 px-4 border-0">
<i class="fas fa-mobile-alt me-2 text-primary"></i> Manage APKs
</a>
<a href="/admin/categories" class="list-group-item list-group-item-action py-3 px-4 border-0">
<i class="fas fa-tags me-2 text-info"></i> <?php echo __('categories'); ?>
</a>
<a href="/admin/withdrawals" class="list-group-item list-group-item-action py-3 px-4 border-0">
<i class="fas fa-cash-register me-2 text-success"></i> <?php echo __('manage_withdrawals'); ?>
<?php if ($pending_withdrawals > 0): ?>
<span class="badge bg-danger rounded-pill float-end"><?php echo $pending_withdrawals; ?></span>
<?php endif; ?>
</a>
<a href="/admin/settings" class="list-group-item list-group-item-action py-3 px-4 border-0">
<i class="fas fa-cog me-2 text-secondary"></i> <?php echo __('settings'); ?>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/footer.php'; ?>

73
views/admin/footer.php Normal file
View File

@ -0,0 +1,73 @@
</main>
<footer class="footer-admin mt-auto py-4 border-top">
<div class="container-fluid px-4">
<div class="d-flex align-items-center justify-content-between small">
<div class="text-muted">Copyright &copy; <?php echo htmlspecialchars(get_setting('site_name', 'APK ADMIN')); ?> <?php echo date('Y'); ?></div>
<div>
<a href="#" class="text-muted text-decoration-none">Privacy Policy</a>
&middot;
<a href="#" class="text-muted text-decoration-none">Terms &amp; Conditions</a>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
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') {
icon.className = 'fa-solid fa-sun';
} else {
icon.className = 'fa-solid fa-moon';
}
});
// Update all theme status texts
const textLabels = document.querySelectorAll('.theme-status-text');
textLabels.forEach(label => {
label.textContent = theme === 'dark' ? 'Dark Mode' : 'Light Mode';
});
};
// Theme Toggle Logic
const initThemeToggle = (btnId) => {
const themeToggle = document.getElementById(btnId);
if (!themeToggle) return;
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);
});
};
// Initial Sync
const currentTheme = html.getAttribute('data-theme') || 'light';
updateIcons(currentTheme);
initThemeToggle('theme-toggle');
initThemeToggle('theme-toggle-mobile');
// Sidebar toggle logic if needed in future
console.log('Admin Dashboard ready.');
});
</script>
</body>
</html>

257
views/admin/header.php Normal file
View File

@ -0,0 +1,257 @@
<?php $currentTheme = \App\Services\ThemeService::getCurrent(); ?>
<!DOCTYPE html>
<html lang="<?php echo \App\Services\LanguageService::getLang(); ?>" data-theme="<?php echo $currentTheme; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo __('admin_dashboard'); ?> - <?php echo htmlspecialchars(get_setting('site_name', 'APK ADMIN')); ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="icon" type="image/x-icon" href="/<?php echo get_setting('site_favicon'); ?>">
<script>
// Early theme initialization
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
})();
</script>
<style>
:root {
--primary-color: #4e73df;
--success-color: #1cc88a;
--bg-color: #f8f9fc;
--card-bg: #ffffff;
--text-color: #5a5c69;
--navbar-bg: #ffffff;
--border-color: #e3e6f0;
--subtle-bg: #eaecf4;
--accent-color: #4e73df;
}
[data-theme="dark"] {
--bg-color: #0f172a;
--card-bg: #1e293b;
--text-color: #f1f5f9;
--navbar-bg: #1e293b;
--border-color: rgba(255, 255, 255, 0.1);
--subtle-bg: #0f172a;
--accent-color: #60a5fa;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Inter', sans-serif;
transition: background-color 0.3s, color 0.3s;
}
.navbar-admin {
background-color: var(--navbar-bg);
box-shadow: 0 .15rem 1.75rem 0 rgba(58,59,69,.15);
border-bottom: 1px solid var(--border-color);
}
.nav-link {
color: var(--primary-color);
padding: 0.5rem 1rem;
border-radius: 0.35rem;
transition: all 0.2s;
font-weight: 600;
}
[data-theme="dark"] .nav-link {
color: #858796;
}
.nav-link:hover {
background-color: rgba(78, 115, 223, 0.1);
color: #224abe;
}
.nav-link.active {
background-color: var(--primary-color);
color: #fff !important;
}
.card {
background-color: var(--card-bg);
border-color: var(--border-color);
color: var(--text-color);
}
.theme-toggle-btn {
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 38px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border-color);
background: var(--subtle-bg);
color: var(--text-color);
}
.theme-toggle-btn:hover {
background-color: var(--border-color);
transform: rotate(15deg) scale(1.05);
}
[data-theme="dark"] .theme-toggle-btn {
box-shadow: 0 0 10px rgba(245, 158, 11, 0.2);
border-color: rgba(245, 158, 11, 0.3);
color: #F59E0B;
}
[data-theme="dark"] .bg-white {
background-color: var(--card-bg) !important;
}
[data-theme="dark"] .text-muted {
color: #94a3b8 !important;
}
[data-theme="dark"] .form-control {
background-color: #0f172a;
border-color: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
[data-theme="dark"] .navbar-toggler-icon {
filter: invert(1);
}
.lang-selector-btn {
background: var(--subtle-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.4rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
color: var(--text-color);
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
}
.lang-selector-btn:hover {
background-color: var(--border-color);
}
@media (max-width: 991.98px) {
.navbar-collapse {
background-color: var(--card-bg);
margin-top: 1rem;
padding: 1rem;
border-radius: 0.75rem;
border: 1px solid var(--border-color);
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.1);
}
.admin-controls-row {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 1rem;
padding-top: 1rem;
margin-top: 1rem;
border-top: 1px solid var(--border-color);
}
.admin-controls-row > div,
.admin-controls-row > button {
width: 100%;
justify-content: space-between !important;
}
.mobile-theme-row {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--subtle-bg);
padding: 0.5rem 1rem;
border-radius: 8px;
}
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-admin sticky-top py-3">
<div class="container-fluid px-4">
<a class="navbar-brand fw-bold text-primary d-flex align-items-center" href="/admin/dashboard">
<?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-shield-halved me-2"></i>
<?php endif; ?>
<?php echo htmlspecialchars(get_setting('site_name', 'APK ADMIN')); ?>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link px-3 mb-1" 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 mb-1" href="/admin/users"><i class="fas fa-users me-1"></i> Members</a>
</li>
<li class="nav-item">
<a class="nav-link px-3 mb-1" href="/admin/apks"><i class="fas fa-mobile-alt me-1"></i> APKs</a>
</li>
<li class="nav-item">
<a class="nav-link px-3 mb-1" href="/admin/apks/mass-upload"><i class="fas fa-cloud-upload-alt me-1"></i> Mass Upload</a>
</li>
<li class="nav-item">
<a class="nav-link px-3 mb-1" href="/admin/posts"><i class="fas fa-newspaper me-1"></i> Blog</a>
</li>
<li class="nav-item">
<a class="nav-link px-3 mb-1" href="/admin/categories"><i class="fas fa-list me-1"></i> <?php echo __('categories'); ?></a>
</li>
<li class="nav-item">
<a class="nav-link px-3 mb-1" href="/admin/withdrawals"><i class="fas fa-wallet me-1"></i> <?php echo __('manage_withdrawals'); ?></a>
</li>
<li class="nav-item">
<a class="nav-link px-3 mb-1" href="/admin/settings"><i class="fas fa-cog me-1"></i> <?php echo __('settings'); ?></a>
</li>
</ul>
<div class="d-lg-flex align-items-center ms-auto admin-controls-row">
<!-- Theme & Lang Row (Desktop) / Column (Mobile) -->
<div class="d-none d-lg-flex align-items-center">
<button id="theme-toggle" class="theme-toggle-btn me-3" aria-label="Toggle theme">
<i class="fa-solid <?php echo $currentTheme === 'dark' ? 'fa-sun' : 'fa-moon'; ?>"></i>
</button>
<div class="dropdown me-3">
<button class="lang-selector-btn dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-globe"></i> <?php echo \App\Services\LanguageService::getLang() == 'id' ? 'ID' : 'EN'; ?>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
<li><a class="dropdown-item" href="/lang/id">🇮🇩 Indonesia</a></li>
<li><a class="dropdown-item" href="/lang/en">🇺🇸 English</a></li>
</ul>
</div>
</div>
<!-- Mobile Specific Controls -->
<div class="d-lg-none">
<div class="mobile-theme-row mb-2">
<span class="theme-status-text small fw-bold"><?php echo $currentTheme === 'dark' ? 'Dark Mode' : 'Light Mode'; ?></span>
<button id="theme-toggle-mobile" class="theme-toggle-btn" aria-label="Toggle theme">
<i class="fa-solid <?php echo $currentTheme === 'dark' ? 'fa-sun' : 'fa-moon'; ?>"></i>
</button>
</div>
<div class="dropdown mb-3">
<button class="lang-selector-btn w-100 justify-content-between dropdown-toggle" type="button" data-bs-toggle="dropdown">
<span><i class="fas fa-globe me-2"></i>Language</span>
<span><?php echo \App\Services\LanguageService::getLang() == 'id' ? 'ID' : 'EN'; ?></span>
</button>
<ul class="dropdown-menu shadow border-0 w-100">
<li><a class="dropdown-item" href="/lang/id">🇮🇩 Indonesia</a></li>
<li><a class="dropdown-item" href="/lang/en">🇺🇸 English</a></li>
</ul>
</div>
</div>
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<span class="text-muted me-3"><b><?php echo $_SESSION['username'] ?? 'Admin'; ?></b></span>
<a href="/" class="btn btn-outline-secondary btn-sm me-2" target="_blank" title="View Site"><i class="fas fa-external-link-alt"></i></a>
</div>
<a href="/admin/logout" class="btn btn-danger btn-sm px-3"><i class="fas fa-sign-out-alt me-1"></i> Logout</a>
</div>
</div>
</div>
</div>
</nav>
<main class="min-vh-100">

53
views/admin/login.php Normal file
View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - ApkNusa</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
width: 100%;
max-width: 400px;
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
background: white;
}
.btn-primary {
background-color: #a4c639; /* Android Green */
border-color: #a4c639;
}
.btn-primary:hover {
background-color: #8fb132;
border-color: #8fb132;
}
</style>
</head>
<body>
<div class="login-card">
<h2 class="text-center mb-4">ApkNusa Admin</h2>
<?php if (isset($error)): ?>
<div class="alert alert-danger"><?= $error ?></div>
<?php endif; ?>
<form action="/admin/login" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,52 @@
<?php include dirname(__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 text-gray-800"><?php echo $action === 'add' ? 'Add New Post' : 'Edit Post'; ?></h2>
<a href="/admin/posts" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> Back to List
</a>
</div>
<div class="card shadow mb-4">
<div class="card-body">
<form action="/admin/posts/<?php echo $action === 'add' ? 'add' : 'edit/' . $post['id']; ?>" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label fw-bold">Title</label>
<input type="text" name="title" class="form-control" value="<?php echo htmlspecialchars($post['title'] ?? ''); ?>" required>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Featured Image</label>
<input type="file" name="image_file" class="form-control" accept="image/*">
<?php if (isset($post['image_path']) && $post['image_path']): ?>
<div class="mt-2">
<img src="/<?php echo $post['image_path']; ?>" alt="" class="img-thumbnail" style="max-height: 150px;">
</div>
<?php endif; ?>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Content</label>
<textarea name="content" class="form-control" rows="15" required><?php echo htmlspecialchars($post['content'] ?? ''); ?></textarea>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Status</label>
<select name="status" class="form-control">
<option value="published" <?php echo (isset($post['status']) && $post['status'] === 'published') ? 'selected' : ''; ?>>Published</option>
<option value="draft" <?php echo (isset($post['status']) && $post['status'] === 'draft') ? 'selected' : ''; ?>>Draft</option>
</select>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fas fa-save me-1"></i> <?php echo $action === 'add' ? 'Create Post' : 'Update Post'; ?>
</button>
</div>
</form>
</div>
</div>
</div>
<?php include dirname(__DIR__) . '/footer.php'; ?>

View File

@ -0,0 +1,68 @@
<?php include dirname(__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 text-gray-800">Blog Posts</h2>
<a href="/admin/posts/add" class="btn btn-primary">
<i class="fas fa-plus me-1"></i> Add New Post
</a>
</div>
<div class="card shadow mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" width="100%" cellspacing="0">
<thead>
<tr>
<th>Image</th>
<th>Title</th>
<th>Status</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($posts as $post): ?>
<tr>
<td style="width: 80px;">
<?php if ($post['image_path']): ?>
<img src="/<?php echo $post['image_path']; ?>" alt="" class="img-thumbnail" style="height: 50px; width: 50px; object-fit: cover;">
<?php else: ?>
<div class="bg-light d-flex align-items-center justify-content-center img-thumbnail" style="height: 50px; width: 50px;">
<i class="fas fa-image text-muted"></i>
</div>
<?php endif; ?>
</td>
<td>
<div class="fw-bold"><?php echo htmlspecialchars($post['title']); ?></div>
<small class="text-muted"><?php echo $post['slug']; ?></small>
</td>
<td>
<span class="badge bg-<?php echo $post['status'] === 'published' ? 'success' : 'warning'; ?>">
<?php echo ucfirst($post['status']); ?>
</span>
</td>
<td><?php echo date('M d, Y', strtotime($post['created_at'])); ?></td>
<td>
<a href="/admin/posts/edit/<?php echo $post['id']; ?>" class="btn btn-sm btn-info">
<i class="fas fa-edit"></i>
</a>
<a href="/admin/posts/delete/<?php echo $post['id']; ?>" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">
<i class="fas fa-trash"></i>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($posts)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">No posts found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include dirname(__DIR__) . '/footer.php'; ?>

146
views/admin/settings.php Normal file
View File

@ -0,0 +1,146 @@
<?php include 'header.php'; ?>
<div class="container-fluid py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold text-primary"><i class="fas fa-cog me-2"></i><?php echo __('general_settings'); ?></h5>
</div>
<div class="card-body p-4">
<form action="/admin/settings" method="POST" enctype="multipart/form-data">
<div class="mb-4">
<div class="mb-4">
<label class="form-label fw-semibold">Contact Email</label>
<input type="email" name="contact_email" class="form-control" value="<?php echo htmlspecialchars($settings['contact_email'] ?? ''); ?>" placeholder="support@yourdomain.com">
</div>
<label class="form-label fw-semibold"><?php echo __('site_name'); ?></label>
<input type="text" name="site_name" class="form-control form-control-lg" value="<?php echo htmlspecialchars($settings['site_name']); ?>" required>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-semibold"><?php echo __('site_icon'); ?></label>
<input type="file" name="site_icon_file" class="form-control" accept="image/*">
<?php if ($settings['site_icon']): ?>
<div class="mt-2">
<img src="/<?php echo $settings['site_icon']; ?>" alt="Icon" class="img-thumbnail" style="max-height: 50px;">
</div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold"><?php echo __('site_favicon'); ?></label>
<input type="file" name="site_favicon_file" class="form-control" accept="image/*">
<?php if ($settings['site_favicon']): ?>
<div class="mt-2">
<img src="/<?php echo $settings['site_favicon']; ?>" alt="Favicon" class="img-thumbnail" style="max-height: 32px;">
</div>
<?php endif; ?>
</div>
</div>
<div class="mb-4 p-3 bg-light rounded border">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="maintenance_mode" value="1" id="maintenanceMode" <?php echo ($settings['maintenance_mode'] ?? '0') === '1' ? 'checked' : ''; ?>>
<label class="form-check-label fw-bold" for="maintenanceMode">
<i class="fas fa-tools me-2 text-warning"></i>Maintenance Mode
</label>
</div>
<div class="form-text mt-1">When enabled, regular visitors will see a maintenance page. Admins can still access the site.</div>
</div>
<hr class="my-4">
<h5 class="fw-bold mb-3"><i class="fas fa-share-alt me-2"></i>Social Media Settings</h5>
<div class="row mb-3">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Facebook URL</label>
<input type="url" name="facebook_url" class="form-control" value="<?php echo htmlspecialchars($settings['facebook_url'] ?? ''); ?>" placeholder="https://facebook.com/yourpage">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Twitter (X) URL</label>
<input type="url" name="twitter_url" class="form-control" value="<?php echo htmlspecialchars($settings['twitter_url'] ?? ''); ?>" placeholder="https://twitter.com/yourprofile">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Instagram URL</label>
<input type="url" name="instagram_url" class="form-control" value="<?php echo htmlspecialchars($settings['instagram_url'] ?? ''); ?>" placeholder="https://instagram.com/yourprofile">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">GitHub URL</label>
<input type="url" name="github_url" class="form-control" value="<?php echo htmlspecialchars($settings['github_url'] ?? ''); ?>" placeholder="https://github.com/yourprofile">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Telegram URL</label>
<input type="url" name="telegram_url" class="form-control" value="<?php echo htmlspecialchars($settings['telegram_url'] ?? ''); ?>" placeholder="https://t.me/yourchannel">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">WhatsApp URL</label>
<input type="url" name="whatsapp_url" class="form-control" value="<?php echo htmlspecialchars($settings['whatsapp_url'] ?? ''); ?>" placeholder="https://wa.me/yournumber">
</div>
</div>
<hr class="my-4">
<h5 class="fw-bold mb-3"><i class="fas fa-search me-2"></i>SEO Settings</h5>
<div class="mb-4">
<label class="form-label fw-semibold">Meta Description</label>
<textarea name="meta_description" class="form-control" rows="3"><?php echo htmlspecialchars($settings['meta_description'] ?? ''); ?></textarea>
<div class="form-text">Brief description of your site for search engines.</div>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">Meta Keywords</label>
<input type="text" name="meta_keywords" class="form-control" value="<?php echo htmlspecialchars($settings['meta_keywords'] ?? ''); ?>">
<div class="form-text">Comma separated keywords (e.g. apk, games, mod).</div>
</div>
<hr class="my-4">
<h5 class="fw-bold mb-3"><i class="fas fa-code me-2"></i>Custom Scripts & Ads</h5>
<div class="mb-4">
<label class="form-label fw-semibold">Head JS (Ads/Analytics)</label>
<textarea name="head_js" class="form-control" rows="5" placeholder="<script>...</script>"><?php echo htmlspecialchars($settings['head_js'] ?? ''); ?></textarea>
<div class="form-text">Injected before &lt;/head&gt;.</div>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">Body JS (Ads/Analytics)</label>
<textarea name="body_js" class="form-control" rows="5" placeholder="<script>...</script>"><?php echo htmlspecialchars($settings['body_js'] ?? ''); ?></textarea>
<div class="form-text">Injected before &lt;/body&gt;.</div>
</div>
<div class="d-grid mt-4">
<button type="submit" class="btn btn-primary btn-lg py-3 fw-bold">
<i class="fas fa-save me-2"></i><?php echo __('save_settings'); ?>
</button>
</div>
</form>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold text-primary"><i class="fas fa-language me-2"></i><?php echo __('select_language'); ?></h5>
</div>
<div class="card-body p-4 text-center">
<div class="btn-group w-100" role="group">
<a href="/lang/id" class="btn btn-outline-primary py-3 fw-bold <?php echo \App\Services\LanguageService::getLang() == 'id' ? 'active' : ''; ?>">
🇮🇩 <?php echo __('language_indonesia'); ?>
</a>
<a href="/lang/en" class="btn btn-outline-primary py-3 fw-bold <?php echo \App\Services\LanguageService::getLang() == 'en' ? 'active' : ''; ?>">
🇺🇸 <?php echo __('language_english'); ?>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<?php include 'footer.php'; ?>

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

@ -0,0 +1,72 @@
<?php include __DIR__ . '/../header.php'; ?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4 px-3">
<h1 class="h3 mb-0 text-gray-800 fw-bold">Withdrawal Requests</h1>
</div>
<div class="card shadow border-0 rounded-4 mx-3">
<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>Amount (IDR)</th>
<th>Method</th>
<th>Details</th>
<th>Date</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($withdrawals)): ?>
<tr>
<td colspan="7" class="text-center py-5 text-muted">No withdrawal requests found.</td>
</tr>
<?php endif; ?>
<?php foreach ($withdrawals as $wd): ?>
<tr>
<td class="ps-4">
<div class="fw-bold"><?php echo $wd['username']; ?></div>
</td>
<td>
<span class="text-success fw-bold">Rp <?php echo number_format($wd['amount'], 0, ',', '.'); ?></span>
</td>
<td><span class="badge bg-info text-dark"><?php echo $wd['method']; ?></span></td>
<td><small class="text-muted"><?php echo nl2br($wd['account_details']); ?></small></td>
<td><?php echo date('d M Y, H:i', strtotime($wd['created_at'])); ?></td>
<td>
<?php
$statusClass = 'secondary';
if ($wd['status'] === 'pending') $statusClass = 'warning';
if ($wd['status'] === 'approved') $statusClass = 'success';
if ($wd['status'] === 'rejected') $statusClass = 'danger';
?>
<span class="badge bg-<?php echo $statusClass; ?>">
<?php echo ucfirst($wd['status']); ?>
</span>
</td>
<td class="text-end pe-4">
<?php if ($wd['status'] === 'pending'): ?>
<div class="btn-group btn-group-sm">
<a href="/admin/withdrawals/approve/<?php echo $wd['id']; ?>" class="btn btn-success" onclick="return confirm('Approve this withdrawal?')">
Approve
</a>
<a href="/admin/withdrawals/reject/<?php echo $wd['id']; ?>" class="btn btn-danger" onclick="return confirm('Reject this withdrawal? Balance will be refunded.')">
Reject
</a>
</div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/../footer.php'; ?>

321
views/apk_detail.php Normal file
View File

@ -0,0 +1,321 @@
<?php include 'header.php'; ?>
<div class="container py-3">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4 d-none d-md-block">
<ol class="breadcrumb mb-0 small">
<li class="breadcrumb-item"><a href="/" class="text-success text-decoration-none"><?php echo __('home'); ?></a></li>
<?php
$db = db_pdo();
$catName = $db->query("SELECT name FROM categories WHERE id = " . ($apk['category_id'] ?: 0))->fetchColumn();
$catSlug = $db->query("SELECT slug FROM categories WHERE id = " . ($apk['category_id'] ?: 0))->fetchColumn();
?>
<li class="breadcrumb-item"><a href="/?category=<?php echo $catSlug; ?>" class="text-success text-decoration-none"><?php echo $catName ?: 'Apps'; ?></a></li>
<li class="breadcrumb-item active text-muted" aria-current="page"><?php echo $apk['title']; ?></li>
</ol>
</nav>
<div class="row g-4">
<!-- Main Content -->
<div class="col-lg-8">
<div class="bg-white p-3 p-md-5 rounded-4 shadow-sm mb-4">
<!-- Header Section -->
<div class="d-flex align-items-start mb-4">
<?php
$icon = !empty($apk['icon_path']) ? '/'.$apk['icon_path'] : $apk['image_url'];
?>
<img src="<?php echo $icon; ?>" class="rounded-4 me-3 me-md-4 shadow-sm" width="80" height="80" alt="<?php echo $apk['title']; ?>" style="object-fit: cover; min-width: 80px;">
<div>
<h1 class="h4 fw-bold mb-1 d-flex align-items-center flex-wrap">
<?php echo $apk['title']; ?>
<span class="badge bg-light text-muted fw-normal ms-2 fs-6">v<?php echo $apk['version']; ?></span>
</h1>
<p class="text-muted small mb-3"><?php echo __('official_version_text'); ?></p>
<div class="d-flex flex-wrap gap-2 mb-0">
<span class="badge bg-success-subtle text-success border border-success-subtle px-2 py-1 fw-medium rounded">
<i class="bi bi-download me-1"></i> <?php echo number_format($apk['total_downloads']); ?>
</span>
<span class="badge bg-info-subtle text-info border border-info-subtle px-2 py-1 fw-medium rounded">
<i class="bi bi-shield-check me-1"></i> <?php echo __('verified'); ?>
</span>
</div>
</div>
</div>
<!-- Action Button -->
<div class="mb-4" id="main-download-btn-area">
<a href="/download/<?php echo $apk['slug']; ?>" target="_blank" class="btn btn-success btn-lg w-100 py-3 rounded-pill fw-bold shadow-sm mb-2">
<i class="bi bi-download me-2"></i> <?php echo __('download_now'); ?>
</a>
<p class="text-muted text-center x-small mt-2" style="font-size: 0.75rem;">
<?php echo __('agree_terms_text'); ?>
</p>
</div>
<!-- Description -->
<div class="mb-5">
<h4 class="fw-bold h5 mb-3"><?php echo __('description'); ?></h4>
<div class="text-muted small lh-lg">
<?php echo nl2br(htmlspecialchars($apk['description'])); ?>
</div>
</div>
<!-- Features & Requirements Grid -->
<div class="row g-3 mb-5">
<div class="col-md-6">
<div class="p-4 rounded-4 bg-light h-100 border-0">
<h6 class="fw-bold mb-3 d-flex align-items-center">
<span class="p-1 bg-white rounded shadow-sm me-2 d-flex align-items-center justify-content-center" style="width: 28px; height: 28px;">
<i class="bi bi-star-fill text-warning" style="font-size: 0.8rem;"></i>
</span>
<?php echo __('main_features'); ?>
</h6>
<ul class="list-unstyled mb-0 text-muted small">
<li class="mb-2 d-flex align-items-center"><i class="bi bi-check-circle-fill text-success me-2"></i> <?php echo __('feature_original'); ?></li>
<li class="mb-2 d-flex align-items-center"><i class="bi bi-check-circle-fill text-success me-2"></i> <?php echo __('feature_no_extra'); ?></li>
<li class="mb-2 d-flex align-items-center"><i class="bi bi-check-circle-fill text-success me-2"></i> <?php echo __('feature_fast'); ?></li>
<li class="d-flex align-items-center"><i class="bi bi-check-circle-fill text-success me-2"></i> <?php echo __('feature_regular'); ?></li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="p-4 rounded-4 bg-light h-100 border-0">
<h6 class="fw-bold mb-3 d-flex align-items-center">
<span class="p-1 bg-white rounded shadow-sm me-2 d-flex align-items-center justify-content-center" style="width: 28px; height: 28px;">
<i class="bi bi-gear-fill text-secondary" style="font-size: 0.8rem;"></i>
</span>
<?php echo __('system_requirements'); ?>
</h6>
<ul class="list-unstyled mb-0 text-muted small">
<li class="mb-2 d-flex align-items-center"><i class="bi bi-info-circle me-2"></i> <?php echo __('req_android'); ?></li>
<li class="mb-2 d-flex align-items-center"><i class="bi bi-memory me-2"></i> <?php echo __('req_ram'); ?></li>
<li class="mb-2 d-flex align-items-center"><i class="bi bi-hdd-network me-2"></i> <?php echo __('req_internet'); ?></li>
<li class="d-flex align-items-center"><i class="bi bi-cpu me-2"></i> <?php echo __('req_cpu'); ?></li>
</ul>
</div>
</div>
</div>
<!-- Safety Banner -->
<div class="bg-success rounded-4 p-4 text-center">
<h6 class="fw-bold text-white mb-2"><?php echo __('safe_question'); ?></h6>
<p class="text-white text-opacity-75 small mb-0">
<?php echo __('safe_answer'); ?>
</p>
</div>
</div>
<!-- Related Apps Section -->
<?php
$relatedApks = $db->query("SELECT * FROM apks WHERE category_id = " . ($apk['category_id'] ?: 0) . " AND id != " . $apk['id'] . " LIMIT 6")->fetchAll();
if ($relatedApks):
?>
<div class="mb-5">
<h4 class="fw-bold h5 mb-3 d-flex align-items-center">
<i class="bi bi-grid-fill text-success me-2"></i> Similar Apps
</h4>
<div class="row g-2">
<?php foreach ($relatedApks as $rapk): ?>
<div class="col-4 col-md-4">
<a href="/apk/<?php echo $rapk['slug']; ?>" class="text-decoration-none">
<div class="card border-0 shadow-sm rounded-4 text-center p-2 h-100 hover-lift">
<img src="<?php echo !empty($rapk['icon_path']) ? '/'.$rapk['icon_path'] : $rapk['image_url']; ?>" class="rounded-3 mx-auto mb-2 shadow-sm" width="48" height="48" style="object-fit: cover;">
<h6 class="card-title fw-bold mb-0 text-truncate small" style="font-size: 0.7rem;"><?php echo $rapk['title']; ?></h6>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<!-- Sidebar / Additional Info -->
<div class="col-lg-4">
<div class="position-sticky" style="top: 5.5rem;">
<div class="bg-white p-4 rounded-4 shadow-sm mb-4 text-center border-0 py-5">
<h5 class="fw-bold mb-3 h6"><?php echo __('share_earn'); ?></h5>
<p class="text-muted small mb-4 lh-sm"><?php echo __('share_earn_text'); ?></p>
<div class="input-group mb-3 border rounded-pill p-1">
<?php
$ref = isset($_SESSION['user_id']) ? $db->query("SELECT referral_code FROM users WHERE id = ".$_SESSION['user_id'])->fetchColumn() : '';
$shareLink = 'http://'.$_SERVER['HTTP_HOST'].'/apk/'.$apk['slug'].($ref ? '?ref='.$ref : '');
?>
<input type="text" class="form-control form-control-sm border-0 bg-transparent ps-3" id="shareLink" value="<?php echo $shareLink; ?>" readonly>
<button class="btn btn-success btn-sm rounded-pill px-3" type="button" onclick="copyShareLink()"><?php echo __('copy'); ?></button>
</div>
<?php if (!$ref): ?>
<a href="/login" class="small text-success text-decoration-none fw-medium"><?php echo __('login_to_earn'); ?></a>
<?php endif; ?>
</div>
<!-- Report / Request Section -->
<div class="bg-light p-4 rounded-4 mb-4 border-0">
<h6 class="fw-bold mb-3 d-flex align-items-center">
<i class="bi bi-flag-fill text-danger me-2"></i> Support & Issues
</h6>
<p class="text-muted small mb-3">Found a problem with this app or want to request a newer version?</p>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-danger btn-sm rounded-pill" data-bs-toggle="modal" data-bs-target="#reportModal" onclick="setReportType('Report Issue')">
<i class="bi bi-exclamation-triangle me-1"></i> Report Issue
</button>
<button type="button" class="btn btn-outline-dark btn-sm rounded-pill" data-bs-toggle="modal" data-bs-target="#reportModal" onclick="setReportType('Request Update')">
<i class="bi bi-arrow-repeat me-1"></i> Request Update
</button>
</div>
</div>
<div class="bg-dark text-white p-4 rounded-4 shadow-sm text-center border-0 d-none d-lg-block">
<div class="bg-success bg-opacity-10 rounded-circle d-inline-flex p-3 mb-3">
<i class="bi bi-trophy text-success h4 mb-0"></i>
</div>
<h5 class="fw-bold mb-3 h6"><?php echo __('referral_program'); ?></h5>
<p class="small text-white-50 mb-4 lh-sm"><?php echo __('referral_program_text'); ?></p>
<a href="/register" class="btn btn-success fw-bold w-100 rounded-pill py-2 shadow-sm"><?php echo __('get_started'); ?></a>
</div>
</div>
</div>
</div>
</div>
<!-- Report/Request Modal -->
<div class="modal fade" id="reportModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-4 overflow-hidden">
<div class="modal-header bg-dark text-white py-4 border-0">
<h5 class="modal-title fw-bold" id="reportModalLabel">Report Issue</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form id="report-form">
<input type="hidden" name="apk_name" value="<?php echo htmlspecialchars($apk['title']); ?>">
<input type="hidden" name="subject" id="report-subject" value="Report Issue">
<div class="modal-body p-4">
<div id="report-alert"></div>
<div class="mb-3">
<label class="form-label fw-bold">Your Email</label>
<input type="email" class="form-control" name="email" placeholder="email@example.com" required>
</div>
<div class="mb-0">
<label class="form-label fw-bold">Message / Details</label>
<textarea class="form-control" name="message" rows="4" placeholder="Please describe the issue or your request..." required></textarea>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light px-4 rounded-pill" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success px-4 rounded-pill" id="report-submit-btn">Send</button>
</div>
</form>
</div>
</div>
</div>
<!-- Sticky Download Bar (Mobile Only) -->
<div class="sticky-download-bar" id="sticky-download-bar">
<div class="d-flex align-items-center">
<img src="<?php echo $icon; ?>" class="rounded-2 me-3" width="40" height="40" alt="<?php echo $apk['title']; ?>">
<div class="flex-grow-1 overflow-hidden">
<h6 class="fw-bold mb-0 text-truncate" style="font-size: 0.9rem;"><?php echo $apk['title']; ?></h6>
<span class="x-small text-muted" style="font-size: 0.7rem;">v<?php echo $apk['version']; ?></span>
</div>
<a href="/download/<?php echo $apk['slug']; ?>" class="btn btn-success btn-sm rounded-pill px-3 fw-bold">
Download
</a>
</div>
</div>
<!-- Share FAB (Mobile Only) -->
<a href="#" class="share-fab" id="mobile-share-btn">
<i class="bi bi-share-fill"></i>
</a>
<script>
function copyShareLink() {
var copyText = document.getElementById("shareLink");
copyText.select();
copyText.setSelectionRange(0, 99999);
navigator.clipboard.writeText(copyText.value);
const btn = event.target;
const originalText = btn.innerText;
btn.innerText = "Copied!";
btn.classList.replace('btn-success', 'btn-dark');
setTimeout(() => {
btn.innerText = originalText;
btn.classList.replace('btn-dark', 'btn-success');
}, 2000);
}
function setReportType(type) {
document.getElementById('reportModalLabel').innerText = type;
document.getElementById('report-subject').value = type + ': <?php echo addslashes($apk['title']); ?>';
}
document.getElementById('report-form').addEventListener('submit', function(e) {
e.preventDefault();
const btn = document.getElementById('report-submit-btn');
const alertBox = document.getElementById('report-alert');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Sending...';
const formData = new FormData(this);
fetch('/api/report', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: formData
})
.then(res => res.json())
.then(data => {
btn.disabled = false;
btn.innerHTML = 'Send';
if (data.success) {
alertBox.innerHTML = '<div class="alert alert-success border-0 small">' + data.success + '</div>';
setTimeout(() => {
const modal = bootstrap.Modal.getInstance(document.getElementById('reportModal'));
modal.hide();
alertBox.innerHTML = '';
this.reset();
}, 2000);
} else {
alertBox.innerHTML = '<div class="alert alert-danger border-0 small">' + data.error + '</div>';
}
})
.catch(err => {
btn.disabled = false;
btn.innerHTML = 'Send';
alertBox.innerHTML = '<div class="alert alert-danger border-0 small">An error occurred.</div>';
});
});
// Sticky Bar Logic
window.addEventListener('scroll', function() {
const mainBtn = document.getElementById('main-download-btn-area');
const stickyBar = document.getElementById('sticky-download-bar');
if (mainBtn && stickyBar) {
const rect = mainBtn.getBoundingClientRect();
if (rect.bottom < 0) {
stickyBar.classList.add('show');
} else {
stickyBar.classList.remove('show');
}
}
});
// Native Web Share API
document.getElementById('mobile-share-btn').addEventListener('click', function(e) {
e.preventDefault();
if (navigator.share) {
navigator.share({
title: '<?php echo $apk['title']; ?>',
text: 'Check out this app on ApkNusa!',
url: '<?php echo $shareLink; ?>',
}).catch((error) => console.log('Error sharing', error));
} else {
copyShareLink();
}
});
</script>
<?php include 'footer.php'; ?>

33
views/auth/login.php Normal file
View File

@ -0,0 +1,33 @@
<?php require_once __DIR__ . '/../header.php'; ?>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow-sm border-0 rounded-4 p-4">
<h2 class="text-center mb-4"><?php echo __('login_title'); ?></h2>
<?php if (isset($error)): ?>
<div class="alert alert-danger"><?= $error ?></div>
<?php endif; ?>
<form action="/login" method="POST">
<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>
</div>
<div class="mb-3">
<label for="password" class="form-label"><?php echo __('password'); ?></label>
<input type="password" class="form-control rounded-3" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100 rounded-3 py-2"><?php echo __('login'); ?></button>
</form>
<div class="text-center mt-4">
<p class="mb-0"><?php echo __('dont_have_account'); ?> <a href="/register" class="text-decoration-none text-primary"><?php echo __('register_here'); ?></a></p>
</div>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/../footer.php'; ?>

236
views/auth/profile.php Normal file
View File

@ -0,0 +1,236 @@
<?php include __DIR__ . '/../header.php'; ?>
<div class="container py-5">
<div class="row">
<div class="col-lg-4">
<div class="card shadow border-0 rounded-4 mb-4">
<div class="card-body text-center p-5">
<div class="bg-success text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-4" style="width: 80px; height: 80px;">
<i class="fas fa-user fs-1"></i>
</div>
<h3 class="fw-bold mb-0"><?php echo $user['username']; ?></h3>
<p class="text-muted"><?php echo __('member_since'); ?> <?php echo date('M Y', strtotime($user['created_at'])); ?></p>
<hr>
<div class="row g-0">
<div class="col-6 border-end">
<h4 class="fw-bold text-success mb-0"><?php echo number_format($user['points']); ?></h4>
<small class="text-muted text-uppercase"><?php echo __('points'); ?></small>
</div>
<div class="col-6">
<h4 class="fw-bold text-primary mb-0"><?php echo $user['total_referrals']; ?></h4>
<small class="text-muted text-uppercase"><?php echo __('referrals'); ?></small>
</div>
</div>
</div>
</div>
<div class="card shadow border-0 rounded-4 mb-4">
<div class="card-body p-4 text-center bg-light">
<h6 class="text-uppercase text-muted fw-bold mb-2"><?php echo __('balance'); ?></h6>
<h2 class="fw-bold text-success mb-3" id="user-balance">Rp <?php echo number_format($user['balance'], 0, ',', '.'); ?></h2>
<button class="btn btn-success btn-lg px-5 rounded-pill" data-bs-toggle="modal" data-bs-target="#withdrawModal">
<i class="fas fa-wallet me-2"></i> <?php echo __('withdraw'); ?>
</button>
<p class="small text-muted mt-3 mb-0"><?php echo __('min_withdraw'); ?>: Rp 10.000</p>
</div>
</div>
</div>
<div class="col-lg-8">
<div id="alert-container">
<?php if (isset($success)): ?>
<div class="alert alert-success border-0 rounded-4 mb-4"><?php echo $success; ?></div>
<?php endif; ?>
<?php if (isset($error)): ?>
<div class="alert alert-danger border-0 rounded-4 mb-4"><?php echo $error; ?></div>
<?php endif; ?>
</div>
<div class="card shadow border-0 rounded-4 mb-4">
<div class="card-header bg-white py-3">
<h5 class="m-0 fw-bold"><?php echo __('referral_link'); ?></h5>
</div>
<div class="card-body p-4">
<p><?php echo __('referral_share_text'); ?></p>
<div class="input-group mb-3">
<input type="text" class="form-control bg-light" id="refLink" value="<?php echo 'http://' . $_SERVER['HTTP_HOST'] . '/?ref=' . $user['referral_code']; ?>" readonly>
<button class="btn btn-outline-success" type="button" onclick="copyText('refLink')"><?php echo __('copy_link'); ?></button>
</div>
<div class="small text-muted"><?php echo __('example_ref_link'); ?></div>
<div class="input-group">
<input type="text" class="form-control bg-light" value="<?php echo 'http://' . $_SERVER['HTTP_HOST'] . '/apk/example-slug?ref=' . $user['referral_code']; ?>" readonly>
</div>
</div>
</div>
<div class="card shadow border-0 rounded-4">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h5 class="m-0 fw-bold"><?php echo __('withdrawal_history'); ?></h5>
<span class="badge bg-light text-dark"><?php echo __('recent_activities'); ?></span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="withdrawal-table">
<thead class="bg-light">
<tr>
<th class="ps-4"><?php echo __('date'); ?></th>
<th><?php echo __('amount'); ?></th>
<th><?php echo __('method'); ?></th>
<th><?php echo __('status'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($withdrawals)): ?>
<tr id="no-history-row">
<td colspan="4" class="text-center py-5 text-muted"><?php echo __('no_history'); ?></td>
</tr>
<?php endif; ?>
<?php foreach ($withdrawals as $wd): ?>
<tr>
<td class="ps-4"><?php echo date('d M Y, H:i', strtotime($wd['created_at'])); ?></td>
<td class="fw-bold text-success">Rp <?php echo number_format($wd['amount'], 0, ',', '.'); ?></td>
<td><?php echo $wd['method']; ?></td>
<td>
<?php
$statusClass = 'secondary';
if ($wd['status'] === 'pending') $statusClass = 'warning';
if ($wd['status'] === 'approved') $statusClass = 'success';
if ($wd['status'] === 'rejected') $statusClass = 'danger';
?>
<span class="badge bg-<?php echo $statusClass; ?>">
<?php echo ucfirst($wd['status']); ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Withdraw Modal -->
<div class="modal fade" id="withdrawModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-4 overflow-hidden">
<div class="modal-header bg-success text-white py-4 border-0">
<h5 class="modal-title fw-bold"><?php echo __('request_withdrawal'); ?></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form id="withdraw-form" action="/withdraw" method="POST">
<div class="modal-body p-4">
<div id="modal-alert"></div>
<div class="mb-3">
<label class="form-label fw-bold"><?php echo __('amount_to_withdraw'); ?></label>
<div class="input-group">
<span class="input-group-text">Rp</span>
<input type="number" class="form-control" name="amount" min="10000" max="<?php echo (int)$user['balance']; ?>" step="1000" placeholder="Min 10.000" required>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold"><?php echo __('payment_method'); ?></label>
<select class="form-select" name="method" required>
<option value=""><?php echo __('select_method'); ?></option>
<option value="DANA">DANA</option>
<option value="OVO">OVO</option>
<option value="GOPAY">GoPay</option>
<option value="ShopeePay">ShopeePay</option>
<option value="BANK">Bank Transfer</option>
</select>
</div>
<div class="mb-0">
<label class="form-label fw-bold"><?php echo __('account_details'); ?></label>
<textarea class="form-control" name="details" rows="3" placeholder="<?php echo __('account_details_placeholder'); ?>" required></textarea>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light px-4 rounded-pill" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
<button type="submit" class="btn btn-success px-4 rounded-pill" id="withdraw-submit-btn"><?php echo __('submit_request'); ?></button>
</div>
</form>
</div>
</div>
</div>
<script>
function copyText(id) {
var copyText = document.getElementById(id);
copyText.select();
copyText.setSelectionRange(0, 99999);
navigator.clipboard.writeText(copyText.value);
alert("<?php echo __('ref_copy_success_js'); ?>");
}
document.getElementById('withdraw-form').addEventListener('submit', function(e) {
e.preventDefault();
const form = this;
const btn = document.getElementById('withdraw-submit-btn');
const modalAlert = document.getElementById('modal-alert');
const alertContainer = document.getElementById('alert-container');
const balanceEl = document.getElementById('user-balance');
const tableBody = document.querySelector('#withdrawal-table tbody');
const noHistoryRow = document.getElementById('no-history-row');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Processing...';
const formData = new FormData(form);
fetch('/withdraw', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(res => res.json())
.then(data => {
btn.disabled = false;
btn.innerHTML = '<?php echo __('submit_request'); ?>';
if (data.success) {
// Update balance
if (data.new_balance !== undefined) {
balanceEl.textContent = 'Rp ' + data.new_balance.toLocaleString('id-ID');
}
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('withdrawModal'));
modal.hide();
// Show success message on main page
alertContainer.innerHTML = '<div class="alert alert-success border-0 rounded-4 mb-4">' + data.success + '</div>';
// Add to table (simplified, just reload or prepend)
// For now, let's just prepend a row if we can
if (noHistoryRow) noHistoryRow.remove();
const now = new Date();
const dateStr = now.toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' }) + ', ' +
now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
const newRow = `<tr>
<td class="ps-4">${dateStr}</td>
<td class="fw-bold text-success">Rp ${parseInt(formData.get('amount')).toLocaleString('id-ID')}</td>
<td>${formData.get('method')}</td>
<td><span class="badge bg-warning">Pending</span></td>
</tr>`;
tableBody.insertAdjacentHTML('afterbegin', newRow);
form.reset();
} else {
modalAlert.innerHTML = '<div class="alert alert-danger border-0 small">' + data.error + '</div>';
}
})
.catch(err => {
btn.disabled = false;
btn.innerHTML = '<?php echo __('submit_request'); ?>';
modalAlert.innerHTML = '<div class="alert alert-danger border-0 small">An error occurred. Please try again.</div>';
});
});
</script>
<?php include __DIR__ . '/../footer.php'; ?>

48
views/auth/register.php Normal file
View File

@ -0,0 +1,48 @@
<?php require_once __DIR__ . '/../header.php'; ?>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm border-0 rounded-4 p-4">
<h2 class="text-center mb-4"><?php echo __('register_title'); ?></h2>
<?php if (isset($error)): ?>
<div class="alert alert-danger"><?= $error ?></div>
<?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>
</div>
<div class="mb-3">
<label for="password" class="form-label"><?php echo __('password'); ?></label>
<input type="password" class="form-control rounded-3" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label"><?php echo __('confirm_password'); ?></label>
<input type="password" class="form-control rounded-3" id="confirm_password" name="confirm_password" required>
</div>
<div class="mb-4">
<label for="ref_code" class="form-label"><?php echo __('referral_code_optional'); ?></label>
<input type="text" class="form-control rounded-3" id="ref_code" name="ref_code" value="<?= $ref ?? '' ?>" placeholder="e.g. abcdef12">
</div>
<button type="submit" class="btn btn-primary w-100 rounded-3 py-2"><?php echo __('create_account'); ?></button>
</form>
<div class="text-center mt-4">
<p class="mb-0"><?php echo __('already_have_account'); ?> <a href="/login" class="text-decoration-none text-primary"><?php echo __('login_here'); ?></a></p>
</div>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/../footer.php'; ?>

67
views/blog/detail.php Normal file
View File

@ -0,0 +1,67 @@
<?php include dirname(__DIR__) . '/header.php'; ?>
<article class="py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/blog">Blog</a></li>
<li class="breadcrumb-item active" aria-current="page"><?php echo htmlspecialchars($post['title']); ?></li>
</ol>
</nav>
<header class="mb-5 text-center">
<h1 class="display-4 fw-bold mb-3"><?php echo htmlspecialchars($post['title']); ?></h1>
<div class="text-muted d-flex align-items-center justify-content-center">
<span class="me-3"><i class="far fa-calendar-alt me-1"></i> <?php echo date('F d, Y', strtotime($post['created_at'])); ?></span>
<span><i class="far fa-user me-1"></i> Admin</span>
</div>
</header>
<?php if ($post['image_path']): ?>
<div class="mb-5 shadow-sm rounded-4 overflow-hidden">
<img src="/<?php echo $post['image_path']; ?>" class="img-fluid w-100" alt="<?php echo htmlspecialchars($post['title']); ?>">
</div>
<?php endif; ?>
<div class="blog-content fs-5 lh-lg text-secondary">
<?php echo nl2br($post['content']); ?>
</div>
<hr class="my-5">
<div class="d-flex justify-content-between align-items-center bg-light p-4 rounded-4">
<div class="fw-bold">Share this article:</div>
<div class="d-flex gap-2">
<a href="https://facebook.com/sharer/sharer.php?u=<?php echo urlencode("https://" . $_SERVER['HTTP_HOST'] . "/blog/" . $post['slug']); ?>" target="_blank" class="btn btn-outline-primary btn-sm rounded-circle">
<i class="fab fa-facebook-f"></i>
</a>
<a href="https://twitter.com/intent/tweet?url=<?php echo urlencode("https://" . $_SERVER['HTTP_HOST'] . "/blog/" . $post['slug']); ?>&text=<?php echo urlencode($post['title']); ?>" target="_blank" class="btn btn-outline-info btn-sm rounded-circle">
<i class="fab fa-twitter"></i>
</a>
<a href="https://api.whatsapp.com/send?text=<?php echo urlencode($post['title'] . " - https://" . $_SERVER['HTTP_HOST'] . "/blog/" . $post['slug']); ?>" target="_blank" class="btn btn-outline-success btn-sm rounded-circle">
<i class="fab fa-whatsapp"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</article>
<style>
.blog-content {
white-space: pre-wrap;
word-break: break-word;
}
.blog-content h2, .blog-content h3 {
color: #333;
margin-top: 2rem;
margin-bottom: 1rem;
font-weight: bold;
}
</style>
<?php include dirname(__DIR__) . '/footer.php'; ?>

57
views/blog/index.php Normal file
View File

@ -0,0 +1,57 @@
<?php include dirname(__DIR__) . '/header.php'; ?>
<section class="py-5 bg-light">
<div class="container">
<div class="row mb-5">
<div class="col-lg-6 mx-auto text-center">
<h1 class="display-4 fw-bold mb-3">Blog & Articles</h1>
<p class="lead text-muted">Dapatkan informasi terbaru seputar aplikasi, game, dan tips teknologi di sini.</p>
</div>
</div>
<div class="row g-4">
<?php foreach ($blogPosts as $item): ?>
<div class="col-md-6 col-lg-4">
<article class="card h-100 border-0 shadow-sm overflow-hidden hover-lift transition-all">
<?php if ($item['image_path']): ?>
<img src="/<?php echo $item['image_path']; ?>" class="card-img-top" alt="<?php echo htmlspecialchars($item['title']); ?>" style="height: 200px; object-fit: cover;">
<?php else: ?>
<div class="bg-primary bg-opacity-10 d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="fas fa-newspaper fa-3x text-primary opacity-25"></i>
</div>
<?php endif; ?>
<div class="card-body p-4">
<div class="text-muted small mb-2">
<i class="far fa-calendar-alt me-1"></i> <?php echo date('M d, Y', strtotime($item['created_at'])); ?>
</div>
<h2 class="h5 card-title fw-bold mb-3">
<a href="/blog/<?php echo $item['slug']; ?>" class="text-dark text-decoration-none stretched-link">
<?php echo htmlspecialchars($item['title']); ?>
</a>
</h2>
<p class="card-text text-muted mb-0">
<?php
$excerpt = strip_tags($item['content']);
echo strlen($excerpt) > 120 ? substr($excerpt, 0, 120) . '...' : $excerpt;
?>
</p>
</div>
</article>
</div>
<?php endforeach; ?>
<?php if (empty($blogPosts)): ?>
<div class="col-12 text-center py-5">
<div class="mb-3">
<i class="fas fa-folder-open fa-3x text-muted opacity-25"></i>
</div>
<h3 class="text-muted">Belum ada artikel yang diterbitkan.</h3>
<a href="/" class="btn btn-primary mt-3">Kembali ke Beranda</a>
</div>
<?php endif; ?>
</div>
</div>
</section>
<?php include dirname(__DIR__) . '/footer.php'; ?>

78
views/contact.php Normal file
View File

@ -0,0 +1,78 @@
<?php include 'header.php'; ?>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow border-0 rounded-4 overflow-hidden">
<div class="row g-0">
<div class="col-lg-5 bg-success p-5 text-white d-flex flex-column justify-content-center">
<h2 class="fw-bold mb-4">Get in Touch</h2>
<p class="mb-4 opacity-75">Have questions or feedback about our APKs? We'd love to hear from you. Send us a message and we'll respond as soon as possible.</p>
<div class="d-flex align-items-center mb-3">
<i class="bi bi-geo-alt-fill me-3 fs-4"></i>
<span>Jakarta, Indonesia</span>
</div>
<div class="d-flex align-items-center mb-3">
<i class="bi bi-envelope-fill me-3 fs-4"></i>
<span><?php echo get_setting('contact_email', 'support@apknusa.com'); ?></span>
</div>
<div class="mt-4 pt-4 border-top border-white border-opacity-25">
<h6 class="fw-bold mb-3">Follow Us</h6>
<div class="d-flex gap-3">
<?php if($fb = get_setting('facebook_url')): ?><a href="<?php echo $fb; ?>" class="text-white fs-5"><i class="bi bi-facebook"></i></a><?php endif; ?>
<?php if($tw = get_setting('twitter_url')): ?><a href="<?php echo $tw; ?>" class="text-white fs-5"><i class="bi bi-twitter-x"></i></a><?php endif; ?>
<?php if($ig = get_setting('instagram_url')): ?><a href="<?php echo $ig; ?>" class="text-white fs-5"><i class="bi bi-instagram"></i></a><?php endif; ?>
<?php if($tg = get_setting('telegram_url')): ?><a href="<?php echo $tg; ?>" class="text-white fs-5"><i class="bi bi-telegram"></i></a><?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-7 p-5 bg-white">
<?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success alert-dismissible fade show rounded-3 mb-4" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<?php echo $_SESSION['success']; unset($_SESSION['success']); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-danger alert-dismissible fade show rounded-3 mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?php echo $_SESSION['error']; unset($_SESSION['error']); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form action="/contact" method="POST">
<div class="mb-3">
<label class="form-label fw-bold small text-uppercase">Full Name</label>
<input type="text" name="name" class="form-control form-control-lg bg-light border-0 px-4" placeholder="Your Name" required>
</div>
<div class="mb-3">
<label class="form-label fw-bold small text-uppercase">Email Address</label>
<input type="email" name="email" class="form-control form-control-lg bg-light border-0 px-4" placeholder="name@example.com" required>
</div>
<div class="mb-3">
<label class="form-label fw-bold small text-uppercase">Subject</label>
<input type="text" name="subject" class="form-control form-control-lg bg-light border-0 px-4" placeholder="How can we help?">
</div>
<div class="mb-4">
<label class="form-label fw-bold small text-uppercase">Message</label>
<textarea name="message" class="form-control bg-light border-0 px-4 py-3" rows="4" placeholder="Your message here..." required></textarea>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success btn-lg rounded-pill py-3 fw-bold">
Send Message <i class="bi bi-send-fill ms-2"></i>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<?php include 'footer.php'; ?>

190
views/footer.php Normal file
View File

@ -0,0 +1,190 @@
</main>
<footer class="bg-white border-top py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-4">
<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="bi bi-robot"></i>
<?php endif; ?>
<?php echo htmlspecialchars(get_setting('site_name', 'ApkNusa')); ?>
</a>
<p class="text-muted mt-3 pe-lg-5">
<?php echo htmlspecialchars(get_setting('site_name', 'ApkNusa')); ?> <?php echo __('footer_about'); ?>
</p>
</div>
<div class="col-6 col-lg-2">
<h6 class="fw-bold mb-3"><?php echo __('popular'); ?></h6>
<ul class="list-unstyled">
<li><a href="/" class="text-muted text-decoration-none py-1 d-block small"><?php echo __('top_games'); ?></a></li>
<li><a href="/" class="text-muted text-decoration-none py-1 d-block small"><?php echo __('top_apps'); ?></a></li>
<li><a href="/" class="text-muted text-decoration-none py-1 d-block small"><?php echo __('new_releases'); ?></a></li>
</ul>
</div>
<div class="col-6 col-lg-2">
<h6 class="fw-bold mb-3"><?php echo __('resources'); ?></h6>
<ul class="list-unstyled">
<li><a href="/blog" class="text-muted text-decoration-none py-1 d-block small">Blog</a></li>
<li><a href="/help-center" class="text-muted text-decoration-none py-1 d-block small"><?php echo __('support_center'); ?></a></li>
<li><a href="/terms-of-service" class="text-muted text-decoration-none py-1 d-block small"><?php echo __('terms_of_service'); ?></a></li>
<li><a href="/privacy-policy" class="text-muted text-decoration-none py-1 d-block small"><?php echo __('privacy_policy'); ?></a></li>
<li><a href="/contact" class="text-muted text-decoration-none py-1 d-block small">Contact Us</a></li>
</ul>
</div>
<div class="col-lg-4">
<h6 class="fw-bold mb-3"><?php echo __('subscribe'); ?></h6>
<p class="text-muted small"><?php echo __('subscribe_text'); ?></p>
<form class="newsletter-form">
<div class="input-group">
<input type="email" class="form-control border-light-subtle newsletter-email" placeholder="<?php echo __('email_placeholder'); ?>">
<button class="btn btn-success px-3 newsletter-submit-btn" type="button"><?php echo __('subscribe'); ?></button>
</div>
<div class="newsletter-msg mt-2 small"></div>
</form>
</div>
</div>
<hr class="my-5 text-black-50 opacity-25">
<div class="row align-items-center">
<div class="col-md-6 text-center text-md-start">
<span class="text-muted small">&copy; <?php echo date('Y'); ?> <?php echo htmlspecialchars(get_setting('site_name', 'ApkNusa')); ?>. <?php echo __('all_rights_reserved'); ?></span>
</div>
<div class="col-md-6 text-center text-md-end mt-3 mt-md-0">
<div class="d-flex justify-content-center justify-content-md-end gap-3">
<?php if ($fb = get_setting('facebook_url')): ?>
<a href="<?php echo $fb; ?>" target="_blank" class="text-muted"><i class="bi bi-facebook"></i></a>
<?php endif; ?>
<?php if ($tw = get_setting('twitter_url')): ?>
<a href="<?php echo $tw; ?>" target="_blank" class="text-muted"><i class="bi bi-twitter-x"></i></a>
<?php endif; ?>
<?php if ($ig = get_setting('instagram_url')): ?>
<a href="<?php echo $ig; ?>" target="_blank" class="text-muted"><i class="bi bi-instagram"></i></a>
<?php endif; ?>
<?php if ($gh = get_setting('github_url')): ?>
<a href="<?php echo $gh; ?>" target="_blank" class="text-muted"><i class="bi bi-github"></i></a>
<?php endif; ?>
<?php if ($tg = get_setting('telegram_url')): ?>
<a href="<?php echo $tg; ?>" target="_blank" class="text-muted"><i class="bi bi-telegram"></i></a>
<?php endif; ?>
<?php if ($wa = get_setting('whatsapp_url')): ?>
<a href="<?php echo $wa; ?>" target="_blank" class="text-muted"><i class="bi bi-whatsapp"></i></a>
<?php endif; ?>
</div>
</div>
</div>
</div>
</footer>
<!-- Mobile Bottom Navigation -->
<nav class="mobile-bottom-nav">
<a href="/" class="mobile-nav-item <?php echo $_SERVER['REQUEST_URI'] == '/' ? 'active' : ''; ?>">
<i class="bi bi-house-door"></i>
<span><?php echo __('home'); ?></span>
</a>
<a href="#" class="mobile-nav-item" id="mobile-search-trigger">
<i class="bi bi-search"></i>
<span><?php echo __('search'); ?></span>
</a>
<a href="/blog" class="mobile-nav-item <?php echo strpos($_SERVER['REQUEST_URI'], '/blog') !== false ? 'active' : ''; ?>">
<i class="bi bi-newspaper"></i>
<span>Blog</span>
</a>
<a href="<?php echo isset($_SESSION['user_id']) ? '/profile' : '/login'; ?>" class="mobile-nav-item <?php echo (strpos($_SERVER['REQUEST_URI'], '/profile') !== false || strpos($_SERVER['REQUEST_URI'], '/login') !== false) ? 'active' : ''; ?>">
<i class="bi bi-person"></i>
<span><?php echo __('profile'); ?></span>
</a>
</nav>
<!-- WhatsApp FAB (Mobile) -->
<?php if ($wa = get_setting('whatsapp_url')): ?>
<a href="<?php echo $wa; ?>" target="_blank" class="whatsapp-fab">
<i class="bi bi-whatsapp fs-3"></i>
</a>
<?php endif; ?>
<!-- Back to Top -->
<a href="#" class="back-to-top" id="back-to-top">
<i class="bi bi-arrow-up"></i>
</a>
<!-- Search Overlay -->
<div class="search-overlay" id="search-overlay">
<button class="btn-close-search" id="close-search-overlay">
<i class="bi bi-x-lg"></i>
</button>
<div class="mt-5">
<h4 class="fw-bold mb-4">Search APKs</h4>
<form action="/" method="GET" id="ajax-search-form">
<div class="input-group input-group-lg border rounded-pill overflow-hidden shadow-sm">
<span class="input-group-text bg-white border-0 ps-3">
<i class="bi bi-search text-muted"></i>
</span>
<input type="text" name="search" class="form-control border-0 ps-1" placeholder="Search for apps or games..." autofocus>
<button class="btn btn-success px-4" type="submit">Go</button>
</div>
</form>
<div class="mt-5">
<h6 class="text-muted small fw-bold text-uppercase mb-3">Popular Categories</h6>
<div class="d-flex flex-wrap gap-2">
<?php
$db = db();
$popCats = $db->query("SELECT * FROM categories LIMIT 6")->fetchAll();
foreach ($popCats as $cat):
?>
<a href="/?category=<?php echo $cat['slug']; ?>" class="btn btn-light btn-sm rounded-pill px-3 ajax-cat-link" data-category="<?php echo $cat['slug']; ?>"><?php echo $cat['name']; ?></a>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<!-- AI Chat Assistant -->
<div id="ai-chat-wrapper" class="fixed-bottom p-3 d-flex flex-column align-items-end" style="z-index: 1050; pointer-events: none;">
<div id="ai-chat-window" class="card shadow-lg border-0 mb-3 d-none" style="width: 350px; max-width: 90vw; height: 450px; pointer-events: auto; border-radius: 20px;">
<div class="card-header bg-success text-white py-3 d-flex justify-content-between align-items-center" style="border-radius: 20px 20px 0 0;">
<div class="d-flex align-items-center">
<div class="bg-white rounded-circle p-1 me-2">
<i class="bi bi-robot text-success"></i>
</div>
<span class="fw-bold">ApkNusa AI</span>
</div>
<button type="button" class="btn-close btn-close-white" id="close-ai-chat"></button>
</div>
<div class="card-body overflow-auto p-3" id="ai-chat-messages" style="background: var(--subtle-bg);">
<div class="mb-3">
<div class="bg-white p-3 rounded-4 shadow-sm small" style="max-width: 85%; border-bottom-left-radius: 0 !important;">
Hello! How can I help you today?
</div>
</div>
</div>
<div class="card-footer bg-white border-0 p-3" style="border-radius: 0 0 20px 20px;">
<div class="input-group">
<input type="text" id="ai-chat-input" class="form-control border-light-subtle rounded-pill-start px-3" placeholder="Type a message...">
<button class="btn btn-success rounded-pill-end px-3" id="send-ai-chat">
<i class="bi bi-send-fill"></i>
</button>
</div>
</div>
</div>
<button class="btn btn-success shadow-lg d-flex align-items-center justify-content-center p-0 rounded-circle" id="toggle-ai-chat" style="width: 60px; height: 60px; pointer-events: auto;">
<i class="bi bi-chat-dots-fill fs-3"></i>
</button>
</div>
<style>
#ai-chat-messages::-webkit-scrollbar {
width: 4px;
}
#ai-chat-messages::-webkit-scrollbar-thumb {
background: #10B981;
border-radius: 10px;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/main.js?v=<?php echo time(); ?>"></script>
<?php echo get_setting('body_js'); ?>
</body>
</html>

178
views/header.php Normal file
View File

@ -0,0 +1,178 @@
<?php
$currentTheme = \App\Services\ThemeService::getCurrent();
$db = db();
$categories = $db->query("SELECT * FROM categories ORDER BY name ASC")->fetchAll();
$currentLang = \App\Services\LanguageService::getLang();
?>
<!DOCTYPE html>
<html lang="<?php echo $currentLang; ?>" data-theme="<?php echo $currentTheme; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $title ?? htmlspecialchars(get_setting('site_name', 'ApkNusa')); ?></title>
<meta name="description" content="<?php echo $meta_description ?? htmlspecialchars(get_setting('meta_description', __('meta_description_default'))); ?>">
<meta name="keywords" content="<?php echo $meta_keywords ?? htmlspecialchars(get_setting('meta_keywords', __('meta_keywords_default'))); ?>">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/dist/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?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">
<?php echo get_setting('head_js'); ?>
<script>
// Early theme initialization to prevent flash
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
})();
</script>
</head>
<body class="animated-bg">
<!-- Dynamic background blobs -->
<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; }
25% { background-color: #3B82F6; }
50% { background-color: #F59E0B; }
75% { background-color: #EC4899; }
100% { background-color: #10B981; }
}
@media (max-width: 991.98px) {
.mobile-theme-row {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--subtle-bg);
padding: 0.65rem 1rem;
border-radius: 12px;
border: 1px solid var(--border-color);
margin-bottom: 0.75rem;
}
.mobile-lang-row {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--subtle-bg);
padding: 0.65rem 1rem;
border-radius: 12px;
border: 1px solid var(--border-color);
}
}
</style>
<nav class="navbar navbar-expand-lg navbar-light 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 me-2"></i>
<?php endif; ?>
<?php echo htmlspecialchars(get_setting('site_name', 'ApkNusa')); ?>
</a>
<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">
<ul class="navbar-nav ms-auto align-items-lg-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="/blog">Blog</a>
</li>
<li class="nav-item dropdown px-3">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<?php echo __('categories'); ?>
</a>
<ul class="dropdown-menu shadow border-0">
<li><a class="dropdown-item category-filter" href="/" data-category=""><?php echo __('all_categories'); ?></a></li>
<?php foreach ($categories as $cat): ?>
<li><a class="dropdown-item category-filter" href="/?category=<?php echo $cat['slug']; ?>" data-category="<?php echo htmlspecialchars($cat['slug']); ?>"><?php echo htmlspecialchars($cat['name']); ?></a></li>
<?php endforeach; ?>
</ul>
</li>
<li class="nav-item dropdown d-none d-lg-block px-2">
<a class="nav-link dropdown-toggle lang-selector-btn" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-globe"></i> <?php echo $currentLang == 'id' ? 'Indonesia' : 'English'; ?>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
<li><a class="dropdown-item d-flex align-items-center" href="/lang/id"><span class="me-2">🇮🇩</span> Indonesia</a></li>
<li><a class="dropdown-item d-flex align-items-center" href="/lang/en"><span class="me-2">🇺🇸</span> English</a></li>
</ul>
</li>
<!-- Theme Toggle Desktop -->
<li class="nav-item d-none d-lg-block px-2">
<button id="theme-toggle" class="theme-toggle-btn" aria-label="Toggle theme">
<i class="fa-solid <?php echo $currentTheme === 'dark' ? 'fa-sun' : 'fa-moon'; ?>"></i>
</button>
</li>
<?php if (isset($_SESSION['user_id'])): ?>
<li class="nav-item ms-lg-3">
<a class="btn btn-success rounded-pill px-4 d-flex align-items-center w-100 w-lg-auto" href="/profile">
<i class="fas fa-user-circle me-2"></i> <?php echo __('profile'); ?>
</a>
</li>
<?php else: ?>
<li class="nav-item ms-lg-3">
<a class="btn btn-outline-secondary border-0 px-3 w-100 w-lg-auto text-start text-lg-center mb-2 mb-lg-0" href="/login"><?php echo __('login'); ?></a>
</li>
<li class="nav-item ms-lg-2">
<a class="btn btn-success rounded-pill px-4 w-100 w-lg-auto mb-2 mb-lg-0" href="/register"><?php echo __('register'); ?></a>
</li>
<?php endif; ?>
<!-- Mobile Controls -->
<li class="d-lg-none mt-3">
<div class="mobile-theme-row">
<span class="theme-status-text small fw-bold"><?php echo $currentTheme === 'dark' ? 'Dark Mode' : 'Light Mode'; ?></span>
<button id="theme-toggle-mobile" class="theme-toggle-btn" aria-label="Toggle theme">
<i class="fa-solid <?php echo $currentTheme === 'dark' ? 'fa-sun' : 'fa-moon'; ?>"></i>
</button>
</div>
<div class="mobile-lang-row">
<div class="d-flex align-items-center gap-2">
<i class="fas fa-globe text-success"></i>
<span class="small fw-bold">Language</span>
</div>
<div class="dropdown">
<button class="btn btn-link text-decoration-none p-0 dropdown-toggle small fw-bold" type="button" data-bs-toggle="dropdown" style="color: inherit;">
<?php echo $currentLang == 'id' ? 'Indonesia' : 'English'; ?>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
<li><a class="dropdown-item" href="/lang/id">🇮🇩 Indonesia</a></li>
<li><a class="dropdown-item" href="/lang/en">🇺🇸 English</a></li>
</ul>
</div>
</div>
</li>
</ul>
</div>
</div>
</nav>
<main class="min-vh-100">

71
views/help_center.php Normal file
View File

@ -0,0 +1,71 @@
<?php include 'header.php'; ?>
<section class="py-5 bg-light">
<div class="container py-lg-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/" class="text-decoration-none text-success"><?php echo __('home'); ?></a></li>
<li class="breadcrumb-item active" aria-current="page"><?php echo __('support_center'); ?></li>
</ol>
</nav>
<h1 class="fw-bold mb-4"><?php echo __('help_center_title'); ?></h1>
<div class="bg-white p-4 p-md-5 rounded-4 shadow-sm">
<div class="mb-5">
<h4 class="fw-bold mb-3"><?php echo __('faq_title'); ?></h4>
<div class="accordion accordion-flush" id="faqAccordion">
<div class="accordion-item border-bottom">
<h2 class="accordion-header">
<button class="accordion-button collapsed fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#faq1">
<?php echo __('faq_q1'); ?>
</button>
</h2>
<div id="faq1" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body text-muted">
<?php echo __('faq_a1'); ?>
</div>
</div>
</div>
<div class="accordion-item border-bottom">
<h2 class="accordion-header">
<button class="accordion-button collapsed fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#faq2">
<?php echo __('faq_q2'); ?>
</button>
</h2>
<div id="faq2" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body text-muted">
<?php echo __('faq_a2'); ?>
</div>
</div>
</div>
<div class="accordion-item border-bottom">
<h2 class="accordion-header">
<button class="accordion-button collapsed fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#faq3">
<?php echo __('faq_q3'); ?>
</button>
</h2>
<div id="faq3" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body text-muted">
<?php echo __('faq_a3'); ?>
</div>
</div>
</div>
</div>
</div>
<div>
<h4 class="fw-bold mb-3"><?php echo __('contact_us'); ?></h4>
<p class="text-muted mb-4"><?php echo __('contact_text'); ?></p>
<a href="mailto:<?php echo get_setting('contact_email', 'support@example.com'); ?>" class="btn btn-success px-4 py-2 rounded-pill">
<i class="bi bi-envelope me-2"></i> <?php echo __('send_email'); ?>
</a>
</div>
</div>
</div>
</div>
</div>
</section>
<?php include 'footer.php'; ?>

124
views/home.php Normal file
View File

@ -0,0 +1,124 @@
<?php include 'header.php'; ?>
<div class="container">
<div class="row align-items-center mb-5 mt-4 mt-lg-5 text-center text-lg-start">
<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 px-3 px-lg-0"><?php echo __('hero_subtitle'); ?></p>
<div class="d-flex gap-2 justify-content-center justify-content-lg-start">
<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 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="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>
</div>
<!-- Featured Section (Horizontal Scroll on Mobile) -->
<?php
$featuredApks = $db->query("SELECT * FROM apks WHERE is_vip = 1 LIMIT 10")->fetchAll();
if ($featuredApks):
?>
<section class="mb-5">
<h4 class="fw-bold mb-3 d-flex align-items-center">
<i class="bi bi-fire text-danger me-2"></i> Featured Apps
</h4>
<div class="d-flex overflow-auto pb-3 featured-scroll" style="scrollbar-width: none; -ms-overflow-style: none;">
<?php foreach ($featuredApks as $f): ?>
<div class="flex-shrink-0 me-3" style="width: 130px;">
<a href="/apk/<?php echo $f['slug']; ?>" class="text-decoration-none">
<div class="card border-0 shadow-sm rounded-4 text-center p-2 h-100 hover-lift">
<img src="<?php echo !empty($f['icon_path']) ? '/'.$f['icon_path'] : $f['image_url']; ?>" class="rounded-3 mx-auto mb-2 shadow-sm" width="56" height="56" style="object-fit: cover;">
<h6 class="card-title fw-bold mb-0 text-truncate small" style="font-size: 0.75rem;"><?php echo $f['title']; ?></h6>
<span class="x-small text-muted" style="font-size: 0.65rem;"><?php echo $f['version']; ?></span>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
</section>
<style>.featured-scroll::-webkit-scrollbar { display: none; }</style>
<?php endif; ?>
<section id="latest" class="mb-5">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="fw-bold mb-0 h4" id="latest-title">
<?php if (!empty($_GET['search'])): ?>
<?php echo __('search_results_for', 'Search results for'); ?>: "<?php echo htmlspecialchars($_GET['search']); ?>"
<?php else: ?>
<?php echo __('latest_apks'); ?>
<?php endif; ?>
</h2>
<div class="dropdown d-none d-md-block">
<button class="btn btn-white shadow-sm border rounded-pill dropdown-toggle btn-sm" type="button" data-bs-toggle="dropdown" id="category-dropdown-btn">
<?php echo __('categories'); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0" id="category-menu">
<li><a class="dropdown-item category-filter" href="/" data-category=""><?php echo __('all_categories'); ?></a></li>
<?php
$categories = $db->query("SELECT * FROM categories")->fetchAll();
foreach ($categories as $cat): ?>
<li><a class="dropdown-item category-filter" href="/?category=<?php echo $cat['slug']; ?>" data-category="<?php echo $cat['slug']; ?>"><?php echo $cat['name']; ?></a></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<!-- Category Chips for Mobile & Quick Filter -->
<div class="d-flex overflow-auto pb-3 mb-4 category-scroll" style="scrollbar-width: none; -ms-overflow-style: none;">
<a href="/" class="btn <?php echo !isset($_GET['category']) ? 'btn-success' : 'btn-light'; ?> rounded-pill px-4 me-2 flex-shrink-0 btn-sm shadow-sm ajax-cat-link" data-category=""><?php echo __('all'); ?></a>
<?php foreach ($categories as $cat): ?>
<?php $isActive = isset($_GET['category']) && $_GET['category'] == $cat['slug']; ?>
<a href="/?category=<?php echo $cat['slug']; ?>" class="btn <?php echo $isActive ? 'btn-success' : 'btn-light'; ?> rounded-pill px-4 me-2 flex-shrink-0 btn-sm shadow-sm ajax-cat-link" data-category="<?php echo $cat['slug']; ?>"><?php echo $cat['name']; ?></a>
<?php endforeach; ?>
</div>
<style>.category-scroll::-webkit-scrollbar { display: none; }</style>
<div id="apk-grid-container">
<?php include 'partials/apk_list.php'; ?>
</div>
</section>
<!-- Referral Banner -->
<div class="bg-dark text-white p-4 p-md-5 rounded-5 mt-5 mb-5 shadow-lg position-relative overflow-hidden">
<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 h3"><?php echo __('referral_journey_title'); ?></h2>
<p class="mb-0 text-white-50 small"><?php echo __('referral_journey_text'); ?></p>
</div>
<div class="col-lg-4 text-center text-lg-end mt-4 mt-lg-0">
<a href="/register" class="btn btn-success btn-lg px-5 rounded-pill w-100 w-lg-auto"><?php echo __('get_started'); ?></a>
</div>
</div>
</div>
<!-- Newsletter Section -->
<section class="py-5 mb-5">
<div class="card border-0 shadow-lg rounded-5 overflow-hidden">
<div class="card-body p-4 p-md-5">
<div class="row align-items-center">
<div class="col-lg-6 mb-4 mb-lg-0">
<h3 class="fw-bold mb-2">Subscribe to our Newsletter</h3>
<p class="text-muted mb-0">Get notified about the latest APKs and updates directly in your inbox.</p>
</div>
<div class="col-lg-6">
<form action="/api/newsletter/subscribe" method="POST" class="d-flex gap-2 p-1 bg-light rounded-pill border shadow-sm newsletter-form">
<input type="email" name="email" class="form-control border-0 bg-transparent px-4 py-2 newsletter-email" placeholder="Enter your email" required>
<button type="submit" class="btn btn-success rounded-pill px-4 newsletter-submit-btn">Subscribe</button>
</form>
<div class="newsletter-msg mt-3 ps-3 small"></div>
</div>
</div>
</div>
</div>
</section>
</div>
<?php include 'footer.php'; ?>

50
views/maintenance.php Normal file
View File

@ -0,0 +1,50 @@
<?php
$currentTheme = \App\Services\ThemeService::getCurrent();
?>
<!DOCTYPE html>
<html lang="en" data-theme="<?php echo $currentTheme; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Under Maintenance - <?php echo htmlspecialchars(get_setting('site_name', 'ApkNusa')); ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="/assets/css/custom.css?v=<?php echo time(); ?>">
<style>
body {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
text-align: center;
background: var(--bg-color);
color: var(--text-color);
}
.maintenance-container {
max-width: 500px;
padding: 2rem;
background: var(--card-bg);
border-radius: 24px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
border: 1px solid var(--border-color);
}
.icon-wrapper {
font-size: 5rem;
color: #10B981;
margin-bottom: 1.5rem;
}
</style>
</head>
<body>
<div class="maintenance-container">
<div class="icon-wrapper">
<i class="bi bi-tools"></i>
</div>
<h1 class="fw-bold mb-3">Under Maintenance</h1>
<p class="text-muted mb-4">We're currently performing some scheduled maintenance. We'll be back shortly. Thank you for your patience!</p>
<div class="d-flex justify-content-center gap-3">
<a href="mailto:<?php echo get_setting('contact_email'); ?>" class="btn btn-outline-success rounded-pill px-4">Contact Support</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,34 @@
<?php if (empty($apks) && !empty($_GET['search'])): ?>
<div class="text-center py-5">
<i class="bi bi-search display-1 text-muted opacity-25 mb-4"></i>
<h3 class="fw-bold"><?php echo __('no_apks_found', 'No APKs found'); ?></h3>
<p class="text-muted"><?php echo __('try_another_search', 'Try another search term or browse categories.'); ?></p>
<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: ?>
<div class="row g-2 g-md-4">
<?php foreach ($apks as $apk): ?>
<div class="col-4 col-md-4 col-lg-3">
<div class="card h-100 border-0 shadow-sm rounded-4 hover-lift">
<div class="card-body p-2 p-md-3 text-center d-flex flex-column h-100">
<?php
$icon = !empty($apk['icon_path']) ? '/'.$apk['icon_path'] : $apk['image_url'];
?>
<div class="mx-auto mb-2" style="width: 50px; height: 50px; min-height: 50px;">
<img src="<?php echo $icon; ?>" class="rounded-3 shadow-sm" width="50" height="50" alt="<?php echo $apk['title']; ?>" style="object-fit: cover; width: 50px; height: 50px;">
</div>
<h6 class="card-title fw-bold mb-1 text-truncate" style="font-size: 0.75rem;"><?php echo $apk['title']; ?></h6>
<div class="mb-2">
<span class="badge bg-light text-muted fw-normal x-small" style="font-size: 0.65rem;">v<?php echo $apk['version']; ?></span>
</div>
<div class="mt-auto">
<a href="/apk/<?php echo $apk['slug']; ?>" class="btn btn-success rounded-pill py-1 btn-sm fw-medium w-100" style="font-size: 0.7rem;"><?php echo __('details'); ?></a>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>

23
views/privacy_policy.php Normal file
View File

@ -0,0 +1,23 @@
<?php include 'header.php'; ?>
<section class="py-5 bg-light">
<div class="container py-lg-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/" class="text-decoration-none text-success"><?php echo __('home'); ?></a></li>
<li class="breadcrumb-item active" aria-current="page"><?php echo __('privacy_policy'); ?></li>
</ol>
</nav>
<h1 class="fw-bold mb-4"><?php echo __('privacy_policy_title'); ?></h1>
<div class="bg-white p-4 p-md-5 rounded-4 shadow-sm content-text">
<?php echo __('privacy_policy_content'); ?>
</div>
</div>
</div>
</div>
</section>
<?php include 'footer.php'; ?>

View File

@ -0,0 +1,23 @@
<?php include 'header.php'; ?>
<section class="py-5 bg-light">
<div class="container py-lg-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/" class="text-decoration-none text-success"><?php echo __('home'); ?></a></li>
<li class="breadcrumb-item active" aria-current="page"><?php echo __('terms_of_service'); ?></li>
</ol>
</nav>
<h1 class="fw-bold mb-4"><?php echo __('terms_of_service_title'); ?></h1>
<div class="bg-white p-4 p-md-5 rounded-4 shadow-sm content-text">
<?php echo __('terms_of_service_content'); ?>
</div>
</div>
</div>
</div>
</section>
<?php include 'footer.php'; ?>