mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 05:25:42 +07:00
Initial Release v1.0.0: Full feature set with Docker automation, Nginx/Alpine stack
This commit is contained in:
54
app/Helpers/EncryptionHelper.php
Normal file
54
app/Helpers/EncryptionHelper.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class EncryptionHelper {
|
||||
|
||||
public static function encrypt($text) {
|
||||
if (empty($text)) return '';
|
||||
|
||||
$key = \App\Config\SiteConfig::getSecretKey();
|
||||
|
||||
// Simple OpenSSL encryption
|
||||
$iv_length = openssl_cipher_iv_length('aes-256-cbc');
|
||||
$iv = openssl_random_pseudo_bytes($iv_length);
|
||||
$encrypted = openssl_encrypt($text, 'aes-256-cbc', $key, 0, $iv);
|
||||
|
||||
return base64_encode($encrypted . '::' . $iv);
|
||||
}
|
||||
|
||||
public static function decrypt($text) {
|
||||
if (empty($text)) return '';
|
||||
|
||||
$key = \App\Config\SiteConfig::getSecretKey();
|
||||
|
||||
try {
|
||||
$decoded = base64_decode($text, true);
|
||||
if ($decoded === false) return $text; // Not valid base64
|
||||
|
||||
$parts = explode('::', $decoded, 2);
|
||||
if (count($parts) !== 2) {
|
||||
return $text; // Not our encrypted format, likely legacy/plain
|
||||
}
|
||||
|
||||
list($encrypted_data, $iv) = $parts;
|
||||
return openssl_decrypt($encrypted_data, 'aes-256-cbc', $key, 0, $iv);
|
||||
} catch (\Exception $e) {
|
||||
return $text; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
public static function formatBytes($bytes, $precision = 2) {
|
||||
$units = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
// Uncomment one of the following alternatives
|
||||
$bytes /= pow(1024, $pow);
|
||||
// $bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
55
app/Helpers/ErrorHelper.php
Normal file
55
app/Helpers/ErrorHelper.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class ErrorHelper {
|
||||
|
||||
public static function show($code = 404, $message = 'Page Not Found', $description = null) {
|
||||
http_response_code($code);
|
||||
|
||||
// Provide default descriptions for common codes
|
||||
if ($description === null) {
|
||||
switch ($code) {
|
||||
case 403:
|
||||
$description = "You do not have permission to access this resource.";
|
||||
break;
|
||||
case 500:
|
||||
$description = "Something went wrong on our end. Please try again later.";
|
||||
break;
|
||||
case 503:
|
||||
$description = "Service Unavailable. The server is currently unable to handle the request due to maintenance or overload.";
|
||||
break;
|
||||
case 404:
|
||||
default:
|
||||
$description = "The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Variables extracted in view
|
||||
$errorCode = $code;
|
||||
$errorMessage = $message;
|
||||
$errorDescription = $description;
|
||||
|
||||
// Ensure strictly NO output before this if keeping clean, but we are in view mode.
|
||||
// Clean buffer if active to remove partial content
|
||||
if (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
require ROOT . '/app/Views/errors/default.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function showException($exception) {
|
||||
http_response_code(500);
|
||||
|
||||
// Clean output buffer to ensure clean error page
|
||||
if (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
require ROOT . '/app/Views/errors/development.php';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
61
app/Helpers/FlashHelper.php
Normal file
61
app/Helpers/FlashHelper.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class FlashHelper {
|
||||
const SESSION_KEY = 'flash_notification';
|
||||
|
||||
/**
|
||||
* Set a flash message.
|
||||
*
|
||||
* @param string $type Notification type: 'success', 'error', 'warning', 'info', 'question'
|
||||
* @param string $title Title of the notification (or i18n key)
|
||||
* @param string $message (Optional) Body text of the notification (or i18n key)
|
||||
* @param array $params (Optional) Parameters for translation interpolation
|
||||
* @param bool $isTranslated Whether to treat title and message as translation keys
|
||||
*/
|
||||
public static function set($type, $title, $message = null, $params = [], $isTranslated = false) {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$_SESSION[self::SESSION_KEY] = [
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'params' => $params,
|
||||
'isTranslated' => $isTranslated
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a flash message exists.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function has() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
return isset($_SESSION[self::SESSION_KEY]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the flash message and clear it from session.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (self::has()) {
|
||||
$notification = $_SESSION[self::SESSION_KEY];
|
||||
unset($_SESSION[self::SESSION_KEY]);
|
||||
return $notification;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
194
app/Helpers/FormatHelper.php
Normal file
194
app/Helpers/FormatHelper.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class FormatHelper
|
||||
{
|
||||
/**
|
||||
* Convert MikroTik duration string to human readable format.
|
||||
* Example: "3w1d8h56m19s" -> "3 Weeks 1 Day 8 Hours 56 Minutes 19 Seconds"
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function elapsedTime($string)
|
||||
{
|
||||
if (empty($string)) return '-';
|
||||
|
||||
// Mikrotik formats:
|
||||
// 1. "3w1d8h56m19s" (Full)
|
||||
// 2. "00:05:00" (Simple H:i:s)
|
||||
// 3. "1d 05:00:00" (Hybrid)
|
||||
// 4. "sep/02/2023 10:00:00" (Absolute date, rarely used for uptime but useful to catch)
|
||||
|
||||
// Maps Mikrotik abbreviations to Human terms (Plural handled in logic)
|
||||
$maps = [
|
||||
'w' => 'Week',
|
||||
'd' => 'Day',
|
||||
'h' => 'Hour',
|
||||
'm' => 'Minute',
|
||||
's' => 'Second'
|
||||
];
|
||||
|
||||
// Result container
|
||||
$parts = [];
|
||||
|
||||
// Check for simple colon format (H:i:s)
|
||||
if (strpos($string, ':') !== false && strpos($string, 'w') === false && strpos($string, 'd') === false) {
|
||||
return $string; // Return as is or parse H:i:s if needed
|
||||
}
|
||||
|
||||
// Parse regex for w, d, h, m, s
|
||||
//preg_match_all('/(\d+)([wdhms])/', $string, $matches, PREG_SET_ORDER);
|
||||
|
||||
// Manual parsing to handle mixed cases more robustly or just regex
|
||||
foreach ($maps as $key => $label) {
|
||||
if (preg_match('/(\d+)'.$key.'/', $string, $match)) {
|
||||
$value = intval($match[1]);
|
||||
if ($value > 0) {
|
||||
$parts[] = $value . ' ' . $label . ($value > 1 ? 's' : '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no matches found, straightforward return (maybe it's raw seconds or weird format)
|
||||
if (empty($parts)) {
|
||||
if ($string === '0s' || $string === '00:00:00') return '-';
|
||||
return $string;
|
||||
}
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize each word (Title Case)
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function capitalize($string)
|
||||
{
|
||||
return ucwords(strtolower($string));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Currency
|
||||
* @param int|float $number
|
||||
* @param string $prefix
|
||||
* @return string
|
||||
*/
|
||||
public static function formatCurrency($number, $prefix = '')
|
||||
{
|
||||
return $prefix . ' ' . number_format($number, 0, ',', '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Bytes to KB, MB, GB
|
||||
* @param int $bytes
|
||||
* @param int $precision
|
||||
* @return string
|
||||
*/
|
||||
public static function formatBytes($bytes, $precision = 2)
|
||||
{
|
||||
if ($bytes <= 0) return '-';
|
||||
|
||||
$base = log($bytes, 1024);
|
||||
$suffixes = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
return round(pow(1024, $base - floor($base)), $precision) . ' ' . $suffixes[floor($base)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Date
|
||||
* @param string $dateStr
|
||||
* @param string $format
|
||||
* @return string
|
||||
*/
|
||||
public static function formatDate($dateStr, $format = 'd M Y H:i')
|
||||
{
|
||||
if(empty($dateStr)) return '-';
|
||||
// Handle Mikrotik default date formats if needed, usually they are readable
|
||||
// e.g. "jan/02/1970 00:00:00"
|
||||
$time = strtotime($dateStr);
|
||||
if(!$time) return $dateStr;
|
||||
return date($format, $time);
|
||||
}
|
||||
/**
|
||||
* Convert Seconds to Human Readable format
|
||||
* @param int $seconds
|
||||
* @return string
|
||||
*/
|
||||
public static function formatSeconds($seconds) {
|
||||
if ($seconds <= 0) return '0s';
|
||||
|
||||
$w = floor($seconds / 604800);
|
||||
$d = floor(($seconds % 604800) / 86400);
|
||||
$h = floor(($seconds % 86400) / 3600);
|
||||
$m = floor(($seconds % 3600) / 60);
|
||||
$s = $seconds % 60;
|
||||
|
||||
$parts = [];
|
||||
if ($w > 0) $parts[] = $w . 'w';
|
||||
if ($d > 0) $parts[] = $d . 'd';
|
||||
if ($h > 0) $parts[] = $h . 'h';
|
||||
if ($m > 0) $parts[] = $m . 'm';
|
||||
if ($s > 0 || empty($parts)) $parts[] = $s . 's';
|
||||
|
||||
return implode('', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse MikroTik duration string to Seconds (int)
|
||||
|
||||
* Supports: 1d2h3m, 00:00:00, 1d 00:00:00
|
||||
*/
|
||||
public static function parseDuration($string) {
|
||||
if (empty($string)) return 0;
|
||||
|
||||
$string = trim($string);
|
||||
$totalSeconds = 0;
|
||||
|
||||
// 1. Handle "00:00:00" or "1d 00:00:00" (Colons)
|
||||
if (strpos($string, ':') !== false) {
|
||||
$parts = explode(' ', $string);
|
||||
$timePart = end($parts); // 00:00:00
|
||||
|
||||
// Calc time part
|
||||
$t = explode(':', $timePart);
|
||||
if (count($t) === 3) {
|
||||
$totalSeconds += ($t[0] * 3600) + ($t[1] * 60) + $t[2];
|
||||
} elseif (count($t) === 2) { // 00:00 (mm:ss or hh:mm? usually hh:mm in routeros logs, but 00:00:59 is uptime)
|
||||
// Assumption: if 2 parts, treat as MM:SS if small, or HH:MM?
|
||||
// RouterOS uptime is usually HH:MM:SS. Let's assume standard time ref.
|
||||
// Actually RouterOS uptime often drops hours if 0.
|
||||
// SAFE BET: Just Parse standard 3 parts.
|
||||
$totalSeconds += ($t[0] * 60) + $t[1];
|
||||
}
|
||||
|
||||
// Calc Day part "1d"
|
||||
if (count($parts) > 1) {
|
||||
$dayPart = $parts[0]; // 1d
|
||||
$totalSeconds += intval($dayPart) * 86400;
|
||||
}
|
||||
return $totalSeconds;
|
||||
}
|
||||
|
||||
// 2. Handle "1w2d3h4m5s" (Letters)
|
||||
if (preg_match_all('/(\d+)([wdhms])/', $string, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $m) {
|
||||
$val = intval($m[1]);
|
||||
$unit = $m[2];
|
||||
switch ($unit) {
|
||||
case 'w': $totalSeconds += $val * 604800; break;
|
||||
case 'd': $totalSeconds += $val * 86400; break;
|
||||
case 'h': $totalSeconds += $val * 3600; break;
|
||||
case 'm': $totalSeconds += $val * 60; break;
|
||||
case 's': $totalSeconds += $val; break;
|
||||
}
|
||||
}
|
||||
return $totalSeconds;
|
||||
}
|
||||
|
||||
// 3. Raw number?
|
||||
return intval($string);
|
||||
}
|
||||
}
|
||||
106
app/Helpers/HotspotHelper.php
Normal file
106
app/Helpers/HotspotHelper.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class HotspotHelper
|
||||
{
|
||||
/**
|
||||
* Parse profile on-login script metadata (Mikhmon format)
|
||||
* Format: :put (",mode,price,validity,selling_price,lock_user,")
|
||||
*/
|
||||
public static function parseProfileMetadata($script) {
|
||||
if (empty($script)) return [];
|
||||
|
||||
// Look for :put (",...,") pattern
|
||||
preg_match('/:put \("([^"]+)"\)/', $script, $matches);
|
||||
if (isset($matches[1])) {
|
||||
// Explode CSV: ,mode,price,validity,selling_price,lock_user,
|
||||
$data = explode(',', $matches[1]);
|
||||
|
||||
$clean = function($val) {
|
||||
return ($val === '0' || $val === '0d' || $val === '0h' || $val === '0m') ? '' : $val;
|
||||
};
|
||||
|
||||
return [
|
||||
'expired_mode' => $data[1] ?? '',
|
||||
'price' => $clean($data[2] ?? ''),
|
||||
'validity' => self::formatValidity($clean($data[3] ?? '')),
|
||||
'selling_price' => $clean($data[4] ?? ''),
|
||||
'lock_user' => $data[6] ?? '',
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format validity string (e.g., 3d2h5m -> 3d 2h 5m)
|
||||
*/
|
||||
public static function formatValidity($val) {
|
||||
if (empty($val)) return '';
|
||||
// Insert space after letters
|
||||
$val = preg_replace('/([a-z]+)/i', '$1 ', $val);
|
||||
return trim($val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format expired mode code to readable text
|
||||
*/
|
||||
public static function formatExpiredMode($mode) {
|
||||
switch ($mode) {
|
||||
case 'rem': return 'Remove';
|
||||
case 'ntf': return 'Notice';
|
||||
case 'remc': return 'Remove & Record';
|
||||
case 'ntfc': return 'Notice & Record';
|
||||
default: return $mode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human readable string (KB, MB, GB)
|
||||
*/
|
||||
public static function formatBytes($bytes, $precision = 2) {
|
||||
if (empty($bytes) || $bytes === '0') return '0 B';
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
$bytes /= pow(1024, $pow);
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get User Status Code
|
||||
* Returns: active, limited, locked, expired
|
||||
*/
|
||||
public static function getUserStatus($user) {
|
||||
// 1. Check for specific comment keywords (Highest Priority - usually set by scripts)
|
||||
$comment = strtolower($user['comment'] ?? '');
|
||||
|
||||
// "exp" explicitly means expired by script
|
||||
if (strpos($comment, 'exp') !== false) {
|
||||
return 'expired';
|
||||
}
|
||||
|
||||
// 2. Check Data Limit (Quota)
|
||||
$limitBytes = isset($user['limit-bytes-total']) ? (int)$user['limit-bytes-total'] : 0;
|
||||
if ($limitBytes > 0) {
|
||||
$bytesIn = isset($user['bytes-in']) ? (int)$user['bytes-in'] : 0;
|
||||
$bytesOut = isset($user['bytes-out']) ? (int)$user['bytes-out'] : 0;
|
||||
if (($bytesIn + $bytesOut) >= $limitBytes) {
|
||||
return 'limited';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check Disabled state
|
||||
if (($user['disabled'] ?? 'false') === 'true') {
|
||||
return 'locked';
|
||||
}
|
||||
|
||||
// 4. Default
|
||||
return 'active';
|
||||
}
|
||||
}
|
||||
45
app/Helpers/LanguageHelper.php
Normal file
45
app/Helpers/LanguageHelper.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class LanguageHelper
|
||||
{
|
||||
/**
|
||||
* Get list of available languages from public/lang directory
|
||||
*
|
||||
* @return array Array of languages with code and name
|
||||
*/
|
||||
public static function getAvailableLanguages()
|
||||
{
|
||||
$langDir = ROOT . '/public/lang';
|
||||
$languages = [];
|
||||
|
||||
if (!is_dir($langDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = scandir($langDir);
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..') continue;
|
||||
|
||||
if (pathinfo($file, PATHINFO_EXTENSION) === 'json') {
|
||||
$code = pathinfo($file, PATHINFO_FILENAME);
|
||||
|
||||
// Read file to get language name if defined, otherwise use code
|
||||
$content = file_get_contents($langDir . '/' . $file);
|
||||
$data = json_decode($content, true);
|
||||
|
||||
$name = $data['_meta']['name'] ?? strtoupper($code);
|
||||
$flag = $data['_meta']['flag'] ?? '🌐';
|
||||
|
||||
$languages[] = [
|
||||
'code' => $code,
|
||||
'name' => $name,
|
||||
'flag' => $flag
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $languages;
|
||||
}
|
||||
}
|
||||
102
app/Helpers/TemplateHelper.php
Normal file
102
app/Helpers/TemplateHelper.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class TemplateHelper {
|
||||
|
||||
public static function getDefaultTemplate() {
|
||||
return '
|
||||
<style>
|
||||
.voucher { width: 250px; background: #fff; padding: 10px; border: 1px solid #ccc; font-family: "Courier New", Courier, monospace; color: #000; }
|
||||
.header { text-align: center; font-weight: bold; margin-bottom: 5px; font-size: 14px; }
|
||||
.row { display: flex; justify-content: space-between; margin-bottom: 2px; font-size: 12px; }
|
||||
.code { font-size: 16px; font-weight: bold; text-align: center; margin: 10px 0; border: 1px dashed #000; padding: 5px; }
|
||||
.qr { text-align: center; margin-top: 5px; }
|
||||
</style>
|
||||
<div class="voucher">
|
||||
<div class="header">{{server_name}}</div>
|
||||
<div class="row"><span>Profile:</span> <span>{{profile}}</span></div>
|
||||
<div class="row"><span>Valid:</span> <span>{{validity}}</span></div>
|
||||
<div class="row"><span>Price:</span> <span>{{price}}</span></div>
|
||||
<div class="code">
|
||||
User: {{username}}<br>
|
||||
Pass: {{password}}
|
||||
</div>
|
||||
<div class="qr">{{qrcode}}</div>
|
||||
<div style="text-align:center; font-size: 10px; margin-top:5px;">
|
||||
Login: http://{{dns_name}}/login
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
public static function getMockContent($content) {
|
||||
if (empty($content)) return '';
|
||||
|
||||
// Dummy Data
|
||||
$dummyData = [
|
||||
'{{server_name}}' => 'Hotspot',
|
||||
'{{dns_name}}' => 'hotspot.lan',
|
||||
'{{username}}' => 'u-5829',
|
||||
'{{password}}' => '5912',
|
||||
'{{price}}' => '5.000',
|
||||
'{{validity}}' => '12 Hours',
|
||||
'{{profile}}' => 'Small-Packet',
|
||||
'{{time_limit}}' => '12h',
|
||||
'{{data_limit}}' => '1 GB',
|
||||
'{{ip_address}}' => '192.168.88.254',
|
||||
'{{mac_address}}' => 'AA:BB:CC:DD:EE:FF',
|
||||
'{{comment}}' => 'Thank You',
|
||||
'{{copyright}}' => 'Mikhmon',
|
||||
];
|
||||
|
||||
$content = str_replace(array_keys($dummyData), array_values($dummyData), $content);
|
||||
|
||||
// QR Code replacement
|
||||
$content = preg_replace('/\{\{\s*qrcode.*?\}\}/i', '<img src="https://api.qrserver.com/v1/create-qr-code/?size=80x80&data=http://hotspot.lan/login?user=u-5829" style="width:80px;height:80px;display:inline-block;">', $content);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public static function getPreviewPage($content) {
|
||||
$mockContent = self::getMockContent($content);
|
||||
|
||||
return '
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
|
||||
body { display: flex; align-items: center; justify-content: center; background-color: transparent; }
|
||||
#wrapper { display: inline-block; transform-origin: center center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">' . $mockContent . '</div>
|
||||
<script>
|
||||
window.addEventListener("load", () => {
|
||||
const wrap = document.getElementById("wrapper");
|
||||
if(!wrap) return;
|
||||
|
||||
const updateScale = () => {
|
||||
const w = wrap.offsetWidth;
|
||||
const h = wrap.offsetHeight;
|
||||
const winW = window.innerWidth - 24;
|
||||
const winH = window.innerHeight - 24;
|
||||
|
||||
let scale = 1;
|
||||
if (w > winW || h > winH) {
|
||||
scale = Math.min(winW / w, winH / h);
|
||||
} else {
|
||||
scale = Math.min(winW / w, winH / h);
|
||||
}
|
||||
wrap.style.transform = `scale(${scale})`;
|
||||
};
|
||||
|
||||
updateScale();
|
||||
window.addEventListener("resize", updateScale);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
}
|
||||
32
app/Helpers/ViewHelper.php
Normal file
32
app/Helpers/ViewHelper.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class ViewHelper
|
||||
{
|
||||
/**
|
||||
* Render a generic badge with icon
|
||||
* @param string $status (active, locked, expired, limited, etc.)
|
||||
* @param string|null $label Optional override text
|
||||
*/
|
||||
public static function badge($status, $label = null) {
|
||||
// Define styles for each status key
|
||||
$styles = [
|
||||
'active' => ['class' => 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', 'icon' => 'check-circle'],
|
||||
'limited' => ['class' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400', 'icon' => 'pie-chart'],
|
||||
'locked' => ['class' => 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', 'icon' => 'lock'],
|
||||
'expired' => ['class' => 'bg-accents-2 text-accents-6', 'icon' => 'clock'],
|
||||
'default' => ['class' => 'bg-blue-100 text-blue-800', 'icon' => 'info']
|
||||
];
|
||||
|
||||
$style = $styles[$status] ?? $styles['default'];
|
||||
$text = $label ?? ucfirst($status === 'limited' ? 'Quota' : $status);
|
||||
|
||||
return sprintf(
|
||||
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium %s gap-1"><i data-lucide="%s" class="w-3 h-3"></i> %s</span>',
|
||||
$style['class'],
|
||||
$style['icon'],
|
||||
$text
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user