Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
10
.env
@ -1,10 +0,0 @@
|
||||
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-...
|
||||
@ -1,9 +0,0 @@
|
||||
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]
|
||||
@ -1,24 +1,58 @@
|
||||
<?php
|
||||
// LocalAIApi — proxy client, now with direct OpenAI support for Shared Hosting.
|
||||
// LocalAIApi — proxy client for the Responses API.
|
||||
// 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
|
||||
{
|
||||
/** @var array<string,mixed>|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
|
||||
{
|
||||
// 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();
|
||||
$payload = $params;
|
||||
|
||||
if (empty($payload['input']) || !is_array($payload['input'])) {
|
||||
return ['success' => false, 'error' => 'input_missing', 'message' => 'Parameter "input" is required.'];
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'input_missing',
|
||||
'message' => 'Parameter "input" is required and must be an array.',
|
||||
];
|
||||
}
|
||||
|
||||
if (!isset($payload['model']) || $payload['model'] === '') {
|
||||
@ -26,132 +60,434 @@ class LocalAIApi
|
||||
}
|
||||
|
||||
$initial = self::request($options['path'] ?? null, $payload, $options);
|
||||
if (empty($initial['success'])) return $initial;
|
||||
if (empty($initial['success'])) {
|
||||
return $initial;
|
||||
}
|
||||
|
||||
// Async flow: if backend returns ai_request_id, poll status until ready
|
||||
$data = $initial['data'] ?? null;
|
||||
if (is_array($data) && isset($data['ai_request_id'])) {
|
||||
return self::awaitResponse($data['ai_request_id'], $options);
|
||||
$aiRequestId = $data['ai_request_id'];
|
||||
$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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
$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'] ?? []
|
||||
]
|
||||
];
|
||||
return self::createResponse($params, $options);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
$cfg = self::config();
|
||||
|
||||
$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;
|
||||
$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']);
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 30;
|
||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
||||
if ($timeout <= 0) {
|
||||
$timeout = 30;
|
||||
}
|
||||
|
||||
$headers = ['Content-Type: application/json', 'Accept: application/json', ($cfg['project_header'] ?? 'Project-UUID') . ': ' . $projectUuid];
|
||||
$payload['project_uuid'] = $projectUuid;
|
||||
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
||||
$verifyTls = array_key_exists('verify_tls', $options)
|
||||
? (bool) $options['verify_tls']
|
||||
: $baseVerifyTls;
|
||||
|
||||
return self::sendCurl($url, 'POST', json_encode($payload), $headers, $timeout, true);
|
||||
$projectHeader = $cfg['project_header'];
|
||||
|
||||
$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
|
||||
{
|
||||
$deadline = time() + 300;
|
||||
while (time() < $deadline) {
|
||||
$statusResp = self::fetchStatus($aiRequestId);
|
||||
if (!empty($statusResp['success'])) {
|
||||
$data = $statusResp['data'];
|
||||
if (($data['status'] ?? '') === 'success') return ['success' => true, 'data' => $data['response'] ?? $data];
|
||||
if (($data['status'] ?? '') === 'failed') return ['success' => false, 'error' => 'AI request failed'];
|
||||
}
|
||||
sleep(5);
|
||||
$cfg = self::config();
|
||||
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds
|
||||
$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'])) {
|
||||
$data = $statusResp['data'] ?? [];
|
||||
if (is_array($data)) {
|
||||
$statusValue = $data['status'] ?? null;
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
$url = self::buildUrl('/ai-request/' . rawurlencode((string)$aiRequestId) . '/status', $cfg['base_url']);
|
||||
$headers = [($cfg['project_header'] ?? 'Project-UUID') . ': ' . $cfg['project_uuid']];
|
||||
return self::sendCurl($url, 'GET', null, $headers, 30, true);
|
||||
$projectUuid = $cfg['project_uuid'];
|
||||
if (empty($projectUuid)) {
|
||||
return [
|
||||
'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
|
||||
{
|
||||
$payload = $response['data'] ?? $response;
|
||||
if (!empty($payload['choices'][0]['message']['content'])) return (string) $payload['choices'][0]['message']['content'];
|
||||
if (!is_array($payload)) {
|
||||
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 '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
if (self::$configCache === null) self::$configCache = require __DIR__ . '/config.php';
|
||||
if (self::$configCache === null) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an absolute URL from base_url and a path.
|
||||
*/
|
||||
private static function buildUrl(string $path, string $baseUrl): string
|
||||
{
|
||||
return str_starts_with($path, 'http') ? $path : $baseUrl . '/' . ltrim($path, '/');
|
||||
$trimmed = trim($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
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_SSL_VERIFYPEER => $verifyTls,
|
||||
]);
|
||||
if ($method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
if (!function_exists('curl_init')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'curl_missing',
|
||||
'message' => 'PHP cURL extension is missing. Install or enable it on the VM.',
|
||||
];
|
||||
}
|
||||
$resp = curl_exec($ch);
|
||||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0);
|
||||
curl_setopt($ch, CURLOPT_FAILONERROR, false);
|
||||
|
||||
$upper = strtoupper($method);
|
||||
if ($upper === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? '');
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_HTTPGET, true);
|
||||
}
|
||||
|
||||
$responseBody = curl_exec($ch);
|
||||
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);
|
||||
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');
|
||||
}
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
<?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.']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,430 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@ -1,260 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
<?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]);
|
||||
}
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@ -1,147 +0,0 @@
|
||||
<?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')
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
<?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.')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
<?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>';
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
<?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";
|
||||
}
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
<?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';
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
<?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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class ThemeService {
|
||||
public static function getCurrent() {
|
||||
return $_COOKIE['theme'] ?? 'light';
|
||||
}
|
||||
}
|
||||
@ -1,595 +1,302 @@
|
||||
: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 {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg-color);
|
||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
background-attachment: fixed;
|
||||
color: var(--text-color);
|
||||
position: relative;
|
||||
animation: gradient 15s ease infinite;
|
||||
color: #212529;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
/* Ensure headings inside light-text containers are visible */
|
||||
.text-white h1, .text-white h2, .text-white h3, .text-white h4, .text-white h5, .text-white h6,
|
||||
.bg-dark h1, .bg-dark h2, .bg-dark h3, .bg-dark h4, .bg-dark h5, .bg-dark h6 {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
body.animated-bg {
|
||||
background: linear-gradient(-45deg, var(--bg-color), var(--subtle-bg), var(--bg-color), var(--subtle-bg));
|
||||
animation: gradientBG 15s ease infinite;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.floating-animation {
|
||||
animation: floating 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes floating {
|
||||
0% { transform: translate(0, 0px); }
|
||||
50% { transform: translate(0, 15px); }
|
||||
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;
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--subtle-bg);
|
||||
color: var(--text-color);
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.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);
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .navbar-collapse {
|
||||
box-shadow: 0 1.5rem 3rem rgba(0,0,0,0.4);
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
.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;
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 85vh;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
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;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
color: var(--muted-text);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.mobile-nav-item i {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 4px;
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.mobile-nav-item.active {
|
||||
color: var(--accent-color);
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.mobile-nav-item.active i {
|
||||
transform: translateY(-2px);
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Sticky Download Button for Mobile */
|
||||
.sticky-download-bar {
|
||||
position: fixed;
|
||||
bottom: 65px; /* Above mobile-bottom-nav */
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--card-bg);
|
||||
::-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;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
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;
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sticky-download-bar.show {
|
||||
transform: translateY(0);
|
||||
.chat-input-area input:focus {
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
|
||||
}
|
||||
|
||||
/* Search Overlay */
|
||||
.search-overlay {
|
||||
.chat-input-area button {
|
||||
background: #212529;
|
||||
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 {
|
||||
background: #000;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Background Animations */
|
||||
.bg-animations {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-color);
|
||||
z-index: 1100;
|
||||
display: none;
|
||||
padding: 2rem 1.5rem;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-overlay.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.search-overlay .btn-close-search {
|
||||
.blob {
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-color);
|
||||
background: none;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
.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;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.share-fab:hover {
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* WhatsApp FAB */
|
||||
.whatsapp-fab {
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
left: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #25D366;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 15px rgba(37, 211, 102, 0.4);
|
||||
z-index: 1020;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.whatsapp-fab:hover {
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Back to Top */
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: 140px;
|
||||
right: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--card-bg);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
z-index: 1010;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.back-to-top.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Category Chips Scroll */
|
||||
.category-scroll .btn {
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.category-scroll .btn-light {
|
||||
background: var(--subtle-bg);
|
||||
color: var(--muted-text);
|
||||
}
|
||||
|
||||
.category-scroll .btn-light:hover {
|
||||
background: var(--border-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Featured Scroll Hover */
|
||||
.featured-scroll .card:hover {
|
||||
background: var(--subtle-bg);
|
||||
}
|
||||
|
||||
/* Hide desktop elements on mobile and vice-versa */
|
||||
@media (min-width: 992px) {
|
||||
.mobile-bottom-nav, .sticky-download-bar, .search-overlay, .share-fab, .whatsapp-fab {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
body {
|
||||
padding-bottom: 70px; /* Space for bottom nav */
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Improve card layout on mobile for 3 columns */
|
||||
.card-body {
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.75rem !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0.2rem 0.4rem !important;
|
||||
}
|
||||
|
||||
#ai-chat-wrapper {
|
||||
bottom: 75px !important;
|
||||
}
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 49 KiB |
@ -1,326 +1,39 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Basic interaction for toasts
|
||||
try {
|
||||
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 chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
const html = document.documentElement;
|
||||
|
||||
const updateIcons = (theme) => {
|
||||
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';
|
||||
}
|
||||
});
|
||||
|
||||
const textLabels = document.querySelectorAll('.theme-status-text');
|
||||
textLabels.forEach(label => {
|
||||
label.textContent = theme === 'dark' ? 'Dark Mode' : 'Light Mode';
|
||||
});
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
|
||||
// Theme Toggle Logic
|
||||
const initThemeToggle = (btnId) => {
|
||||
const themeToggle = document.getElementById(btnId);
|
||||
if (!themeToggle) return;
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
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);
|
||||
});
|
||||
};
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
// Unified AJAX Content Loader
|
||||
const loadContent = (url, updateUrl = true) => {
|
||||
const gridContainer = document.getElementById('apk-grid-container');
|
||||
if (!gridContainer) return;
|
||||
|
||||
gridContainer.style.opacity = '0.5';
|
||||
gridContainer.style.pointerEvents = 'none';
|
||||
|
||||
fetch(url, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
gridContainer.innerHTML = data;
|
||||
gridContainer.style.opacity = '1';
|
||||
gridContainer.style.pointerEvents = 'all';
|
||||
|
||||
if (updateUrl) {
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
// Scroll to grid top on mobile if needed
|
||||
if (window.innerWidth < 992) {
|
||||
const latestSection = document.getElementById('latest');
|
||||
if (latestSection) {
|
||||
window.scrollTo({
|
||||
top: latestSection.offsetTop - 100,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Fetch error:', err);
|
||||
gridContainer.style.opacity = '1';
|
||||
gridContainer.style.pointerEvents = 'all';
|
||||
});
|
||||
};
|
||||
|
||||
// AJAX Category Filtering
|
||||
const initCategoryAjax = () => {
|
||||
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', {
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
if (data.success) {
|
||||
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>';
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
});
|
||||
|
||||
// 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' : '');
|
||||
const data = await response.json();
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = (isUser ? 'bg-success text-white' : 'bg-white') + ' p-3 rounded-4 shadow-sm small';
|
||||
content.style.maxWidth = '85%';
|
||||
content.style.borderBottomRightRadius = isUser ? '0' : 'inherit';
|
||||
content.style.borderBottomLeftRadius = isUser ? 'inherit' : '0';
|
||||
content.textContent = message;
|
||||
|
||||
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);
|
||||
});
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
}
|
||||
|
||||
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.');
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 348 KiB |
|
Before Width: | Height: | Size: 619 KiB |
|
Before Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 536 KiB |
|
Before Width: | Height: | Size: 518 KiB |
|
Before Width: | Height: | Size: 381 KiB |
|
Before Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 730 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
@ -1,5 +0,0 @@
|
||||
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', '');
|
||||
@ -1,40 +0,0 @@
|
||||
-- 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)
|
||||
);
|
||||
@ -1,18 +0,0 @@
|
||||
-- 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', '');
|
||||
@ -1,16 +0,0 @@
|
||||
-- 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`)
|
||||
);
|
||||
@ -1,4 +0,0 @@
|
||||
-- Add Telegram and WhatsApp Social Media Settings
|
||||
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES
|
||||
('telegram_url', ''),
|
||||
('whatsapp_url', '');
|
||||
@ -1,5 +0,0 @@
|
||||
Count: 3
|
||||
Count: 3
|
||||
Count: 3
|
||||
Count: 3
|
||||
Count: 3
|
||||
@ -1,46 +0,0 @@
|
||||
<?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
@ -1,108 +0,0 @@
|
||||
-- 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
@ -1,127 +1,150 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
// Autoloader
|
||||
spl_autoload_register(function ($class) {
|
||||
$prefix = 'App\\';
|
||||
$base_dir = __DIR__ . '/app/';
|
||||
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
return;
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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);
|
||||
}
|
||||
|
||||
$relative_class = substr($class, $len);
|
||||
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
});
|
||||
|
||||
require_once 'app/Helpers/functions.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
session_start();
|
||||
|
||||
// Initialize Language Service
|
||||
\App\Services\LanguageService::init();
|
||||
|
||||
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;
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
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>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
$router = new Router();
|
||||
$router->post('/api/newsletter/subscribe', 'NewsletterController@subscribe');
|
||||
$router->post('/api/report', 'ContactController@ajaxReport');
|
||||
$router->post('/api/ai/chat', 'AIController@chat');
|
||||
|
||||
// Sitemap
|
||||
$router->get('/sitemap.xml', 'SitemapController@index');
|
||||
|
||||
// Language Switch
|
||||
$router->get('/lang/:code', function($params) {
|
||||
$code = $params['code'];
|
||||
\App\Services\LanguageService::setLang($code);
|
||||
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/'));
|
||||
exit;
|
||||
});
|
||||
|
||||
// Home & APKs
|
||||
$router->get('/', 'HomeController@index');
|
||||
$router->get('/apk/:slug', 'HomeController@apkDetail');
|
||||
$router->get('/download/:slug', 'HomeController@download');
|
||||
|
||||
// Blog
|
||||
$router->get('/blog', 'BlogController@index');
|
||||
$router->get('/blog/:slug', 'BlogController@detail');
|
||||
|
||||
// Static Pages
|
||||
$router->get('/contact', 'ContactController@index');
|
||||
$router->post('/contact', 'ContactController@submit');
|
||||
$router->get('/help-center', 'HomeController@helpCenter');
|
||||
$router->get('/privacy-policy', 'HomeController@privacyPolicy');
|
||||
$router->get('/terms-of-service', 'HomeController@termsOfService');
|
||||
|
||||
// Auth
|
||||
$router->get('/login', 'AuthController@loginForm');
|
||||
$router->post('/login', 'AuthController@login');
|
||||
$router->get('/register', 'AuthController@registerForm');
|
||||
$router->post('/register', 'AuthController@register');
|
||||
$router->get('/logout', 'AuthController@logout');
|
||||
$router->get('/profile', 'AuthController@profile');
|
||||
$router->post('/withdraw', 'AuthController@requestWithdrawal');
|
||||
|
||||
// Admin Auth
|
||||
$router->get('/admin/login', 'AdminController@loginForm');
|
||||
$router->post('/admin/login', 'AdminController@login');
|
||||
$router->get('/admin/logout', 'AdminController@logout');
|
||||
|
||||
// Admin Dashboard
|
||||
$router->get('/admin/dashboard', 'AdminController@dashboard');
|
||||
|
||||
// Admin Settings
|
||||
$router->get('/admin/settings', 'AdminController@settingsForm');
|
||||
$router->post('/admin/settings', 'AdminController@saveSettings');
|
||||
|
||||
// Admin Users
|
||||
$router->get('/admin/users', 'AdminController@users');
|
||||
$router->post('/admin/users/toggle-ban/:id', 'AdminController@toggleBan');
|
||||
|
||||
// Admin APKs
|
||||
$router->get('/admin/apks', 'AdminController@apks');
|
||||
$router->get('/admin/apks/mass-upload', 'AdminController@massUploadForm');
|
||||
$router->post('/admin/apks/mass-upload', 'AdminController@massUpload');
|
||||
$router->get('/admin/apks/add', 'AdminController@addApkForm');
|
||||
$router->post('/admin/apks/add', 'AdminController@addApk');
|
||||
$router->get('/admin/apks/edit/:id', 'AdminController@editApkForm');
|
||||
$router->post('/admin/apks/edit/:id', 'AdminController@editApk');
|
||||
$router->get('/admin/apks/delete/:id', 'AdminController@deleteApk');
|
||||
$router->post('/admin/apks/reorder', 'AdminController@updateOrder');
|
||||
|
||||
// Admin Posts (Blog)
|
||||
$router->get('/admin/posts', 'AdminController@posts');
|
||||
$router->get('/admin/posts/add', 'AdminController@addPostForm');
|
||||
$router->post('/admin/posts/add', 'AdminController@addPost');
|
||||
$router->get('/admin/posts/edit/:id', 'AdminController@editPostForm');
|
||||
$router->post('/admin/posts/edit/:id', 'AdminController@editPost');
|
||||
$router->get('/admin/posts/delete/:id', 'AdminController@deletePost');
|
||||
|
||||
// Admin Categories
|
||||
$router->get('/admin/categories', 'AdminController@categories');
|
||||
$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();
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
174
install.php
@ -1,174 +0,0 @@
|
||||
<?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
@ -1,152 +0,0 @@
|
||||
<?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
@ -1,152 +0,0 @@
|
||||
<?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.',
|
||||
);
|
||||
@ -1,39 +1,48 @@
|
||||
<?php
|
||||
// Mail configuration sourced from environment variables.
|
||||
// Updated to prioritize local .env in workspace root.
|
||||
// No secrets are stored here; the file just maps env -> config array for MailService.
|
||||
|
||||
function env_val(string $key, $default = null) {
|
||||
$v = getenv($key);
|
||||
return ($v === false || $v === null || $v === '') ? $default : $v;
|
||||
}
|
||||
|
||||
// Loads .env files (either executor/.env or local .env)
|
||||
function load_env(): void {
|
||||
$paths = [__DIR__ . '/../../.env', __DIR__ . '/../.env']; // executor/.env and workspace/.env
|
||||
foreach ($paths as $envPath) {
|
||||
$envPath = realpath($envPath);
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
if ($line[0] === '#' || trim($line) === '') continue;
|
||||
if (!str_contains($line, '=')) continue;
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$v = trim($v, "' ");
|
||||
// Set env if not set
|
||||
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
|
||||
putenv("{$k}={$v}");
|
||||
}
|
||||
// Fallback: if critical vars are missing from process env, try to parse executor/.env
|
||||
// This helps in web/Apache contexts where .env is not exported.
|
||||
// Supports simple KEY=VALUE lines; ignores quotes and comments.
|
||||
function load_dotenv_if_needed(array $keys): void {
|
||||
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
|
||||
if (empty($missing)) return;
|
||||
static $loaded = false;
|
||||
if ($loaded) return;
|
||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
if ($line[0] === '#' || trim($line) === '') continue;
|
||||
if (!str_contains($line, '=')) continue;
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
// 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_env();
|
||||
load_dotenv_if_needed([
|
||||
'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');
|
||||
$smtp_host = env_val('SMTP_HOST');
|
||||
$smtp_port = (int) env_val('SMTP_PORT', 587);
|
||||
$smtp_secure = env_val('SMTP_SECURE', 'tls');
|
||||
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
|
||||
$smtp_user = env_val('SMTP_USER');
|
||||
$smtp_pass = env_val('SMTP_PASS');
|
||||
|
||||
@ -41,14 +50,27 @@ $from_email = env_val('MAIL_FROM', 'no-reply@localhost');
|
||||
$from_name = env_val('MAIL_FROM_NAME', 'App');
|
||||
$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 [
|
||||
'transport' => $transport,
|
||||
|
||||
// SMTP
|
||||
'smtp_host' => $smtp_host,
|
||||
'smtp_port' => $smtp_port,
|
||||
'smtp_secure' => $smtp_secure,
|
||||
'smtp_user' => $smtp_user,
|
||||
'smtp_pass' => $smtp_pass,
|
||||
|
||||
// From / Reply-To
|
||||
'from_email' => $from_email,
|
||||
'from_name' => $from_name,
|
||||
'reply_to' => $reply_to,
|
||||
];
|
||||
|
||||
// DKIM (optional)
|
||||
'dkim_domain' => $dkim_domain,
|
||||
'dkim_selector' => $dkim_selector,
|
||||
'dkim_private_key_path' => $dkim_private_key_path,
|
||||
];
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
<?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";
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,98 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,137 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,96 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,60 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,213 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,73 +0,0 @@
|
||||
</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 © <?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>
|
||||
·
|
||||
<a href="#" class="text-muted text-decoration-none">Terms & 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>
|
||||
@ -1,257 +0,0 @@
|
||||
<?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">
|
||||
@ -1,53 +0,0 @@
|
||||
<!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>
|
||||
@ -1,52 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,68 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,146 +0,0 @@
|
||||
<?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 </head>.</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 </body>.</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'; ?>
|
||||
@ -1,72 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,72 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,321 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,33 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,236 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,48 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,67 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,57 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,78 +0,0 @@
|
||||
<?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
@ -1,190 +0,0 @@
|
||||
</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">© <?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
@ -1,178 +0,0 @@
|
||||
<?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">
|
||||
@ -1,71 +0,0 @@
|
||||
<?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
@ -1,124 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,50 +0,0 @@
|
||||
<?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>
|
||||
@ -1,34 +0,0 @@
|
||||
<?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; ?>
|
||||
@ -1,23 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,23 +0,0 @@
|
||||
<?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'; ?>
|
||||