Initial Release v1.0.0: Full feature set with Docker automation, Nginx/Alpine stack
21
public/.htaccess
Normal file
@@ -0,0 +1,21 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
5055
public/assets/css/styles.css
Normal file
BIN
public/assets/flag-icons-main.zip
Normal file
BIN
public/assets/fonts/Geist-Bold.woff2
Normal file
BIN
public/assets/fonts/Geist-Regular.woff2
Normal file
BIN
public/assets/fonts/GeistMono-Regular.woff2
Normal file
BIN
public/assets/img/favicon.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
20
public/assets/img/index.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Laksamadi Guko.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
echo "<meta http-equiv='refresh' content='0;url=../' />";
|
||||
?>
|
||||
|
||||
6
public/assets/img/logo-m-dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512">
|
||||
<path d="M0 0 C40.59 0 81.18 0 123 0 C128.94 9.9 128.94 9.9 135 20 C137.46577512 23.98910833 139.93737241 27.97040257 142.4375 31.9375 C143.71783436 33.97762211 144.99777509 36.01799131 146.27734375 38.05859375 C146.90237793 39.05487793 147.52741211 40.05116211 148.17138672 41.07763672 C150.96446678 45.54134411 153.73307277 50.02005064 156.5 54.5 C161.0792303 61.91158465 165.68818082 69.30397167 170.3125 76.6875 C176.26318836 86.19138864 182.15150549 95.73229223 188.01879883 105.2878418 C193.08709306 113.53496427 198.208198 121.74780885 203.34887695 129.94995117 C208.00076575 137.38038241 212.60177706 144.84108448 217.1875 152.3125 C221.7457377 159.73815683 226.3289587 167.14474569 231 174.5 C236.37297853 182.96055964 241.62451084 191.49338078 246.8671875 200.03515625 C251.37750651 207.37833217 255.92338863 214.69797726 260.5 222 C265.77200627 230.41153127 270.99463848 238.85188721 276.1875 247.3125 C280.74574404 254.73816715 285.32948761 262.14440277 290 269.5 C294.5525814 276.67922327 299.0626269 283.88437921 303.53466797 291.11401367 C304.25463221 292.27682202 304.97596045 293.43878708 305.69873047 294.59985352 C306.71188663 296.22822455 307.71932676 297.86014774 308.7265625 299.4921875 C309.31002441 300.43433105 309.89348633 301.37647461 310.49462891 302.34716797 C313.29448494 307.28119905 313.29448494 307.28119905 314 310 C313.10742187 312.33886719 313.10742187 312.33886719 311.46875 314.796875 C310.85410889 315.73821289 310.23946777 316.67955078 309.60620117 317.64941406 C308.91115479 318.67260742 308.2161084 319.69580078 307.5 320.75 C306.42359253 322.38146973 306.42359253 322.38146973 305.32543945 324.04589844 C303.33059412 327.06821098 301.312855 330.07460252 299.29058838 333.07861328 C297.08153396 336.36738288 294.89143772 339.6687451 292.69921875 342.96875 C289.93705235 347.12519373 287.17076618 351.27866178 284.38696289 355.4206543 C279.69084095 362.41374195 275.05797703 369.43667567 270.546875 376.55078125 C269.67715056 377.91328474 268.80712089 379.27559345 267.93676758 380.63769531 C266.69343815 382.58352582 265.45644896 384.53196515 264.23886108 386.49401855 C263.1314603 388.27830807 262.00378362 390.04810218 260.87109375 391.81640625 C260.24130615 392.82131104 259.61151855 393.82621582 258.96264648 394.86157227 C257 397 257 397 254.55784607 397.3649292 C252 397 252 397 250.43936157 395.46855164 C250.00127167 394.75278458 249.56318176 394.03701752 249.11181641 393.29956055 C248.60661469 392.49629837 248.10141296 391.69303619 247.5809021 390.86543274 C247.05147003 389.98558212 246.52203796 389.10573151 245.9765625 388.19921875 C244.81884975 386.34633241 243.66030081 384.49396831 242.50097656 382.64208984 C241.91658813 381.69622009 241.33219971 380.75035034 240.73010254 379.77581787 C238.15263697 375.6404705 235.46629193 371.58029076 232.7734375 367.51953125 C231.72553934 365.92993275 230.67801875 364.34008528 229.63085938 362.75 C228.54557801 361.1041633 227.46029141 359.45833007 226.375 357.8125 C225.20371001 356.03519977 224.03248774 354.2578549 222.86132812 352.48046875 C220.44133946 348.80793487 218.02106714 345.13558818 215.60058594 341.46337891 C210.45583874 333.65758543 205.31468042 325.84942899 200.17340088 318.04135132 C197.36576183 313.77746136 194.55786745 309.51373955 191.75 305.25 C190.62499879 303.54166746 189.49999879 301.83333413 188.375 300.125 C187.818125 299.279375 187.26125 298.43375 186.6875 297.5625 C181.62499997 289.87499995 181.62499997 289.87499995 179.93762207 287.31268311 C178.81221794 285.6037376 177.68681104 283.89479391 176.56140137 282.18585205 C173.75506191 277.92439992 170.94877005 273.66291647 168.14257812 269.40136719 C163.00738565 261.60304695 157.87180924 253.80498071 152.73449707 246.00805664 C150.38571799 242.44323839 148.03721141 238.87824066 145.6887207 235.31323242 C144.60145193 233.66291655 143.51405939 232.01268221 142.42651367 230.36254883 C132.25421049 214.9279819 122.13043857 199.46224835 112 184 C111.67 292.24 111.34 400.48 111 512 C74.37 512 37.74 512 0 512 C0 343.04 0 174.08 0 0 Z " fill="#ffffff" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C40.59 0 81.18 0 123 0 C123 168.96 123 337.92 123 512 C86.04 512 49.08 512 11 512 C10.67 402.77 10.34 293.54 10 181 C4.72 189.25 -0.56 197.5 -6 206 C-9.39444207 211.15548467 -12.78716673 216.30779963 -16.22192383 221.43579102 C-17.599304 223.49245466 -18.97271714 225.55172895 -20.34570312 227.61132812 C-22.50073142 230.84064053 -24.66547146 234.06319701 -26.83917236 237.27996826 C-27.94230355 238.91450952 -29.03943552 240.55309486 -30.13623047 242.19189453 C-30.76029785 243.1095459 -31.38436523 244.02719727 -32.02734375 244.97265625 C-32.559646 245.760354 -33.09194824 246.54805176 -33.64038086 247.35961914 C-34.08905518 247.90094482 -34.53772949 248.44227051 -35 249 C-35.66 249 -36.32 249 -37 249 C-38.35009766 247.20947266 -38.35009766 247.20947266 -39.9140625 244.6640625 C-40.501875 243.7153125 -41.0896875 242.7665625 -41.6953125 241.7890625 C-42.33210938 240.74492187 -42.96890625 239.70078125 -43.625 238.625 C-44.29015625 237.54734375 -44.9553125 236.4696875 -45.640625 235.359375 C-47.41266874 232.48561118 -49.17847981 229.6082147 -50.94104004 226.7286377 C-52.63090725 223.9701121 -54.32738918 221.21566567 -56.0234375 218.4609375 C-58.01919588 215.21887138 -60.01443826 211.97649876 -62.00732422 208.73266602 C-66.55995548 201.32481658 -71.14231268 193.93622184 -75.75 186.5625 C-76.66652344 185.09151855 -76.66652344 185.09151855 -77.6015625 183.59082031 C-80.57044721 178.83646363 -83.57016988 174.10746656 -86.640625 169.41796875 C-87.20813477 168.54390381 -87.77564453 167.66983887 -88.36035156 166.76928711 C-89.42453538 165.1341899 -90.49885882 163.50563072 -91.58496094 161.88500977 C-92.05192383 161.16498779 -92.51888672 160.44496582 -93 159.703125 C-93.4125 159.08083008 -93.825 158.45853516 -94.25 157.81738281 C-95.23752366 155.42443811 -94.91065312 154.39823636 -94 152 C-93.28247855 150.60752627 -92.51012761 149.24258512 -91.69921875 147.90234375 C-90.98511841 146.71560059 -90.98511841 146.71560059 -90.2565918 145.50488281 C-89.7387915 144.65764648 -89.22099121 143.81041016 -88.6875 142.9375 C-88.14843018 142.04772461 -87.60936035 141.15794922 -87.05395508 140.24121094 C-82.51613674 132.77957799 -77.86646672 125.39067843 -73.18359375 118.01953125 C-63.57216547 102.88747676 -54.1487842 87.63834556 -44.77050781 72.36108398 C-40.88941569 66.04649596 -36.97338141 59.75688711 -33 53.5 C-21.76980025 35.81118073 -10.92512644 17.87747962 0 0 Z " fill="#ffffff" transform="translate(389,0)"/>
|
||||
<path d="M0 0 C4.62276577 4.14454862 7.47322448 8.77374995 10.5625 14.125 C11.62963614 15.93860414 12.69869312 17.75107923 13.76953125 19.5625 C14.30916504 20.47644531 14.84879883 21.39039063 15.40478516 22.33203125 C17.78240918 26.30856558 20.25885329 30.21891917 22.75 34.125 C28.22964988 42.73406002 33.58176135 51.41803925 38.89916992 60.12817383 C42.24750462 65.59578939 45.64232668 71.01967842 49.16796875 76.375 C49.82361816 77.37684326 50.47926758 78.37868652 51.15478516 79.41088867 C52.39829787 81.30225655 53.6539539 83.18571316 54.92333984 85.05981445 C55.47586426 85.9024585 56.02838867 86.74510254 56.59765625 87.61328125 C57.08758057 88.34329346 57.57750488 89.07330566 58.08227539 89.82543945 C59.28133227 92.66662052 58.92600786 94.09059312 58 97 C57.01611328 99.00463867 57.01611328 99.00463867 55.8203125 100.91796875 C55.38589844 101.61470703 54.95148438 102.31144531 54.50390625 103.02929688 C54.04886719 103.74150391 53.59382813 104.45371094 53.125 105.1875 C52.67769531 105.91130859 52.23039062 106.63511719 51.76953125 107.38085938 C49.65391647 110.73526333 48.36691842 112.75538772 45 115 C44.37508667 113.98353394 44.37508667 113.98353394 43.73754883 112.9465332 C27.17658577 86.00544711 27.17658577 86.00544711 10.46337891 59.15869141 C2.53411428 46.50404945 -5.26881404 33.77655539 -13 21 C-4.738041 6.21867882 -4.738041 6.21867882 0 0 Z " fill="#ffffff" transform="translate(281,175)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
6
public/assets/img/logo-m.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512">
|
||||
<path d="M0 0 C40.59 0 81.18 0 123 0 C128.94 9.9 128.94 9.9 135 20 C137.46577512 23.98910833 139.93737241 27.97040257 142.4375 31.9375 C143.71783436 33.97762211 144.99777509 36.01799131 146.27734375 38.05859375 C146.90237793 39.05487793 147.52741211 40.05116211 148.17138672 41.07763672 C150.96446678 45.54134411 153.73307277 50.02005064 156.5 54.5 C161.0792303 61.91158465 165.68818082 69.30397167 170.3125 76.6875 C176.26318836 86.19138864 182.15150549 95.73229223 188.01879883 105.2878418 C193.08709306 113.53496427 198.208198 121.74780885 203.34887695 129.94995117 C208.00076575 137.38038241 212.60177706 144.84108448 217.1875 152.3125 C221.7457377 159.73815683 226.3289587 167.14474569 231 174.5 C236.37297853 182.96055964 241.62451084 191.49338078 246.8671875 200.03515625 C251.37750651 207.37833217 255.92338863 214.69797726 260.5 222 C265.77200627 230.41153127 270.99463848 238.85188721 276.1875 247.3125 C280.74574404 254.73816715 285.32948761 262.14440277 290 269.5 C294.5525814 276.67922327 299.0626269 283.88437921 303.53466797 291.11401367 C304.25463221 292.27682202 304.97596045 293.43878708 305.69873047 294.59985352 C306.71188663 296.22822455 307.71932676 297.86014774 308.7265625 299.4921875 C309.31002441 300.43433105 309.89348633 301.37647461 310.49462891 302.34716797 C313.29448494 307.28119905 313.29448494 307.28119905 314 310 C313.10742187 312.33886719 313.10742187 312.33886719 311.46875 314.796875 C310.85410889 315.73821289 310.23946777 316.67955078 309.60620117 317.64941406 C308.91115479 318.67260742 308.2161084 319.69580078 307.5 320.75 C306.42359253 322.38146973 306.42359253 322.38146973 305.32543945 324.04589844 C303.33059412 327.06821098 301.312855 330.07460252 299.29058838 333.07861328 C297.08153396 336.36738288 294.89143772 339.6687451 292.69921875 342.96875 C289.93705235 347.12519373 287.17076618 351.27866178 284.38696289 355.4206543 C279.69084095 362.41374195 275.05797703 369.43667567 270.546875 376.55078125 C269.67715056 377.91328474 268.80712089 379.27559345 267.93676758 380.63769531 C266.69343815 382.58352582 265.45644896 384.53196515 264.23886108 386.49401855 C263.1314603 388.27830807 262.00378362 390.04810218 260.87109375 391.81640625 C260.24130615 392.82131104 259.61151855 393.82621582 258.96264648 394.86157227 C257 397 257 397 254.55784607 397.3649292 C252 397 252 397 250.43936157 395.46855164 C250.00127167 394.75278458 249.56318176 394.03701752 249.11181641 393.29956055 C248.60661469 392.49629837 248.10141296 391.69303619 247.5809021 390.86543274 C247.05147003 389.98558212 246.52203796 389.10573151 245.9765625 388.19921875 C244.81884975 386.34633241 243.66030081 384.49396831 242.50097656 382.64208984 C241.91658813 381.69622009 241.33219971 380.75035034 240.73010254 379.77581787 C238.15263697 375.6404705 235.46629193 371.58029076 232.7734375 367.51953125 C231.72553934 365.92993275 230.67801875 364.34008528 229.63085938 362.75 C228.54557801 361.1041633 227.46029141 359.45833007 226.375 357.8125 C225.20371001 356.03519977 224.03248774 354.2578549 222.86132812 352.48046875 C220.44133946 348.80793487 218.02106714 345.13558818 215.60058594 341.46337891 C210.45583874 333.65758543 205.31468042 325.84942899 200.17340088 318.04135132 C197.36576183 313.77746136 194.55786745 309.51373955 191.75 305.25 C190.62499879 303.54166746 189.49999879 301.83333413 188.375 300.125 C187.818125 299.279375 187.26125 298.43375 186.6875 297.5625 C181.62499997 289.87499995 181.62499997 289.87499995 179.93762207 287.31268311 C178.81221794 285.6037376 177.68681104 283.89479391 176.56140137 282.18585205 C173.75506191 277.92439992 170.94877005 273.66291647 168.14257812 269.40136719 C163.00738565 261.60304695 157.87180924 253.80498071 152.73449707 246.00805664 C150.38571799 242.44323839 148.03721141 238.87824066 145.6887207 235.31323242 C144.60145193 233.66291655 143.51405939 232.01268221 142.42651367 230.36254883 C132.25421049 214.9279819 122.13043857 199.46224835 112 184 C111.67 292.24 111.34 400.48 111 512 C74.37 512 37.74 512 0 512 C0 343.04 0 174.08 0 0 Z " fill="#000000" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C40.59 0 81.18 0 123 0 C123 168.96 123 337.92 123 512 C86.04 512 49.08 512 11 512 C10.67 402.77 10.34 293.54 10 181 C4.72 189.25 -0.56 197.5 -6 206 C-9.39444207 211.15548467 -12.78716673 216.30779963 -16.22192383 221.43579102 C-17.599304 223.49245466 -18.97271714 225.55172895 -20.34570312 227.61132812 C-22.50073142 230.84064053 -24.66547146 234.06319701 -26.83917236 237.27996826 C-27.94230355 238.91450952 -29.03943552 240.55309486 -30.13623047 242.19189453 C-30.76029785 243.1095459 -31.38436523 244.02719727 -32.02734375 244.97265625 C-32.559646 245.760354 -33.09194824 246.54805176 -33.64038086 247.35961914 C-34.08905518 247.90094482 -34.53772949 248.44227051 -35 249 C-35.66 249 -36.32 249 -37 249 C-38.35009766 247.20947266 -38.35009766 247.20947266 -39.9140625 244.6640625 C-40.501875 243.7153125 -41.0896875 242.7665625 -41.6953125 241.7890625 C-42.33210938 240.74492187 -42.96890625 239.70078125 -43.625 238.625 C-44.29015625 237.54734375 -44.9553125 236.4696875 -45.640625 235.359375 C-47.41266874 232.48561118 -49.17847981 229.6082147 -50.94104004 226.7286377 C-52.63090725 223.9701121 -54.32738918 221.21566567 -56.0234375 218.4609375 C-58.01919588 215.21887138 -60.01443826 211.97649876 -62.00732422 208.73266602 C-66.55995548 201.32481658 -71.14231268 193.93622184 -75.75 186.5625 C-76.66652344 185.09151855 -76.66652344 185.09151855 -77.6015625 183.59082031 C-80.57044721 178.83646363 -83.57016988 174.10746656 -86.640625 169.41796875 C-87.20813477 168.54390381 -87.77564453 167.66983887 -88.36035156 166.76928711 C-89.42453538 165.1341899 -90.49885882 163.50563072 -91.58496094 161.88500977 C-92.05192383 161.16498779 -92.51888672 160.44496582 -93 159.703125 C-93.4125 159.08083008 -93.825 158.45853516 -94.25 157.81738281 C-95.23752366 155.42443811 -94.91065312 154.39823636 -94 152 C-93.28247855 150.60752627 -92.51012761 149.24258512 -91.69921875 147.90234375 C-90.98511841 146.71560059 -90.98511841 146.71560059 -90.2565918 145.50488281 C-89.7387915 144.65764648 -89.22099121 143.81041016 -88.6875 142.9375 C-88.14843018 142.04772461 -87.60936035 141.15794922 -87.05395508 140.24121094 C-82.51613674 132.77957799 -77.86646672 125.39067843 -73.18359375 118.01953125 C-63.57216547 102.88747676 -54.1487842 87.63834556 -44.77050781 72.36108398 C-40.88941569 66.04649596 -36.97338141 59.75688711 -33 53.5 C-21.76980025 35.81118073 -10.92512644 17.87747962 0 0 Z " fill="#000000" transform="translate(389,0)"/>
|
||||
<path d="M0 0 C4.62276577 4.14454862 7.47322448 8.77374995 10.5625 14.125 C11.62963614 15.93860414 12.69869312 17.75107923 13.76953125 19.5625 C14.30916504 20.47644531 14.84879883 21.39039063 15.40478516 22.33203125 C17.78240918 26.30856558 20.25885329 30.21891917 22.75 34.125 C28.22964988 42.73406002 33.58176135 51.41803925 38.89916992 60.12817383 C42.24750462 65.59578939 45.64232668 71.01967842 49.16796875 76.375 C49.82361816 77.37684326 50.47926758 78.37868652 51.15478516 79.41088867 C52.39829787 81.30225655 53.6539539 83.18571316 54.92333984 85.05981445 C55.47586426 85.9024585 56.02838867 86.74510254 56.59765625 87.61328125 C57.08758057 88.34329346 57.57750488 89.07330566 58.08227539 89.82543945 C59.28133227 92.66662052 58.92600786 94.09059312 58 97 C57.01611328 99.00463867 57.01611328 99.00463867 55.8203125 100.91796875 C55.38589844 101.61470703 54.95148438 102.31144531 54.50390625 103.02929688 C54.04886719 103.74150391 53.59382813 104.45371094 53.125 105.1875 C52.67769531 105.91130859 52.23039062 106.63511719 51.76953125 107.38085938 C49.65391647 110.73526333 48.36691842 112.75538772 45 115 C44.37508667 113.98353394 44.37508667 113.98353394 43.73754883 112.9465332 C27.17658577 86.00544711 27.17658577 86.00544711 10.46337891 59.15869141 C2.53411428 46.50404945 -5.26881404 33.77655539 -13 21 C-4.738041 6.21867882 -4.738041 6.21867882 0 0 Z " fill="#000000" transform="translate(281,175)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
BIN
public/assets/img/logo-outlined.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
5
public/assets/img/logo-outlined.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2664.00 862.00">
|
||||
<g transform="translate(1332, 431) scale(1, -1) translate(-1346, -355)">
|
||||
<path d="M74 710L277 710L458 193L638 710L841 710L841 0L689 0L689 460L522 2L392 2L226 460L226 0L74 0Z M967 710L1119 710L1119 0L967 0Z M1218 710L1375 710L1564 171L1752 710L1910 710L1652 0L1474 0Z M2272 -16Q2165 -16 2087 28.5Q2009 73 1968 156.5Q1927 240 1927 354Q1927 468 1968.5 552Q2010 636 2087.5 681Q2165 726 2272 726Q2379 726 2457 681Q2535 636 2576.5 552Q2618 468 2618 354Q2618 240 2576.5 156.5Q2535 73 2457 28.5Q2379 -16 2272 -16ZM2272 112Q2362 112 2412 175.5Q2462 239 2462 354Q2462 469 2411.5 533.5Q2361 598 2272 598Q2183 598 2133 533.5Q2083 469 2083 354Q2083 239 2133 175.5Q2183 112 2272 112Z " fill="#000000" stroke="#ffffff" stroke-width="100" stroke-linejoin="round" paint-order="stroke"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 859 B |
BIN
public/assets/img/logo.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
5
public/assets/img/logo.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2664.00 862.00">
|
||||
<g transform="translate(1332, 431) scale(1, -1) translate(-1346, -355)">
|
||||
<path d="M74 710L277 710L458 193L638 710L841 710L841 0L689 0L689 460L522 2L392 2L226 460L226 0L74 0Z M967 710L1119 710L1119 0L967 0Z M1218 710L1375 710L1564 171L1752 710L1910 710L1652 0L1474 0Z M2272 -16Q2165 -16 2087 28.5Q2009 73 1968 156.5Q1927 240 1927 354Q1927 468 1968.5 552Q2010 636 2087.5 681Q2165 726 2272 726Q2379 726 2457 681Q2535 636 2576.5 552Q2618 468 2618 354Q2618 240 2576.5 156.5Q2535 73 2457 28.5Q2379 -16 2272 -16ZM2272 112Q2362 112 2412 175.5Q2462 239 2462 354Q2462 469 2411.5 533.5Q2361 598 2272 598Q2183 598 2133 533.5Q2083 469 2083 354Q2083 239 2133 175.5Q2183 112 2272 112Z " fill="#000000"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 778 B |
|
After Width: | Height: | Size: 88 KiB |
BIN
public/assets/img/logos/whc7WD.jpg
Normal file
|
After Width: | Height: | Size: 88 KiB |
148
public/assets/js/alert-helper.js
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Global Alert Helper for Mivo
|
||||
* Provides a standardized way to trigger premium SweetAlert2 dialogs.
|
||||
*/
|
||||
const Mivo = {
|
||||
/**
|
||||
* Show a simple alert dialog.
|
||||
* @param {string} type - 'success', 'error', 'warning', 'info', 'question'
|
||||
* @param {string} title - The title of the alert
|
||||
* @param {string} message - The body text/HTML
|
||||
* @returns {Promise}
|
||||
*/
|
||||
alert: function(type, title, message = '') {
|
||||
const typeMap = {
|
||||
'success': { icon: 'check-circle-2', color: 'text-success' },
|
||||
'error': { icon: 'x-circle', color: 'text-error' },
|
||||
'warning': { icon: 'alert-triangle', color: 'text-warning' },
|
||||
'info': { icon: 'info', color: 'text-info' },
|
||||
'question':{ icon: 'help-circle', color: 'text-question' }
|
||||
};
|
||||
|
||||
const config = typeMap[type] || typeMap['info'];
|
||||
|
||||
return Swal.fire({
|
||||
iconHtml: `<i data-lucide="${config.icon}" class="w-12 h-12 ${config.color}"></i>`,
|
||||
title: title,
|
||||
html: message,
|
||||
confirmButtonText: 'OK',
|
||||
customClass: {
|
||||
popup: 'swal2-premium-card',
|
||||
confirmButton: 'btn btn-primary',
|
||||
cancelButton: 'btn btn-secondary',
|
||||
},
|
||||
buttonsStyling: false,
|
||||
heightAuto: false,
|
||||
didOpen: () => {
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a confirmation dialog.
|
||||
* @param {string} title - The title of the confirmation
|
||||
* @param {string} message - The body text/HTML
|
||||
* @param {string} confirmText - Text for the confirm button
|
||||
* @param {string} cancelText - Text for the cancel button
|
||||
* @returns {Promise} Resolves if confirmed, rejects if cancelled
|
||||
*/
|
||||
confirm: function(title, message = '', confirmText = 'Yes, Proceed', cancelText = 'Cancel') {
|
||||
return Swal.fire({
|
||||
iconHtml: `<i data-lucide="help-circle" class="w-12 h-12 text-question"></i>`,
|
||||
title: title,
|
||||
html: message,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: confirmText,
|
||||
cancelButtonText: cancelText,
|
||||
customClass: {
|
||||
popup: 'swal2-premium-card',
|
||||
confirmButton: 'btn btn-primary',
|
||||
cancelButton: 'btn btn-secondary',
|
||||
},
|
||||
buttonsStyling: false,
|
||||
reverseButtons: true,
|
||||
heightAuto: false,
|
||||
didOpen: () => {
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}).then(result => result.isConfirmed);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a premium stacking toast.
|
||||
* @param {string} type - 'success', 'error', 'warning', 'info'
|
||||
* @param {string} title - Title
|
||||
* @param {string} message - Body text
|
||||
* @param {number} duration - ms before auto-close
|
||||
*/
|
||||
toast: function(type, title, message = '', duration = 5000) {
|
||||
let container = document.getElementById('mivo-toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'mivo-toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const typeMap = {
|
||||
'success': { icon: 'check-circle-2', color: 'text-success' },
|
||||
'error': { icon: 'x-circle', color: 'text-error' },
|
||||
'warning': { icon: 'alert-triangle', color: 'text-warning' },
|
||||
'info': { icon: 'info', color: 'text-info' }
|
||||
};
|
||||
|
||||
const config = typeMap[type] || typeMap['info'];
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `mivo-toast ${config.color}`;
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="mivo-toast-icon">
|
||||
<i data-lucide="${config.icon}" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div class="mivo-toast-content">
|
||||
<div class="mivo-toast-title">${title}</div>
|
||||
${message ? `<div class="mivo-toast-message">${message}</div>` : ''}
|
||||
</div>
|
||||
<button class="mivo-toast-close">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<div class="mivo-toast-progress"></div>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
// Close logic
|
||||
const closeToast = () => {
|
||||
toast.classList.add('mivo-toast-fade-out');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
if (container.children.length === 0) container.remove();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
toast.querySelector('.mivo-toast-close').addEventListener('click', closeToast);
|
||||
|
||||
// Auto-close with progress bar
|
||||
const progress = toast.querySelector('.mivo-toast-progress');
|
||||
const start = Date.now();
|
||||
|
||||
const updateProgress = () => {
|
||||
const elapsed = Date.now() - start;
|
||||
const percentage = Math.min((elapsed / duration) * 100, 100);
|
||||
progress.style.width = percentage + '%';
|
||||
|
||||
if (percentage < 100) {
|
||||
requestAnimationFrame(updateProgress);
|
||||
} else {
|
||||
closeToast();
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(updateProgress);
|
||||
}
|
||||
};
|
||||
|
||||
// Also expose as global shortcuts if needed
|
||||
window.Mivo = Mivo;
|
||||
14
public/assets/js/chart.min.js
vendored
Normal file
261
public/assets/js/custom-select.js
Normal file
@@ -0,0 +1,261 @@
|
||||
class CustomSelect {
|
||||
static instances = [];
|
||||
|
||||
constructor(selectElement) {
|
||||
if (selectElement.dataset.customSelectInitialized === 'true') {
|
||||
return;
|
||||
}
|
||||
selectElement.dataset.customSelectInitialized = 'true';
|
||||
|
||||
this.originalSelect = selectElement;
|
||||
this.originalSelect.style.display = 'none';
|
||||
this.options = Array.from(this.originalSelect.options);
|
||||
|
||||
// Settings
|
||||
this.wrapper = document.createElement('div');
|
||||
|
||||
// Standard classes
|
||||
let wrapperClasses = 'custom-select-wrapper relative active-select';
|
||||
|
||||
// Intelligent Width:
|
||||
// If original select expects full width, wrapper must be full width.
|
||||
// Otherwise, use w-fit (Crucial for Right-Alignment in toolbars to work).
|
||||
const widthClass = Array.from(this.originalSelect.classList).find(c => c.startsWith('w-') && c !== 'w-full');
|
||||
const isFullWidth = this.originalSelect.classList.contains('w-full') ||
|
||||
this.originalSelect.classList.contains('form-control') ||
|
||||
this.originalSelect.classList.contains('form-input');
|
||||
|
||||
if (widthClass) {
|
||||
wrapperClasses += ' ' + widthClass;
|
||||
} else if (isFullWidth) {
|
||||
wrapperClasses += ' w-full';
|
||||
} else {
|
||||
wrapperClasses += ' w-fit';
|
||||
}
|
||||
|
||||
this.wrapper.className = wrapperClasses;
|
||||
|
||||
this.init();
|
||||
|
||||
// Store instance
|
||||
if (!CustomSelect.instances) CustomSelect.instances = [];
|
||||
CustomSelect.instances.push(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
// Create Trigger
|
||||
this.trigger = document.createElement('div');
|
||||
|
||||
const isFilter = this.originalSelect.classList.contains('form-filter');
|
||||
const baseClass = isFilter ? 'form-filter' : 'form-input';
|
||||
|
||||
this.trigger.className = `${baseClass} flex items-center justify-between cursor-pointer pr-3`;
|
||||
this.trigger.style.paddingLeft = '0.75rem';
|
||||
|
||||
this.trigger.innerHTML = `
|
||||
<span class="custom-select-value truncate text-foreground flex-1 text-left">${this.originalSelect.options[this.originalSelect.selectedIndex].text}</span>
|
||||
<div class="custom-select-icon flex-shrink-0 ml-2 transition-transform duration-200 transform">
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 text-foreground opacity-70"></i>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Inherit classes from original select (excluding custom-select marker)
|
||||
if (this.originalSelect.classList.length > 0) {
|
||||
const inheritedClasses = Array.from(this.originalSelect.classList)
|
||||
.filter(c => c !== 'custom-select' && c !== 'hidden')
|
||||
.join(' ');
|
||||
if (inheritedClasses) {
|
||||
this.trigger.className += ' ' + inheritedClasses;
|
||||
}
|
||||
}
|
||||
|
||||
// Final sanity check for full width
|
||||
if (this.wrapper.classList.contains('w-full')) {
|
||||
this.trigger.classList.add('w-full');
|
||||
}
|
||||
|
||||
// Create Options Menu Wrapper (No Scroll Here)
|
||||
this.menu = document.createElement('div');
|
||||
// Create Options Menu Wrapper (No Scroll Here)
|
||||
// Create Options Menu Wrapper (No Scroll Here)
|
||||
this.menu = document.createElement('div');
|
||||
this.menu.className = 'custom-select-dropdown';
|
||||
|
||||
// Create Scrollable List Container
|
||||
this.listContainer = document.createElement('div');
|
||||
this.listContainer.className = 'overflow-y-auto flex-1 py-1 custom-scrollbar';
|
||||
|
||||
// Search Functionality
|
||||
if (this.originalSelect.dataset.search === 'true') {
|
||||
const searchContainer = document.createElement('div');
|
||||
searchContainer.className = 'p-2 bg-background z-10 border-b border-accents-2 flex-shrink-0 rounded-t-md';
|
||||
|
||||
this.searchInput = document.createElement('input');
|
||||
this.searchInput.type = 'text';
|
||||
this.searchInput.className = 'w-full px-2 py-1 text-sm bg-accents-1 border border-accents-2 rounded focus:outline-none focus:ring-1 focus:ring-foreground';
|
||||
this.searchInput.placeholder = 'Search...';
|
||||
|
||||
searchContainer.appendChild(this.searchInput);
|
||||
this.menu.appendChild(searchContainer);
|
||||
|
||||
// Search Event
|
||||
this.searchInput.addEventListener('input', (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
this.options.forEach((option, index) => {
|
||||
const item = this.listContainer.querySelector(`[data-index="${index}"]`);
|
||||
if (item) {
|
||||
const text = option.text.toLowerCase();
|
||||
item.style.display = text.includes(term) ? 'flex' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.searchInput.addEventListener('click', (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
// Build Options
|
||||
this.options.forEach((option, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'px-3 py-2 text-sm cursor-pointer hover:bg-accents-1 transition-colors flex items-center justify-between whitespace-nowrap';
|
||||
if(option.selected) item.classList.add('bg-accents-1', 'font-medium');
|
||||
|
||||
item.textContent = option.text;
|
||||
item.dataset.value = option.value;
|
||||
item.dataset.index = index;
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
this.select(index);
|
||||
});
|
||||
|
||||
this.listContainer.appendChild(item);
|
||||
});
|
||||
|
||||
// Append List to Menu
|
||||
this.menu.appendChild(this.listContainer);
|
||||
|
||||
// Append to wrapper
|
||||
this.wrapper.appendChild(this.trigger);
|
||||
this.wrapper.appendChild(this.menu);
|
||||
this.originalSelect.parentNode.insertBefore(this.wrapper, this.originalSelect);
|
||||
|
||||
// Event Listeners
|
||||
this.trigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggle();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.wrapper.contains(e.target)) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons({ root: this.trigger });
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (!this.menu.classList.contains('open')) {
|
||||
this.open();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
CustomSelect.instances.forEach(instance => {
|
||||
if (instance !== this) instance.close();
|
||||
});
|
||||
|
||||
// Smart Positioning
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const spaceRight = window.innerWidth - rect.left;
|
||||
|
||||
// Reset positioning classes
|
||||
this.menu.classList.remove('right-0', 'origin-top-right', 'left-0', 'origin-top-left');
|
||||
|
||||
// Logic: Zone Check - If near right edge (< 300px), Force Right Align.
|
||||
// Doing this purely based on coordinates prevents "Layout Jumping" caused by measuring content width.
|
||||
if (spaceRight < 300) {
|
||||
this.menu.classList.add('right-0', 'origin-top-right');
|
||||
} else {
|
||||
this.menu.classList.add('left-0', 'origin-top-left');
|
||||
}
|
||||
|
||||
// Apply visual open states
|
||||
this.menu.classList.add('open');
|
||||
|
||||
this.trigger.classList.add('ring-1', 'ring-foreground');
|
||||
const icon = this.trigger.querySelector('.custom-select-icon');
|
||||
if(icon) icon.classList.add('rotate-180');
|
||||
|
||||
if (this.searchInput) {
|
||||
setTimeout(() => this.searchInput.focus(), 50);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.menu.classList.remove('open');
|
||||
|
||||
this.trigger.classList.remove('ring-1', 'ring-foreground');
|
||||
const icon = this.trigger.querySelector('.custom-select-icon');
|
||||
if(icon) icon.classList.remove('rotate-180');
|
||||
}
|
||||
|
||||
select(index) {
|
||||
// Update Original Select
|
||||
this.originalSelect.selectedIndex = index;
|
||||
|
||||
// Update UI
|
||||
this.trigger.querySelector('.custom-select-value').textContent = this.options[index].text;
|
||||
|
||||
// Update Active State in List
|
||||
Array.from(this.listContainer.children).forEach((child) => {
|
||||
// Safe check
|
||||
if (!child.dataset.index) return;
|
||||
|
||||
if (parseInt(child.dataset.index) === index) {
|
||||
child.classList.add('bg-accents-1', 'font-medium');
|
||||
} else {
|
||||
child.classList.remove('bg-accents-1', 'font-medium');
|
||||
}
|
||||
});
|
||||
|
||||
this.close();
|
||||
this.originalSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
refresh() {
|
||||
// Clear list items
|
||||
this.listContainer.innerHTML = '';
|
||||
|
||||
// Re-read options
|
||||
this.options = Array.from(this.originalSelect.options);
|
||||
|
||||
this.options.forEach((option, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'px-3 py-2 text-sm cursor-pointer hover:bg-accents-1 transition-colors flex items-center justify-between whitespace-nowrap';
|
||||
if(option.selected) item.classList.add('bg-accents-1', 'font-medium');
|
||||
|
||||
item.textContent = option.text;
|
||||
item.dataset.value = option.value;
|
||||
item.dataset.index = index;
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
this.select(index);
|
||||
});
|
||||
|
||||
this.listContainer.appendChild(item);
|
||||
});
|
||||
|
||||
// Update Trigger
|
||||
if (this.originalSelect.selectedIndex >= 0) {
|
||||
this.trigger.querySelector('.custom-select-value').textContent = this.originalSelect.options[this.originalSelect.selectedIndex].text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('select.custom-select').forEach(el => new CustomSelect(el));
|
||||
});
|
||||
336
public/assets/js/datatable.js
Normal file
@@ -0,0 +1,336 @@
|
||||
class SimpleDataTable {
|
||||
constructor(tableSelector, options = {}) {
|
||||
this.table = document.querySelector(tableSelector);
|
||||
if (!this.table) return;
|
||||
|
||||
this.tbody = this.table.querySelector('tbody');
|
||||
this.rows = Array.from(this.tbody.querySelectorAll('tr'));
|
||||
this.originalRows = [...this.rows]; // Keep copy
|
||||
|
||||
this.options = {
|
||||
itemsPerPage: 10,
|
||||
searchable: true,
|
||||
pagination: true,
|
||||
filters: [], // Array of { index: number, label: string }
|
||||
...options
|
||||
};
|
||||
|
||||
this.currentPage = 1;
|
||||
this.searchQuery = '';
|
||||
this.activeFilters = {}; // { columnIndex: value }
|
||||
this.filteredRows = [...this.originalRows];
|
||||
|
||||
// Wait for translations to load if i18n is used
|
||||
if (window.i18n && window.i18n.ready) {
|
||||
window.i18n.ready.then(() => this.init());
|
||||
} else {
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Listen for language change
|
||||
window.addEventListener('languageChanged', () => {
|
||||
this.reTranslate();
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
reTranslate() {
|
||||
// Update perPage label
|
||||
const labels = this.wrapper.querySelectorAll('span.text-accents-5');
|
||||
labels.forEach(label => {
|
||||
if (label.textContent.includes('entries per page') || (window.i18n && label.textContent === window.i18n.t('common.table.entries_per_page'))) {
|
||||
label.textContent = window.i18n ? window.i18n.t('common.table.entries_per_page') : 'entries per page';
|
||||
}
|
||||
});
|
||||
|
||||
// Update search placeholder
|
||||
const searchInput = this.wrapper.querySelector('input[type="text"]');
|
||||
if (searchInput) {
|
||||
searchInput.placeholder = window.i18n ? window.i18n.t('common.table.search_placeholder') : 'Search...';
|
||||
}
|
||||
|
||||
// Update All option
|
||||
const perPageSelect = this.wrapper.querySelector('select');
|
||||
if (perPageSelect) {
|
||||
const allOption = Array.from(perPageSelect.options).find(opt => opt.value === "-1");
|
||||
if (allOption) {
|
||||
allOption.text = window.i18n ? window.i18n.t('common.table.all') : 'All';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
// Create Wrapper
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'datatable-wrapper space-y-4';
|
||||
this.table.parentNode.insertBefore(this.wrapper, this.table);
|
||||
|
||||
// Create Controls Header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'flex flex-col sm:flex-row justify-between items-center gap-4 mb-4';
|
||||
|
||||
// Show Entries Wrapper
|
||||
const controlsLeft = document.createElement('div');
|
||||
controlsLeft.className = 'flex items-center gap-3 w-full sm:w-auto flex-wrap';
|
||||
|
||||
const perPageSelect = document.createElement('select');
|
||||
perPageSelect.className = 'form-filter w-20';
|
||||
|
||||
[5, 10, 25, 50, 100].forEach(num => {
|
||||
const option = document.createElement('option');
|
||||
option.value = num;
|
||||
option.text = num;
|
||||
if (num === this.options.itemsPerPage) option.selected = true;
|
||||
perPageSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// All option
|
||||
const allOption = document.createElement('option');
|
||||
allOption.value = -1;
|
||||
allOption.text = window.i18n ? window.i18n.t('common.table.all') : 'All';
|
||||
perPageSelect.appendChild(allOption);
|
||||
|
||||
perPageSelect.addEventListener('change', (e) => {
|
||||
const val = parseInt(e.target.value);
|
||||
this.options.itemsPerPage = val === -1 ? this.originalRows.length : val;
|
||||
this.currentPage = 1;
|
||||
this.render();
|
||||
});
|
||||
|
||||
// Label
|
||||
const label = document.createElement('span');
|
||||
label.className = 'text-sm text-accents-5 whitespace-nowrap';
|
||||
label.textContent = window.i18n ? window.i18n.t('common.table.entries_per_page') : 'entries per page';
|
||||
|
||||
controlsLeft.appendChild(perPageSelect);
|
||||
controlsLeft.appendChild(label);
|
||||
|
||||
// Initialize Filters if provided
|
||||
if (this.options.filters && this.options.filters.length > 0) {
|
||||
this.options.filters.forEach(filterConfig => {
|
||||
this.initFilter(filterConfig, controlsLeft); // Append to Left Controls
|
||||
});
|
||||
}
|
||||
|
||||
header.appendChild(controlsLeft);
|
||||
|
||||
// Initialize CustomSelect if available (for perPage)
|
||||
if (typeof CustomSelect !== 'undefined') {
|
||||
new CustomSelect(perPageSelect);
|
||||
}
|
||||
|
||||
// Search Input
|
||||
if (this.options.searchable) {
|
||||
const searchWrapper = document.createElement('div');
|
||||
searchWrapper.className = 'input-group sm:w-64 z-10';
|
||||
const placeholder = window.i18n ? window.i18n.t('common.table.search_placeholder') : 'Search...';
|
||||
searchWrapper.innerHTML = `
|
||||
<div class="input-icon">
|
||||
<i data-lucide="search" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<input type="text" class="form-input-search w-full" placeholder="${placeholder}">
|
||||
`;
|
||||
const input = searchWrapper.querySelector('input');
|
||||
input.addEventListener('input', (e) => this.handleSearch(e.target.value));
|
||||
header.appendChild(searchWrapper);
|
||||
}
|
||||
|
||||
this.wrapper.appendChild(header);
|
||||
|
||||
// Move Table into Wrapper
|
||||
// Move Table into Wrapper
|
||||
this.tableWrapper = document.createElement('div');
|
||||
this.tableWrapper.className = 'rounded-md border border-accents-2 overflow-x-auto bg-white/30 dark:bg-black/30 backdrop-blur-sm'; // overflow-x-auto for responsiveness
|
||||
this.tableWrapper.appendChild(this.table);
|
||||
this.wrapper.appendChild(this.tableWrapper);
|
||||
|
||||
// Render Icons for Header Controls
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons({
|
||||
root: header
|
||||
});
|
||||
}
|
||||
|
||||
// Pagination Controls
|
||||
if (this.options.pagination) {
|
||||
this.paginationContainer = document.createElement('div');
|
||||
this.paginationContainer.className = 'flex items-center justify-between px-2';
|
||||
this.wrapper.appendChild(this.paginationContainer);
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
initFilter(config, container) {
|
||||
// config = { index: number, label: string }
|
||||
const colIndex = config.index;
|
||||
|
||||
// Get unique values
|
||||
const values = new Set();
|
||||
this.originalRows.forEach(row => {
|
||||
const cell = row.cells[colIndex];
|
||||
if (cell) {
|
||||
const text = cell.textContent.trim();
|
||||
// Basic cleanup: remove extra whitespace
|
||||
if(text && text !== '-' && text !== '') values.add(text);
|
||||
}
|
||||
});
|
||||
|
||||
// Create Select
|
||||
const select = document.createElement('select');
|
||||
select.className = 'form-filter datatable-select'; // Use a different class to avoid auto-init by custom-select.js
|
||||
|
||||
// Default Option
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.value = '';
|
||||
defaultOption.text = config.label;
|
||||
select.appendChild(defaultOption);
|
||||
|
||||
Array.from(values).sort().forEach(val => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = val;
|
||||
opt.text = val;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
// Event Listener
|
||||
select.addEventListener('change', (e) => {
|
||||
const val = e.target.value;
|
||||
if (val === '') {
|
||||
delete this.activeFilters[colIndex];
|
||||
} else {
|
||||
this.activeFilters[colIndex] = val;
|
||||
}
|
||||
this.currentPage = 1;
|
||||
this.filterRows();
|
||||
this.render();
|
||||
});
|
||||
|
||||
container.appendChild(select);
|
||||
|
||||
if (typeof CustomSelect !== 'undefined') {
|
||||
new CustomSelect(select);
|
||||
}
|
||||
}
|
||||
|
||||
handleSearch(query) {
|
||||
this.searchQuery = query.toLowerCase();
|
||||
this.currentPage = 1;
|
||||
this.filterRows();
|
||||
this.render();
|
||||
}
|
||||
|
||||
filterRows() {
|
||||
this.filteredRows = this.originalRows.filter(row => {
|
||||
// 1. Text Search
|
||||
let matchesSearch = true;
|
||||
if (this.searchQuery) {
|
||||
const text = row.textContent.toLowerCase();
|
||||
matchesSearch = text.includes(this.searchQuery);
|
||||
}
|
||||
|
||||
// 2. Column Filters
|
||||
let matchesFilters = true;
|
||||
for (const [colIndex, filterValue] of Object.entries(this.activeFilters)) {
|
||||
const cell = row.cells[colIndex];
|
||||
if (!cell) {
|
||||
matchesFilters = false;
|
||||
break;
|
||||
}
|
||||
// Exact match (trimmed)
|
||||
if (cell.textContent.trim() !== filterValue) {
|
||||
matchesFilters = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return matchesSearch && matchesFilters;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
// Calculate pagination
|
||||
const totalItems = this.filteredRows.length;
|
||||
const totalPages = Math.ceil(totalItems / this.options.itemsPerPage);
|
||||
|
||||
// Ensure current page is valid
|
||||
if (this.currentPage > totalPages) this.currentPage = totalPages || 1;
|
||||
if (this.currentPage < 1) this.currentPage = 1;
|
||||
|
||||
const start = (this.currentPage - 1) * this.options.itemsPerPage;
|
||||
const end = start + this.options.itemsPerPage;
|
||||
const currentItems = this.filteredRows.slice(start, end);
|
||||
|
||||
// Clear and Re-append rows
|
||||
this.tbody.innerHTML = '';
|
||||
if (currentItems.length > 0) {
|
||||
currentItems.forEach(row => this.tbody.appendChild(row));
|
||||
} else {
|
||||
// Empty State
|
||||
const emptyRow = document.createElement('tr');
|
||||
const noMatchText = window.i18n ? window.i18n.t('common.table.no_match') : 'No match found.';
|
||||
emptyRow.innerHTML = `
|
||||
<td colspan="100%" class="px-6 py-12 text-center text-accents-5">
|
||||
<span class="text-sm">${noMatchText}</span>
|
||||
</td>
|
||||
`;
|
||||
this.tbody.appendChild(emptyRow);
|
||||
}
|
||||
|
||||
// Render Pagination
|
||||
if (this.options.pagination) {
|
||||
this.renderPagination(totalItems, totalPages, start + 1, Math.min(end, totalItems));
|
||||
}
|
||||
|
||||
// Re-initialize icons if Lucide is available
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
renderPagination(totalItems, totalPages, start, end) {
|
||||
if (totalItems === 0) {
|
||||
this.paginationContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const showingText = window.i18n ? window.i18n.t('common.table.showing', {start, end, total: totalItems}) : `Showing ${start} to ${end} of ${totalItems}`;
|
||||
const previousText = window.i18n ? window.i18n.t('common.previous') : 'Previous';
|
||||
const nextText = window.i18n ? window.i18n.t('common.next') : 'Next';
|
||||
const pageText = window.i18n ? window.i18n.t('common.page_of', {current: this.currentPage, total: totalPages}) : `Page ${this.currentPage} of ${totalPages}`;
|
||||
|
||||
this.paginationContainer.innerHTML = `
|
||||
<div class="text-sm text-accents-5">
|
||||
${showingText}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn-prev btn btn-secondary py-1 px-3 text-xs disabled:opacity-50 disabled:cursor-not-allowed" ${this.currentPage === 1 ? 'disabled' : ''}>
|
||||
${previousText}
|
||||
</button>
|
||||
<div class="text-sm font-medium">${pageText}</div>
|
||||
<button class="btn-next btn btn-secondary py-1 px-3 text-xs disabled:opacity-50 disabled:cursor-not-allowed" ${this.currentPage === totalPages ? 'disabled' : ''}>
|
||||
${nextText}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.paginationContainer.querySelector('.btn-prev').addEventListener('click', () => {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
this.paginationContainer.querySelector('.btn-next').addEventListener('click', () => {
|
||||
if (this.currentPage < totalPages) {
|
||||
this.currentPage++;
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export if using modules, otherwise it's global
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = SimpleDataTable;
|
||||
}
|
||||
93
public/assets/js/i18n.js
Normal file
@@ -0,0 +1,93 @@
|
||||
class I18n {
|
||||
constructor() {
|
||||
this.currentLang = localStorage.getItem('mivo_lang') || 'en';
|
||||
this.translations = {};
|
||||
this.isLoaded = false;
|
||||
// The ready promise resolves after the first language load
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadLanguage(this.currentLang);
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
async loadLanguage(lang) {
|
||||
try {
|
||||
// Add cache busting to ensure fresh translation files
|
||||
const cacheBuster = Date.now();
|
||||
const response = await fetch(`/lang/${lang}.json?v=${cacheBuster}`);
|
||||
if (!response.ok) throw new Error(`Failed to load language: ${lang}`);
|
||||
|
||||
this.translations = await response.json();
|
||||
this.currentLang = lang;
|
||||
localStorage.setItem('mivo_lang', lang);
|
||||
this.applyTranslations();
|
||||
|
||||
// Dispatch event for other components
|
||||
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } }));
|
||||
|
||||
// Update html lang attribute
|
||||
document.documentElement.lang = lang;
|
||||
} catch (error) {
|
||||
console.error('I18n Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
applyTranslations() {
|
||||
document.querySelectorAll('[data-i18n]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n');
|
||||
const translation = this.getNestedValue(this.translations, key);
|
||||
|
||||
if (translation) {
|
||||
if (element.tagName === 'INPUT' && element.getAttribute('placeholder')) {
|
||||
element.placeholder = translation;
|
||||
} else {
|
||||
// Check if element has child nodes that are not text (e.g. icons)
|
||||
// If simple text, just replace
|
||||
// If complex, try to preserve icon?
|
||||
// For now, let's assume strictly text replacement or user wraps text in span
|
||||
// Better approach: Look for a text node?
|
||||
// Simplest for now: innerText
|
||||
element.textContent = translation;
|
||||
}
|
||||
} else {
|
||||
// Log missing translation for developers (only if fully loaded)
|
||||
if (this.isLoaded) {
|
||||
console.warn(`[i18n] Missing translation for key: "${key}" (lang: ${this.currentLang})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getNestedValue(obj, path) {
|
||||
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
||||
}
|
||||
|
||||
t(key, params = {}) {
|
||||
let text = this.getNestedValue(this.translations, key);
|
||||
|
||||
if (!text) {
|
||||
if (this.isLoaded) {
|
||||
console.warn(`[i18n] Missing translation for key: "${key}" (lang: ${this.currentLang})`);
|
||||
}
|
||||
text = key; // Fallback to key
|
||||
}
|
||||
|
||||
// Simple interpolation: {key}
|
||||
if (params) {
|
||||
Object.keys(params).forEach(param => {
|
||||
text = text.replace(new RegExp(`{${param}}`, 'g'), params[param]);
|
||||
});
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
window.i18n = new I18n();
|
||||
|
||||
// Global helper
|
||||
function changeLanguage(lang) {
|
||||
window.i18n.loadLanguage(lang);
|
||||
}
|
||||
2
public/assets/js/jquery.min.js
vendored
Normal file
12
public/assets/js/lucide.min.js
vendored
Normal file
6
public/assets/js/qrious.min.js
vendored
Normal file
98
public/assets/js/router-form.js
Normal file
@@ -0,0 +1,98 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const checkBtn = document.getElementById('check-interface-btn');
|
||||
const ifaceSelect = document.getElementById('iface');
|
||||
|
||||
if (checkBtn && ifaceSelect) {
|
||||
checkBtn.addEventListener('click', async () => {
|
||||
const originalText = checkBtn.innerHTML;
|
||||
checkBtn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 mr-2 animate-spin"></i> Checking...';
|
||||
checkBtn.disabled = true;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
// Collect Data
|
||||
const ip = document.querySelector('input[name="ipmik"]').value;
|
||||
const user = document.querySelector('input[name="usermik"]').value;
|
||||
const pass = document.querySelector('input[name="passmik"]').value;
|
||||
const idInput = document.querySelector('input[name="id"]');
|
||||
const id = idInput ? idInput.value : null;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/router/interfaces', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ip, user, password: pass, id })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.interfaces) {
|
||||
// Update Select
|
||||
ifaceSelect.innerHTML = ''; // Clear
|
||||
|
||||
data.interfaces.forEach(iface => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = iface;
|
||||
opt.textContent = iface;
|
||||
if (iface === 'ether1') opt.selected = true; // Default preferred?
|
||||
ifaceSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
// Refresh Custom Select
|
||||
if (typeof CustomSelect !== 'undefined' && CustomSelect.instances) {
|
||||
const instance = CustomSelect.instances.find(i => i.originalSelect.id === 'iface');
|
||||
if (instance) instance.refresh();
|
||||
}
|
||||
|
||||
// Show success
|
||||
checkBtn.innerHTML = '<i data-lucide="check" class="w-4 h-4 mr-2"></i> Interfaces Loaded';
|
||||
setTimeout(() => {
|
||||
checkBtn.innerHTML = originalText;
|
||||
checkBtn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Failed to fetch interfaces'));
|
||||
checkBtn.innerHTML = originalText;
|
||||
checkBtn.disabled = false;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Connection Error');
|
||||
checkBtn.innerHTML = originalText;
|
||||
checkBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Session Name Auto-Conversion
|
||||
const sessInput = document.querySelector('input[name="sessname"]');
|
||||
const sessPreview = document.getElementById('sessname-preview');
|
||||
|
||||
if (sessInput) {
|
||||
// Initial set if editing
|
||||
if(sessPreview) sessPreview.textContent = sessInput.value;
|
||||
|
||||
sessInput.addEventListener('input', (e) => {
|
||||
let val = e.target.value;
|
||||
// 1. Lowercase
|
||||
val = val.toLowerCase();
|
||||
// 2. Space -> Dash
|
||||
val = val.replace(/\s+/g, '-');
|
||||
// 3. Remove non-alphanumeric (except dash)
|
||||
val = val.replace(/[^a-z0-9-]/g, '');
|
||||
// 4. No double dashes
|
||||
val = val.replace(/-+/g, '-');
|
||||
|
||||
// Write back to input (Auto Convert)
|
||||
e.target.value = val;
|
||||
|
||||
// Update Preview
|
||||
if (sessPreview) {
|
||||
sessPreview.textContent = val || '...';
|
||||
sessPreview.className = val ? 'font-mono text-primary font-bold' : 'font-mono text-accents-4';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
2
public/assets/js/sweetalert2.all.min.js
vendored
Normal file
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 550 KiB |
72
public/index.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
use App\Core\Router;
|
||||
|
||||
// Start Output Buffering
|
||||
ob_start();
|
||||
|
||||
// Define Root Path
|
||||
define('ROOT', dirname(__DIR__));
|
||||
|
||||
// Handle Static Files for PHP Built-in Server
|
||||
if (php_sapi_name() === 'cli-server') {
|
||||
$url = parse_url($_SERVER['REQUEST_URI']);
|
||||
$file = __DIR__ . $url['path'];
|
||||
if (is_file($file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Start Session
|
||||
session_start();
|
||||
|
||||
// Manual require for the Autoloader class since it can't autoload itself
|
||||
require_once ROOT . '/app/Core/Autoloader.php';
|
||||
\App\Core\Autoloader::register();
|
||||
|
||||
// Load Environment Variables
|
||||
\App\Core\Env::load(ROOT . '/.env');
|
||||
|
||||
// Initialize Router
|
||||
$router = new Router();
|
||||
|
||||
|
||||
// Global Error Handling for Dev Mode
|
||||
if (\App\Config\SiteConfig::IS_DEV) {
|
||||
// Catch Fatal Errors (Shutdown)
|
||||
register_shutdown_function(function() {
|
||||
$error = error_get_last();
|
||||
if ($error && ($error['type'] === E_ERROR || $error['type'] === E_PARSE || $error['type'] === E_CORE_ERROR || $error['type'] === E_COMPILE_ERROR)) {
|
||||
// Convert to exception format for our helper
|
||||
$e = new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']);
|
||||
\App\Helpers\ErrorHelper::showException($e);
|
||||
}
|
||||
});
|
||||
|
||||
// Catch Uncaught Exceptions
|
||||
set_exception_handler(function($e) {
|
||||
\App\Helpers\ErrorHelper::showException($e);
|
||||
});
|
||||
}
|
||||
|
||||
// Define Routes
|
||||
require_once ROOT . '/routes/web.php';
|
||||
require_once ROOT . '/routes/api.php';
|
||||
|
||||
// Dispatch
|
||||
// Dispatch
|
||||
try {
|
||||
$router->dispatch($_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD']);
|
||||
} catch (Exception $e) {
|
||||
if (\App\Config\SiteConfig::IS_DEV) {
|
||||
\App\Helpers\ErrorHelper::showException($e);
|
||||
} else {
|
||||
\App\Helpers\ErrorHelper::show(500, 'Internal Server Error', $e->getMessage());
|
||||
}
|
||||
} catch (Error $e) {
|
||||
if (\App\Config\SiteConfig::IS_DEV) {
|
||||
\App\Helpers\ErrorHelper::showException($e);
|
||||
} else {
|
||||
\App\Helpers\ErrorHelper::show(500, 'System Error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
544
public/lang/en.json
Normal file
@@ -0,0 +1,544 @@
|
||||
{
|
||||
"_meta": {
|
||||
"name": "English",
|
||||
"flag": "gb"
|
||||
},
|
||||
"common": {
|
||||
"dashboard": "Dashboard",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
"back": "Back",
|
||||
"actions": "Actions",
|
||||
"status": "Status",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"search": "Search",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"confirm_delete": "Are you sure you want to delete this item?",
|
||||
"page_of": "Page {current} of {total}",
|
||||
"all_topics": "All Topics",
|
||||
"open": "Open",
|
||||
"session": "Session",
|
||||
"table": {
|
||||
"entries_per_page": "entries per page",
|
||||
"search_placeholder": "Search...",
|
||||
"all": "All",
|
||||
"showing": "Showing {start} to {end} of {total} entries",
|
||||
"no_match": "No matching records found"
|
||||
},
|
||||
"char_length": "{n} Characters",
|
||||
"forms": {
|
||||
"general": "General",
|
||||
"required": "required",
|
||||
"save_changes": "Save Changes",
|
||||
"please_wait": "Please wait...",
|
||||
"none": "none",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"subtitle": "A modern, lightweight MikroTik Hotspot Manager built for performance and simplicity.",
|
||||
"manage_routers": "Manage Routers",
|
||||
"manage_routers_desc": "Configure RouterOS connections and view status.",
|
||||
"source_code": "Source Code",
|
||||
"source_code_desc": "View the project repository and contribute.",
|
||||
"quick_access": "Quick Access",
|
||||
"session_name": "Session Name",
|
||||
"hotspot_name": "Hotspot Name",
|
||||
"ip_address": "IP Address"
|
||||
},
|
||||
"dashboard": {
|
||||
"system_info": "System Info",
|
||||
"model": "Model",
|
||||
"board_name": "Board Name",
|
||||
"router_os": "RouterOS",
|
||||
"architecture": "Architecture",
|
||||
"uptime": "Uptime",
|
||||
"resources": "Resources",
|
||||
"cpu_load": "CPU Load",
|
||||
"memory": "Memory",
|
||||
"free": "Free",
|
||||
"hdd": "HDD",
|
||||
"income_today": "Income Today",
|
||||
"traffic_monitor": "Traffic Monitor",
|
||||
"rx_download": "Rx (Download)",
|
||||
"tx_upload": "Tx (Upload)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"subtitle": "Manage your application preferences and configurations.",
|
||||
"system": "General",
|
||||
"system_desc": "System-wide configurations and security.",
|
||||
"admin_username": "Admin Username",
|
||||
"admin_username_desc": "For security reasons, the administrator username cannot be changed.",
|
||||
"change_password": "Change Password",
|
||||
"new_password_placeholder": "Enter new password",
|
||||
"update_password": "Update Password",
|
||||
"quick_print_mode": "Quick Print Mode",
|
||||
"quick_print_mode_desc": "Enable direct printing for voucher generation.",
|
||||
"save_global": "Save Global Settings",
|
||||
"data_management": "Data Management",
|
||||
"data_management_desc": "Backup or restore your application data.",
|
||||
"backup_data": "Backup Data",
|
||||
"backup_data_desc": "Download a configuration file (.mivo) containing your database and settings.",
|
||||
"download_backup": "Download Backup",
|
||||
"restore_data": "Restore Data",
|
||||
"restore_data_desc": "Upload a previously backup file (.mivo). Overwrites or adds to existing data.",
|
||||
"restore": "Restore",
|
||||
"warning_restore": "WARNING: This will restore settings from the file and may overwrite existing data. Continue?",
|
||||
"routers_title": "Routers",
|
||||
"routers_subtitle": "Manage your MikroTik RouterOS connections.",
|
||||
"add_router": "Add Router",
|
||||
"no_routers": "No Routers Found",
|
||||
"connect_first": "Connect your first MikroTik router to start managing hotspots and vouchers.",
|
||||
"delete_router_title": "Delete Session?",
|
||||
"delete_router_confirm": "Are you sure you want to delete <strong>{name}</strong>? This action cannot be undone.",
|
||||
"connect": "Connect",
|
||||
"logos_title": "Logos",
|
||||
"logos_subtitle": "Upload and manage logos for your hotspots and vouchers.",
|
||||
"upload_new_logo": "Upload New Logo",
|
||||
"drag_drop": "Drag and drop or click to select file",
|
||||
"supports_formats": "Supports PNG, JPG, SVG, GIF",
|
||||
"no_logos": "No logos uploaded yet.",
|
||||
"id_copied": "ID Copied",
|
||||
"logo_id_copied_desc": "Logo ID <strong>{id}</strong> copied to clipboard.",
|
||||
"delete_logo_title": "Delete Logo?",
|
||||
"delete_logo_confirm": "Are you sure you want to delete logo <strong>{id}</strong>?",
|
||||
"templates_title": "Voucher Templates",
|
||||
"templates_subtitle": "Manage and customize your voucher print designs.",
|
||||
"new_template": "New Template",
|
||||
"edit_template": "Edit Template",
|
||||
"default_template": "Default Template",
|
||||
"system_label": "System",
|
||||
"custom_label": "Custom",
|
||||
"built_in": "Built-in",
|
||||
"default_template_desc": "Standard thermal printer friendly template.",
|
||||
"delete_template_title": "Delete Template?",
|
||||
"delete_template_confirm": "Are you sure you want to delete <strong>{name}</strong>?",
|
||||
"template_name": "Template Name",
|
||||
"html_source": "HTML Source",
|
||||
"docs": "Docs",
|
||||
"live_preview": "Live Preview",
|
||||
"template_variables": "Template Variables",
|
||||
"variables_desc": "Use these variables in your HTML source. They will be replaced with actual user data during printing.",
|
||||
"qr_code": "QR Code",
|
||||
"qr_desc": "Generates a QR Code containing the Login URL with username and password.",
|
||||
"custom_attributes": "Custom Attributes",
|
||||
"examples": "Examples",
|
||||
"api_cors_title": "API CORS",
|
||||
"api_cors_subtitle": "Manage Cross-Origin Resource Sharing for API access.",
|
||||
"add_rule": "Add CORS Rule",
|
||||
"edit_rule": "Edit CORS Rule",
|
||||
"origin": "Origin",
|
||||
"methods": "Allowed Methods",
|
||||
"headers": "Allowed Headers",
|
||||
"max_age": "Max Age (seconds)"
|
||||
},
|
||||
"login": {
|
||||
"welcome": "Welcome back, please sign in to continue.",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"sign_in": "Sign In"
|
||||
},
|
||||
"quick_print": {
|
||||
"title": "Quick Print",
|
||||
"subtitle": "Instant voucher generation and printing.",
|
||||
"manage": "Manage Packages",
|
||||
"manage_title": "Manage Packages",
|
||||
"manage_subtitle": "Configure your Quick Print voucher packages for:",
|
||||
"no_packages": "No Packages Found",
|
||||
"no_packages_found": "No packages found.",
|
||||
"create_first": "Create a Quick Print package to get started.",
|
||||
"create_package": "Create Package",
|
||||
"add_package": "Add Package",
|
||||
"edit_package": "Edit Package",
|
||||
"save_package": "Save Package",
|
||||
"delete_package": "Delete Package",
|
||||
"print_voucher": "Print Voucher",
|
||||
"name": "Name",
|
||||
"package_name": "Package Name",
|
||||
"profile": "Profile",
|
||||
"select_profile": "Select Profile",
|
||||
"prefix": "Prefix",
|
||||
"server": "Server",
|
||||
"price": "Price",
|
||||
"selling_price": "Selling Price",
|
||||
"time_limit": "Time Limit",
|
||||
"data_limit": "Data Limit",
|
||||
"card_color": "Card Color",
|
||||
"char_length": "Char Length",
|
||||
"vouchers_to_generate": "Vouchers to Generate",
|
||||
"comment": "Comment"
|
||||
},
|
||||
"sidebar": {
|
||||
"dashboard": "Dashboard",
|
||||
"quick_print": "Quick Print",
|
||||
"hotspot": "Hotspot",
|
||||
"status": "Status",
|
||||
"security": "Security",
|
||||
"reports": "Reports",
|
||||
"network": "Network",
|
||||
"system": "System",
|
||||
"settings": "Settings",
|
||||
"templates": "Templates",
|
||||
"disconnect": "Disconnect",
|
||||
"logout": "Logout from Mivo"
|
||||
},
|
||||
"hotspot_active": {
|
||||
"title": "Active Users",
|
||||
"subtitle": "Monitor currently active hotspot sessions",
|
||||
"filter_server": "All Servers",
|
||||
"server": "Server",
|
||||
"user": "User",
|
||||
"address": "Address / MAC",
|
||||
"uptime": "Uptime / Left",
|
||||
"bytes_in_out": "Bytes In/Out",
|
||||
"time_left": "Left",
|
||||
"remove": "Disconnect User?"
|
||||
},
|
||||
"hotspot_hosts": {
|
||||
"title": "Hotspot Hosts",
|
||||
"subtitle": "Devices connected to the hotspot network for:",
|
||||
"mac": "MAC Address",
|
||||
"address": "Address",
|
||||
"to_address": "To Address",
|
||||
"server": "Server",
|
||||
"comment": "Comment"
|
||||
},
|
||||
"hotspot_menu": {
|
||||
"users": "Users",
|
||||
"profiles": "User Profiles",
|
||||
"generate": "Generate",
|
||||
"cookies": "Cookies",
|
||||
"active": "Active",
|
||||
"hosts": "Hosts",
|
||||
"bindings": "IP Bindings",
|
||||
"walled_garden": "Walled Garden"
|
||||
},
|
||||
"reports_menu": {
|
||||
"resume": "Resume",
|
||||
"selling": "Selling Report",
|
||||
"user_log": "User Log"
|
||||
},
|
||||
"network_menu": {
|
||||
"dhcp": "DHCP Leases"
|
||||
},
|
||||
"system_menu": {
|
||||
"scheduler": "Scheduler",
|
||||
"reboot": "Reboot",
|
||||
"shutdown": "Shutdown"
|
||||
},
|
||||
"dhcp": {
|
||||
"title": "DHCP Leases",
|
||||
"subtitle": "Active DHCP leases for:",
|
||||
"all_servers": "All Servers",
|
||||
"address": "Address",
|
||||
"mac": "MAC Address",
|
||||
"server": "Server",
|
||||
"status": "Status",
|
||||
"host": "Host Name"
|
||||
},
|
||||
"cookies": {
|
||||
"title": "Hotspot Cookies",
|
||||
"subtitle": "Active authentication cookies for:",
|
||||
"user": "User",
|
||||
"mac": "MAC Address",
|
||||
"ip": "IP Address",
|
||||
"expires": "Expires In",
|
||||
"remove_cookie": "Remove Cookie?",
|
||||
"remove_confirm": "Are you sure you want to remove the cookie for {user}?"
|
||||
},
|
||||
"reports": {
|
||||
"selling_title": "Selling Report",
|
||||
"selling_subtitle": "Sales summary and details for:",
|
||||
"user_log_title": "User Log",
|
||||
"user_log_subtitle": "Login and logout history for:",
|
||||
"resume_title": "Resume Report",
|
||||
"resume_subtitle": "Overview of aggregated income.",
|
||||
"total_income": "Total Income",
|
||||
"total_vouchers": "Total Vouchers Sold",
|
||||
"date_batch": "Date / Batch (Comment)",
|
||||
"qty": "Qty",
|
||||
"total": "Total",
|
||||
"no_data": "No sales data found.",
|
||||
"time": "Time",
|
||||
"topics": "Topics",
|
||||
"message": "Message",
|
||||
"daily": "Daily",
|
||||
"monthly": "Monthly",
|
||||
"yearly": "Yearly",
|
||||
"date": "Date",
|
||||
"month": "Month",
|
||||
"year": "Year",
|
||||
"refresh": "Refresh",
|
||||
"print_report": "Print Report"
|
||||
},
|
||||
"hotspot_profiles": {
|
||||
"title": "User Profiles",
|
||||
"subtitle": "Manage hotspot rate limits and pricing",
|
||||
"add_profile": "Add Profile",
|
||||
"edit_profile": "Edit Profile",
|
||||
"all_modes": "All Expired Modes",
|
||||
"name": "Name",
|
||||
"shared_users": "Shared Users",
|
||||
"rate_limit": "Rate Limit",
|
||||
"parent_queue": "Parent Queue",
|
||||
"expired_mode": "Expired Mode",
|
||||
"validity": "Validity",
|
||||
"price": "Price",
|
||||
"selling_price": "Selling Price",
|
||||
"lock_user": "Lock User",
|
||||
"form": {
|
||||
"add_title": "Add Profile",
|
||||
"edit_title": "Edit Profile",
|
||||
"add_subtitle": "Create a new hotspot user profile for: {name}",
|
||||
"edit_subtitle": "Edit hotspot user profile: {name}",
|
||||
"settings": "Profile Settings",
|
||||
"general": "General Settings",
|
||||
"limits_queues": "Limits & Queues",
|
||||
"pricing_validity": "Pricing & Validity",
|
||||
"address_pool": "Address Pool",
|
||||
"rate_limit_help": "Rx/Tx (Upload/Download). Example: 512k/1M",
|
||||
"expired_mode_help": "Action when validity expires.",
|
||||
"validity_help": "Days / Hours / Minutes",
|
||||
"lock_user_help": "Lock user to one specific MAC address.",
|
||||
"save": "Save Profile",
|
||||
"name_placeholder": "e.g. 1Hour-Package",
|
||||
"quick_tips": "Quick Tips",
|
||||
"tip_rate_limit": "<strong>Rate Limit</strong>: Rx/Tx (Upload/Download). Example: <code>512k/1M</code>",
|
||||
"tip_expired_mode": "<strong>Expired Mode</strong>: Select 'Remove' or 'Notice' to enable Validity.",
|
||||
"tip_parent_queue": "<strong>Parent Queue</strong>: Assigns users to a specific parent queue for bandwidth management."
|
||||
}
|
||||
},
|
||||
"hotspot_users": {
|
||||
"form": {
|
||||
"add_title": "Add User",
|
||||
"edit_title": "Edit User",
|
||||
"subtitle": "Hotspot user account",
|
||||
"edit_subtitle": "Update user details for: {name}",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"profile": "Profile",
|
||||
"server": "Server",
|
||||
"time_limit": "Time Limit",
|
||||
"data_limit": "Data Limit",
|
||||
"comment": "Comment",
|
||||
"save": "Save User",
|
||||
"username_help": "Unique username for login.",
|
||||
"password_help": "Strong password for security.",
|
||||
"profile_help": "Profile determines speed limit and shared user policy.",
|
||||
"time_limit_help": "Total allowed uptime (Days, Hours, Minutes).",
|
||||
"data_limit_help": "Limit data usage (0 for unlimited).",
|
||||
"comment_help": "Additional notes or contact info.",
|
||||
"username_placeholder": "e.g. voucher123",
|
||||
"password_placeholder": "e.g. 123456",
|
||||
"comment_placeholder": "Optional note for this user",
|
||||
"quick_tips": "Quick Tips",
|
||||
"tip_profiles": "<strong>Profiles</strong> define the default speed limits, session timeout, and shared users policy.",
|
||||
"tip_time_limit": "<strong>Time Limit</strong> is the total accumulated uptime allowed for this user.",
|
||||
"tip_data_limit": "<strong>Data Limit</strong> will override the profile's data limit settings if specified here. Set to 0 to use profile default."
|
||||
}
|
||||
},
|
||||
"hotspot_generate": {
|
||||
"title": "Generate Vouchers",
|
||||
"form": {
|
||||
"qty": "Quantity",
|
||||
"server": "Server",
|
||||
"user_mode": "User Mode",
|
||||
"user_length": "User Length",
|
||||
"prefix": "Prefix",
|
||||
"characters": "Characters",
|
||||
"profile": "Profile",
|
||||
"time_limit": "Time Limit",
|
||||
"data_limit": "Data Limit",
|
||||
"comment": "Comment",
|
||||
"generate": "Generate Vouchers",
|
||||
"subtitle": "Create multiple hotspot vouchers in batch for: {name}",
|
||||
"batch_settings": "Batch Generation Settings",
|
||||
"core_config": "Core Config",
|
||||
"qty_help": "Count of vouchers to generate.",
|
||||
"server_help": "Target Hotspot Instance.",
|
||||
"user_mode_help": "Login credential format.",
|
||||
"comment_help": "Note for this batch.",
|
||||
"user_format": "User Format",
|
||||
"name_length_help": "Length of username/password.",
|
||||
"prefix_placeholder": "e.g. VIP-",
|
||||
"prefix_help": "Prefix for generated usernames.",
|
||||
"characters_help": "Character types to include.",
|
||||
"limits_profile": "Limits & Profile",
|
||||
"profile_help": "Apply speed limits from profile.",
|
||||
"time_limit_help": "Max uptime (e.g. 1h, 30m).",
|
||||
"data_limit_help": "Max data transfer (MB).",
|
||||
"quick_tips": "Quick Tips",
|
||||
"tip_user_mode": "<strong>User Mode</strong>: UP (separate), VC (same).",
|
||||
"tip_format_examples": "<strong>Format Examples</strong>: abcd (lower), 1234 (num), Mix (upper/lower/num).",
|
||||
"tip_limits": "<strong>Limits</strong>: Time (e.g. 1h, 30m), Data (e.g. 100MB). Leave empty to use Profile default."
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"bindings": {
|
||||
"title": "IP Bindings",
|
||||
"subtitle": "Manage IP bindings (bypass/blocked) for: {name}",
|
||||
"all_types": "All Types",
|
||||
"regular": "Regular",
|
||||
"bypassed": "Bypassed",
|
||||
"blocked": "Blocked",
|
||||
"table": {
|
||||
"mac": "MAC Address",
|
||||
"address": "Address",
|
||||
"to_address": "To Address",
|
||||
"type": "Type",
|
||||
"comment": "Comment"
|
||||
},
|
||||
"form": {
|
||||
"add_title": "Add Binding",
|
||||
"mac_address": "MAC Address",
|
||||
"address": "Address",
|
||||
"to_address": "To Address",
|
||||
"type": "Type",
|
||||
"server": "Server",
|
||||
"comment": "Comment",
|
||||
"mac_help": "Target device MAC address.",
|
||||
"address_help": "Target IP address (optional).",
|
||||
"to_address_help": "Translate to this IP (optional).",
|
||||
"server_help": "Apply to specific Hotspot server.",
|
||||
"comment_help": "Note for this binding.",
|
||||
"save": "Save & Bind",
|
||||
"tip_bypassed": "<strong>Bypassed</strong>: Access without login.",
|
||||
"tip_blocked": "<strong>Blocked</strong>: Deny access completely.",
|
||||
"tip_regular": "<strong>Regular</strong>: Normal hotspot client."
|
||||
}
|
||||
},
|
||||
"walled_garden": {
|
||||
"title": "Walled Garden",
|
||||
"subtitle": "Manage allowed destinations (bypass without login) for: {name}",
|
||||
"all_actions": "All Actions",
|
||||
"allow": "Allow",
|
||||
"deny": "Deny",
|
||||
"table": {
|
||||
"host_ip": "Dst. Host / IP",
|
||||
"proto_port": "Protocol / Port",
|
||||
"action": "Action",
|
||||
"comment": "Comment"
|
||||
},
|
||||
"form": {
|
||||
"add_title": "Add Entry",
|
||||
"action": "Action",
|
||||
"dst_host": "Dst. Host (Domain)",
|
||||
"dst_address": "Dst. Address (IP)",
|
||||
"protocol": "Protocol",
|
||||
"dst_port": "Dst. Port",
|
||||
"server": "Server",
|
||||
"comment": "Comment",
|
||||
"host_help": "Domain to allow (wildcard supported).",
|
||||
"addr_help": "Destination IP Address.",
|
||||
"action_help": "Allow (bypass) or Deny access.",
|
||||
"server_help": "Apply to specific Hotspot server.",
|
||||
"comment_help": "Note for this rule.",
|
||||
"save": "Save Entry",
|
||||
"tip_host": "<strong>Dst. Host</strong>: Domain name (e.g. <code>*.google.com</code>).",
|
||||
"tip_ip": "<strong>Dst. IP</strong>: Specific IP address.",
|
||||
"tip_action": "<strong>Action</strong>: Allow to bypass auth."
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_tools": {
|
||||
"scheduler_subtitle": "Manage RouterOS automated tasks for:",
|
||||
"add_task": "Add Task",
|
||||
"add_title": "Add Scheduler Task",
|
||||
"edit_title": "Edit Scheduler Task",
|
||||
"save_task": "Save Task",
|
||||
"update_task": "Update Task",
|
||||
"delete_task": "Delete Task",
|
||||
"table_name": "Name",
|
||||
"name": "Name",
|
||||
"interval": "Interval",
|
||||
"next_run": "Next Run",
|
||||
"status": "Status",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"start_date": "Start Date",
|
||||
"start_time": "Start Time",
|
||||
"on_event": "On Event (Script)",
|
||||
"comment": "Comment"
|
||||
},
|
||||
"toasts": {
|
||||
"profile_created": "Profile Created",
|
||||
"profile_created_desc": "User profile \"{name}\" has been created successfully.",
|
||||
"profile_updated": "Profile Updated",
|
||||
"profile_updated_desc": "Changes to profile \"{name}\" have been saved.",
|
||||
"profile_deleted": "Profile Deleted",
|
||||
"profile_deleted_desc": "The user profile has been removed.",
|
||||
"user_added": "User Added",
|
||||
"user_added_desc": "Hotspot user \"{name}\" has been created.",
|
||||
"user_deleted": "User(s) Deleted",
|
||||
"user_deleted_desc": "The selected hotspot user(s) have been removed.",
|
||||
"user_updated": "User Updated",
|
||||
"user_updated_desc": "Changes to user \"{name}\" have been saved.",
|
||||
"session_removed": "Session Removed",
|
||||
"session_removed_desc": "The active hotspot session has been terminated.",
|
||||
"binding_added": "Binding Added",
|
||||
"binding_added_desc": "IP Binding has been created successfully.",
|
||||
"binding_removed": "Binding Removed",
|
||||
"binding_removed_desc": "IP Binding has been removed.",
|
||||
"rule_added": "Rule Added",
|
||||
"rule_added_desc": "Walled Garden rule has been added.",
|
||||
"rule_removed": "Rule Removed",
|
||||
"rule_removed_desc": "Walled Garden rule has been removed.",
|
||||
"cookie_removed": "Cookie Removed",
|
||||
"cookie_removed_desc": "Hotspot cookie has been cleared.",
|
||||
"package_saved": "Package Saved",
|
||||
"package_saved_desc": "Quick print package has been updated.",
|
||||
"package_deleted": "Package Deleted",
|
||||
"package_deleted_desc": "Quick print package has been removed.",
|
||||
"vouchers_generated": "Vouchers Generated",
|
||||
"vouchers_generated_desc": "{qty} users have been created successfully.",
|
||||
"schedule_added": "Schedule Added",
|
||||
"schedule_added_desc": "The task has been scheduled.",
|
||||
"schedule_updated": "Schedule Updated",
|
||||
"schedule_updated_desc": "Changes to the task have been saved.",
|
||||
"schedule_deleted": "Schedule Deleted",
|
||||
"schedule_deleted_desc": "The scheduled task has been removed.",
|
||||
"router_added": "Router Added",
|
||||
"router_added_desc": "Session {name} has been created.",
|
||||
"router_updated": "Router Updated",
|
||||
"router_updated_desc": "Settings for {name} have been saved.",
|
||||
"router_deleted": "Router Deleted",
|
||||
"router_deleted_desc": "The session has been removed.",
|
||||
"password_updated": "Password Updated",
|
||||
"password_updated_desc": "Administrator password has been changed.",
|
||||
"settings_saved": "Settings Saved",
|
||||
"settings_saved_desc": "Global preferences have been updated.",
|
||||
"restore_success": "Restore Successful",
|
||||
"restore_success_desc": "Sessions and settings have been restored from backup.",
|
||||
"restore_failed": "Restore Failed",
|
||||
"no_file_selected": "No backup file selected.",
|
||||
"invalid_file_type": "Invalid file type. Please upload a .mivo or .json file.",
|
||||
"invalid_file_type_mivo": "Invalid file type. Please upload a .mivo file.",
|
||||
"file_empty": "The uploaded file is empty.",
|
||||
"file_corrupted": "The file is corrupted or not a valid Mivo backup.",
|
||||
"logo_uploaded": "Logo Uploaded",
|
||||
"logo_uploaded_desc": "New logo has been added successfully.",
|
||||
"logo_deleted": "Logo Deleted",
|
||||
"logo_deleted_desc": "The logo has been removed.",
|
||||
"template_created": "Template Created",
|
||||
"template_created_desc": "Voucher template \"{name}\" has been added.",
|
||||
"template_updated": "Template Updated",
|
||||
"template_updated_desc": "Changes to \"{name}\" have been saved.",
|
||||
"template_deleted": "Template Deleted",
|
||||
"template_deleted_desc": "The template has been removed.",
|
||||
"test_alert": "Mivo Alert Test",
|
||||
"test_alert_desc": "If you see this, the new alert system is working perfectly! 🚀",
|
||||
"cors_rule_added": "CORS Rule Added",
|
||||
"cors_rule_added_desc": "The CORS rule for {origin} has been created.",
|
||||
"cors_rule_updated": "CORS Rule Updated",
|
||||
"cors_rule_updated_desc": "Changes to CORS rule for {origin} have been saved.",
|
||||
"cors_rule_deleted": "CORS Rule Deleted",
|
||||
"cors_rule_deleted_desc": "The CORS rule has been removed."
|
||||
}
|
||||
}
|
||||
554
public/lang/id.json
Normal file
@@ -0,0 +1,554 @@
|
||||
{
|
||||
"_meta": {
|
||||
"name": "Bahasa Indonesia",
|
||||
"flag": "id"
|
||||
},
|
||||
"common": {
|
||||
"dashboard": "Dashboard",
|
||||
"save": "Simpan",
|
||||
"cancel": "Batal",
|
||||
"delete": "Hapus",
|
||||
"edit": "Edit",
|
||||
"add": "Tambah",
|
||||
"back": "Kembali",
|
||||
"actions": "Aksi",
|
||||
"status": "Status",
|
||||
"name": "Nama",
|
||||
"description": "Deskripsi",
|
||||
"search": "Cari",
|
||||
"previous": "Sebelumnya",
|
||||
"next": "Selanjutnya",
|
||||
"confirm_delete": "Apakah Anda yakin ingin menghapus item ini?",
|
||||
"page_of": "Halaman {current} dari {total}",
|
||||
"all_topics": "Semua Topik",
|
||||
"open": "Buka",
|
||||
"session": "Sesi",
|
||||
"table": {
|
||||
"entries_per_page": "entri per halaman",
|
||||
"search_placeholder": "Cari...",
|
||||
"all": "Semua",
|
||||
"showing": "Menampilkan {start} sampai {end} dari {total} entri",
|
||||
"no_match": "Data tidak ditemukan"
|
||||
},
|
||||
"enabled": "Aktif",
|
||||
"disabled": "Nonaktif",
|
||||
"char_length": "{n} Karakter",
|
||||
"forms": {
|
||||
"general": "Umum",
|
||||
"required": "wajib",
|
||||
"save_changes": "Simpan Perubahan",
|
||||
"please_wait": "Mohon tunggu...",
|
||||
"none": "tidak ada"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"subtitle": "Hotspot Manager MikroTik modern dan ringan yang dirancang untuk performa dan kemudahan.",
|
||||
"manage_routers": "Kelola Router",
|
||||
"manage_routers_desc": "Konfigurasi koneksi RouterOS dan lihat status.",
|
||||
"source_code": "Kode Sumber",
|
||||
"source_code_desc": "Lihat repositori proyek dan berkontribusi.",
|
||||
"quick_access": "Akses Cepat",
|
||||
"session_name": "Nama Sesi",
|
||||
"hotspot_name": "Nama Hotspot",
|
||||
"ip_address": "Alamat IP"
|
||||
},
|
||||
"dashboard": {
|
||||
"system_info": "Info Sistem",
|
||||
"model": "Model",
|
||||
"board_name": "Nama Board",
|
||||
"router_os": "RouterOS",
|
||||
"architecture": "Arsitektur",
|
||||
"uptime": "Uptime",
|
||||
"resources": "Sumber Daya",
|
||||
"cpu_load": "Beban CPU",
|
||||
"memory": "Memori",
|
||||
"free": "Bebas",
|
||||
"hdd": "HDD",
|
||||
"income_today": "Pendapatan Hari Ini",
|
||||
"traffic_monitor": "Monitor Lalu Lintas",
|
||||
"rx_download": "Rx (Download)",
|
||||
"tx_upload": "Tx (Upload)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Pengaturan",
|
||||
"subtitle": "Kelola preferensi dan konfigurasi aplikasi Anda.",
|
||||
"system": "Umum",
|
||||
"system_desc": "Konfigurasi sistem dan keamanan.",
|
||||
"admin_username": "Username Admin",
|
||||
"admin_username_desc": "Untuk alasan keamanan, username administrator tidak dapat diubah.",
|
||||
"change_password": "Ganti Password",
|
||||
"new_password_placeholder": "Masukkan password baru",
|
||||
"update_password": "Perbarui Password",
|
||||
"quick_print_mode": "Mode Cetak Cepat",
|
||||
"quick_print_mode_desc": "Aktifkan pencetakan langsung untuk pembuatan voucher.",
|
||||
"save_global": "Simpan Pengaturan Global",
|
||||
"data_management": "Manajemen Data",
|
||||
"data_management_desc": "Cadangkan atau pulihkan data aplikasi Anda.",
|
||||
"backup_data": "Cadangkan Data",
|
||||
"backup_data_desc": "Unduh file konfigurasi (.mivo) yang berisi database dan pengaturan Anda.",
|
||||
"download_backup": "Unduh Cadangan",
|
||||
"restore_data": "Pulihkan Data",
|
||||
"restore_data_desc": "Unggah file cadangan (.mivo). Menimpa atau menambahkan data yang sudah ada.",
|
||||
"restore": "Pulihkan",
|
||||
"warning_restore": "PERINGATAN: Ini akan memulihkan pengaturan dari file dan dapat menimpa data yang ada. Lanjutkan?",
|
||||
"routers_title": "Router",
|
||||
"routers_subtitle": "Kelola koneksi MikroTik RouterOS Anda.",
|
||||
"add_router": "Tambah Router",
|
||||
"no_routers": "Tidak Ada Router Ditemukan",
|
||||
"connect_first": "Hubungkan router MikroTik pertama Anda untuk mulai mengelola hotspot dan voucher.",
|
||||
"delete_router_title": "Hapus Sesi?",
|
||||
"delete_router_confirm": "Apakah Anda yakin ingin menghapus <strong>{name}</strong>? Tindakan ini tidak dapat dibatalkan.",
|
||||
"connect": "Hubungkan",
|
||||
"logos_title": "Logo",
|
||||
"logos_subtitle": "Unggah dan kelola logo untuk hotspot dan voucher Anda.",
|
||||
"upload_new_logo": "Unggah Logo Baru",
|
||||
"drag_drop": "Seret dan lepas atau klik untuk memilih file",
|
||||
"supports_formats": "Mendukung PNG, JPG, SVG, GIF",
|
||||
"no_logos": "Belum ada logo yang diunggah.",
|
||||
"id_copied": "ID Disalin",
|
||||
"logo_id_copied_desc": "ID Logo <strong>{id}</strong> disalin ke papan klip.",
|
||||
"delete_logo_title": "Hapus Logo?",
|
||||
"delete_logo_confirm": "Apakah Anda yakin ingin menghapus logo <strong>{id}</strong>?",
|
||||
"templates_title": "Template Voucher",
|
||||
"templates_subtitle": "Kelola dan sesuaikan desain cetak voucher Anda.",
|
||||
"new_template": "Template Baru",
|
||||
"edit_template": "Edit Template",
|
||||
"default_template": "Template Default",
|
||||
"system_label": "Sistem",
|
||||
"custom_label": "Kustom",
|
||||
"built_in": "Bawaan",
|
||||
"default_template_desc": "Template ramah printer termal standar.",
|
||||
"delete_template_title": "Hapus Template?",
|
||||
"delete_template_confirm": "Apakah Anda yakin ingin menghapus <strong>{name}</strong>?",
|
||||
"template_name": "Nama Template",
|
||||
"html_source": "Sumber HTML",
|
||||
"docs": "Dokumentasi",
|
||||
"live_preview": "Pratinjau Langsung",
|
||||
"template_variables": "Variabel Template",
|
||||
"variables_desc": "Gunakan variabel ini dalam sumber HTML Anda. Variabel akan diganti dengan data user yang sebenarnya saat mencetak.",
|
||||
"qr_code": "Kode QR",
|
||||
"qr_desc": "Menghasilkan Kode QR yang berisi URL Login dengan username dan password.",
|
||||
"custom_attributes": "Atribut Kustom",
|
||||
"examples": "Contoh",
|
||||
"api_cors_title": "API CORS",
|
||||
"api_cors_subtitle": "Kelola Cross-Origin Resource Sharing untuk akses API.",
|
||||
"add_rule": "Tambah Aturan CORS",
|
||||
"edit_rule": "Edit Aturan CORS",
|
||||
"origin": "Origin",
|
||||
"methods": "Metode Diizinkan",
|
||||
"headers": "Header Diizinkan",
|
||||
"max_age": "Max Age (detik)"
|
||||
},
|
||||
"login": {
|
||||
"welcome": "Selamat datang kembali, silakan masuk untuk melanjutkan.",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"sign_in": "Masuk"
|
||||
},
|
||||
"quick_print": {
|
||||
"title": "Cetak Cepat",
|
||||
"subtitle": "Pembuatan dan pencetakan voucher instan.",
|
||||
"manage": "Kelola Paket",
|
||||
"manage_title": "Kelola Paket",
|
||||
"manage_subtitle": "Konfigurasi paket voucher Quick Print untuk:",
|
||||
"no_packages": "Tidak Ada Paket Ditemukan",
|
||||
"no_packages_found": "Paket tidak ditemukan.",
|
||||
"create_first": "Buat paket Cetak Cepat untuk memulai.",
|
||||
"create_package": "Buat Paket",
|
||||
"add_package": "Tambah Paket",
|
||||
"edit_package": "Edit Paket",
|
||||
"save_package": "Simpan Paket",
|
||||
"delete_package": "Hapus Paket",
|
||||
"print_voucher": "Cetak Voucher",
|
||||
"name": "Nama",
|
||||
"package_name": "Nama Paket",
|
||||
"profile": "Profil",
|
||||
"select_profile": "Pilih Profil",
|
||||
"prefix": "Awalan",
|
||||
"server": "Server",
|
||||
"price": "Harga",
|
||||
"selling_price": "Harga Jual",
|
||||
"time_limit": "Batas Waktu",
|
||||
"data_limit": "Batas Data",
|
||||
"card_color": "Warna Kartu",
|
||||
"char_length": "Panjang Karakter",
|
||||
"vouchers_to_generate": "Jumlah Voucher",
|
||||
"comment": "Komentar"
|
||||
},
|
||||
"sidebar": {
|
||||
"dashboard": "Dashboard",
|
||||
"quick_print": "Cetak Cepat",
|
||||
"hotspot": "Hotspot",
|
||||
"status": "Status",
|
||||
"security": "Keamanan",
|
||||
"reports": "Laporan",
|
||||
"network": "Jaringan",
|
||||
"system": "Sistem",
|
||||
"settings": "Pengaturan",
|
||||
"templates": "Template",
|
||||
"disconnect": "Putuskan Sesi",
|
||||
"logout": "Keluar dari Mivo"
|
||||
},
|
||||
"hotspot_active": {
|
||||
"title": "User Aktif",
|
||||
"subtitle": "Pantau sesi hotspot yang sedang aktif",
|
||||
"filter_server": "Semua Server",
|
||||
"server": "Server",
|
||||
"user": "User",
|
||||
"address": "Alamat / MAC",
|
||||
"uptime": "Waktu Aktif / Sisa",
|
||||
"bytes_in_out": "Bytes Masuk/Keluar",
|
||||
"time_left": "Sisa",
|
||||
"remove": "Putuskan User?"
|
||||
},
|
||||
"hotspot_hosts": {
|
||||
"title": "Host Hotspot",
|
||||
"subtitle": "Perangkat yang terhubung ke jaringan hotspot untuk:",
|
||||
"mac": "Alamat MAC",
|
||||
"address": "Alamat IP",
|
||||
"to_address": "Ke Alamat",
|
||||
"server": "Server",
|
||||
"comment": "Komentar"
|
||||
},
|
||||
"hotspot_menu": {
|
||||
"users": "User",
|
||||
"profiles": "Profil User",
|
||||
"generate": "Generate",
|
||||
"cookies": "Cookie",
|
||||
"active": "Aktif",
|
||||
"hosts": "Host",
|
||||
"bindings": "IP Binding",
|
||||
"walled_garden": "Walled Garden"
|
||||
},
|
||||
"reports_menu": {
|
||||
"resume": "Resume",
|
||||
"selling": "Laporan Penjualan",
|
||||
"user_log": "Log User"
|
||||
},
|
||||
"network_menu": {
|
||||
"dhcp": "DHCP Lease"
|
||||
},
|
||||
"system_menu": {
|
||||
"scheduler": "Penjadwal",
|
||||
"reboot": "Reboot",
|
||||
"shutdown": "Shutdown"
|
||||
},
|
||||
"dhcp": {
|
||||
"title": "DHCP Lease",
|
||||
"subtitle": "DHCP lease aktif untuk:",
|
||||
"all_servers": "Semua Server",
|
||||
"address": "Alamat",
|
||||
"mac": "Alamat MAC",
|
||||
"server": "Server",
|
||||
"status": "Status",
|
||||
"host": "Nama Host"
|
||||
},
|
||||
"cookies": {
|
||||
"title": "Cookie Hotspot",
|
||||
"subtitle": "Cookie autentikasi aktif untuk:",
|
||||
"user": "User",
|
||||
"mac": "Alamat MAC",
|
||||
"ip": "Alamat IP",
|
||||
"expires": "Kedaluwarsa Dalam",
|
||||
"remove_cookie": "Hapus Cookie?",
|
||||
"remove_confirm": "Apakah Anda yakin ingin menghapus cookie untuk {user}?"
|
||||
},
|
||||
"reports": {
|
||||
"selling_title": "Laporan Penjualan",
|
||||
"selling_subtitle": "Ringkasan dan detail penjualan untuk:",
|
||||
"user_log_title": "Log User",
|
||||
"user_log_subtitle": "Riwayat login dan logout untuk:",
|
||||
"resume_title": "Laporan Resume",
|
||||
"resume_subtitle": "Ikhtisar pendapatan yang diagregasi.",
|
||||
"total_income": "Total Pendapatan",
|
||||
"total_vouchers": "Total Voucher Terjual",
|
||||
"date_batch": "Tanggal / Batch (Komentar)",
|
||||
"qty": "Jumlah",
|
||||
"total": "Total",
|
||||
"no_data": "Data penjualan tidak ditemukan.",
|
||||
"time": "Waktu",
|
||||
"topics": "Topik",
|
||||
"message": "Pesan",
|
||||
"daily": "Harian",
|
||||
"monthly": "Bulanan",
|
||||
"yearly": "Tahunan",
|
||||
"date": "Tanggal",
|
||||
"month": "Bulan",
|
||||
"year": "Tahun",
|
||||
"refresh": "Refresh",
|
||||
"print_report": "Cetak Laporan"
|
||||
},
|
||||
"colors": {
|
||||
"blue": "Biru",
|
||||
"red": "Merah",
|
||||
"green": "Hijau",
|
||||
"yellow": "Kuning",
|
||||
"purple": "Ungu",
|
||||
"pink": "Merah Jambu",
|
||||
"indigo": "Nila",
|
||||
"dark": "Gelap"
|
||||
},
|
||||
"hotspot_profiles": {
|
||||
"title": "Profil User",
|
||||
"subtitle": "Kelola batas kecepatan dan harga hotspot",
|
||||
"add_profile": "Tambah Profil",
|
||||
"edit_profile": "Edit Profil",
|
||||
"all_modes": "Semua Mode Kedaluwarsa",
|
||||
"name": "Nama",
|
||||
"shared_users": "User Bersama",
|
||||
"rate_limit": "Batas Kecepatan",
|
||||
"parent_queue": "Antrean Induk",
|
||||
"expired_mode": "Mode Kedaluwarsa",
|
||||
"validity": "Masa Aktif",
|
||||
"price": "Harga",
|
||||
"selling_price": "Harga Jual",
|
||||
"lock_user": "Kunci User",
|
||||
"form": {
|
||||
"add_title": "Tambah Profil",
|
||||
"edit_title": "Edit Profil",
|
||||
"add_subtitle": "Buat profil user hotspot baru untuk: {name}",
|
||||
"edit_subtitle": "Ubah profil user hotspot: {name}",
|
||||
"settings": "Pengaturan Profil",
|
||||
"general": "Pengaturan Umum",
|
||||
"limits_queues": "Batas & Antrean",
|
||||
"pricing_validity": "Harga & Masa Aktif",
|
||||
"address_pool": "Address Pool",
|
||||
"rate_limit_help": "Rx/Tx (Upload/Download). Contoh: 512k/1M",
|
||||
"expired_mode_help": "Tindakan saat masa aktif habis.",
|
||||
"validity_help": "Hari / Jam / Menit",
|
||||
"lock_user_help": "Kunci user ke satu alamat MAC tertentu.",
|
||||
"save": "Simpan Profil",
|
||||
"name_placeholder": "misal: Paket-1Jam",
|
||||
"quick_tips": "Tips Cepat",
|
||||
"tip_rate_limit": "<strong>Batas Kecepatan</strong>: Rx/Tx (Upload/Download). Contoh: <code>512k/1M</code>",
|
||||
"tip_expired_mode": "<strong>Mode Kedaluwarsa</strong>: Pilih 'Remove' atau 'Notice' untuk mengaktifkan Masa Aktif.",
|
||||
"tip_parent_queue": "<strong>Antrean Induk</strong>: Menempatkan user pada antrean induk tertentu untuk manajemen bandwidth."
|
||||
}
|
||||
},
|
||||
"hotspot_users": {
|
||||
"form": {
|
||||
"add_title": "Tambah User",
|
||||
"edit_title": "Edit User",
|
||||
"subtitle": "Akun user hotspot",
|
||||
"edit_subtitle": "Perbarui detail user untuk: {name}",
|
||||
"username": "Username",
|
||||
"password": "Kata Sandi",
|
||||
"profile": "Profil",
|
||||
"server": "Server",
|
||||
"time_limit": "Batas Waktu",
|
||||
"data_limit": "Batas Data",
|
||||
"comment": "Komentar",
|
||||
"save": "Simpan User",
|
||||
"username_help": "Username unik untuk login.",
|
||||
"password_help": "Kata sandi yang kuat untuk keamanan.",
|
||||
"profile_help": "Profil menentukan batas kecepatan dan kebijakan user bersama.",
|
||||
"time_limit_help": "Total waktu aktif yang diperbolehkan (Hari, Jam, Menit).",
|
||||
"data_limit_help": "Batasi penggunaan data (0 untuk tidak terbatas).",
|
||||
"comment_help": "Catatan tambahan atau info kontak.",
|
||||
"username_placeholder": "misal: voucher123",
|
||||
"password_placeholder": "misal: 123456",
|
||||
"comment_placeholder": "Catatan opsional untuk user ini",
|
||||
"quick_tips": "Tips Cepat",
|
||||
"tip_profiles": "<strong>Profil</strong> menentukan batas kecepatan default, timeout sesi, dan kebijakan user bersama.",
|
||||
"tip_time_limit": "<strong>Batas Waktu</strong> adalah total akumulasi waktu aktif yang diperbolehkan untuk user ini.",
|
||||
"tip_data_limit": "<strong>Batas Data</strong> akan menimpa pengaturan batas data profil jika ditentukan di sini. Set ke 0 untuk menggunakan default profil."
|
||||
}
|
||||
},
|
||||
"hotspot_generate": {
|
||||
"title": "Buat Voucher",
|
||||
"form": {
|
||||
"qty": "Jumlah",
|
||||
"server": "Server",
|
||||
"user_mode": "Mode User",
|
||||
"user_length": "Panjang User",
|
||||
"prefix": "Awalan",
|
||||
"characters": "Karakter",
|
||||
"profile": "Profil",
|
||||
"time_limit": "Batas Waktu",
|
||||
"data_limit": "Batas Data",
|
||||
"comment": "Komentar",
|
||||
"generate": "Buat Voucher",
|
||||
"subtitle": "Buat voucher hotspot dalam jumlah banyak untuk: {name}",
|
||||
"batch_settings": "Pengaturan Pembuatan Batch",
|
||||
"core_config": "Konfigurasi Inti",
|
||||
"qty_help": "Jumlah voucher yang akan dibuat.",
|
||||
"server_help": "Instansi Hotspot tujuan.",
|
||||
"user_mode_help": "Format kredensial login.",
|
||||
"comment_help": "Catatan untuk batch ini.",
|
||||
"user_format": "Format User",
|
||||
"name_length_help": "Panjang username/password.",
|
||||
"prefix_placeholder": "misal: VIP-",
|
||||
"prefix_help": "Awalan untuk username yang dibuat.",
|
||||
"characters_help": "Tipe karakter yang disertakan.",
|
||||
"limits_profile": "Batas & Profil",
|
||||
"profile_help": "Terapkan batas kecepatan dari profil.",
|
||||
"time_limit_help": "Waktu aktif maksimal (misal: 1h, 30m).",
|
||||
"data_limit_help": "Transfer data maksimal (MB).",
|
||||
"quick_tips": "Tips Cepat",
|
||||
"tip_user_mode": "<strong>Mode User</strong>: UP (terpisah), VC (sama).",
|
||||
"tip_format_examples": "<strong>Contoh Format</strong>: abcd (kecil), 1234 (angka), Mix (besar/kecil/angka).",
|
||||
"tip_limits": "<strong>Batas</strong>: Waktu (misal: 1h, 30m), Data (misal: 100MB). Kosongkan untuk menggunakan default profil."
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"bindings": {
|
||||
"title": "IP Binding",
|
||||
"subtitle": "Kelola IP binding (bypass/blokir) untuk: {name}",
|
||||
"all_types": "Semua Tipe",
|
||||
"regular": "Reguler",
|
||||
"bypassed": "Bypass",
|
||||
"blocked": "Blokir",
|
||||
"table": {
|
||||
"mac": "Alamat MAC",
|
||||
"address": "Alamat IP",
|
||||
"to_address": "Ke Alamat",
|
||||
"type": "Tipe",
|
||||
"comment": "Komentar"
|
||||
},
|
||||
"form": {
|
||||
"add_title": "Tambah Binding",
|
||||
"mac_address": "Alamat MAC",
|
||||
"address": "Alamat IP",
|
||||
"to_address": "Ke Alamat",
|
||||
"type": "Tipe",
|
||||
"server": "Server",
|
||||
"comment": "Komentar",
|
||||
"mac_help": "Alamat MAC perangkat target.",
|
||||
"address_help": "Alamat IP target (opsional).",
|
||||
"to_address_help": "Terjemahkan ke IP ini (opsional).",
|
||||
"server_help": "Terapkan ke server Hotspot tertentu.",
|
||||
"comment_help": "Catatan untuk binding ini.",
|
||||
"save": "Simpan & Bind",
|
||||
"tip_bypassed": "<strong>Bypass</strong>: Akses tanpa login.",
|
||||
"tip_blocked": "<strong>Blokir</strong>: Tolak akses sepenuhnya.",
|
||||
"tip_regular": "<strong>Reguler</strong>: Klien hotspot normal."
|
||||
}
|
||||
},
|
||||
"walled_garden": {
|
||||
"title": "Walled Garden",
|
||||
"subtitle": "Kelola tujuan yang diperbolehkan (bypass tanpa login) untuk: {name}",
|
||||
"all_actions": "Semua Aksi",
|
||||
"allow": "Izinkan",
|
||||
"deny": "Tolak",
|
||||
"table": {
|
||||
"host_ip": "Host / IP Tujuan",
|
||||
"proto_port": "Protokol / Port",
|
||||
"action": "Aksi",
|
||||
"comment": "Komentar"
|
||||
},
|
||||
"form": {
|
||||
"add_title": "Tambah Entri",
|
||||
"action": "Aksi",
|
||||
"dst_host": "Host Tujuan (Domain)",
|
||||
"dst_address": "Alamat Tujuan (IP)",
|
||||
"protocol": "Protokol",
|
||||
"dst_port": "Port Tujuan",
|
||||
"server": "Server",
|
||||
"comment": "Komentar",
|
||||
"host_help": "Domain yang diperbolehkan (mendukung wildcard).",
|
||||
"addr_help": "Alamat IP Tujuan.",
|
||||
"action_help": "Izinkan (bypass) atau Tolak akses.",
|
||||
"server_help": "Terapkan ke server Hotspot tertentu.",
|
||||
"comment_help": "Catatan untuk aturan ini.",
|
||||
"save": "Simpan Entri",
|
||||
"tip_host": "<strong>Host Tujuan</strong>: Nama domain (misal: <code>*.google.com</code>).",
|
||||
"tip_ip": "<strong>IP Tujuan</strong>: Alamat IP tertentu.",
|
||||
"tip_action": "<strong>Aksi</strong>: Izinkan untuk melewati autentikasi."
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_tools": {
|
||||
"scheduler_subtitle": "Kelola tugas otomatis RouterOS untuk:",
|
||||
"add_task": "Tambah Tugas",
|
||||
"add_title": "Tambah Tugas Penjadwal",
|
||||
"edit_title": "Edit Tugas Penjadwal",
|
||||
"save_task": "Simpan Tugas",
|
||||
"update_task": "Update Tugas",
|
||||
"delete_task": "Hapus Tugas",
|
||||
"table_name": "Nama",
|
||||
"name": "Nama",
|
||||
"interval": "Interval",
|
||||
"next_run": "Run Berikutnya",
|
||||
"status": "Status",
|
||||
"enabled": "Aktif",
|
||||
"disabled": "Nonaktif",
|
||||
"start_date": "Tgl Mulai",
|
||||
"start_time": "Waktu Mulai",
|
||||
"on_event": "Pada Event (Script)",
|
||||
"comment": "Komentar"
|
||||
},
|
||||
"toasts": {
|
||||
"profile_created": "Profil Dibuat",
|
||||
"profile_created_desc": "Profil user \"{name}\" berhasil dibuat.",
|
||||
"profile_updated": "Profil Diperbarui",
|
||||
"profile_updated_desc": "Perubahan pada profil \"{name}\" berhasil disimpan.",
|
||||
"profile_deleted": "Profil Dihapus",
|
||||
"profile_deleted_desc": "Profil user berhasil dihapus.",
|
||||
"user_added": "User Ditambahkan",
|
||||
"user_added_desc": "User hotspot \"{name}\" berhasil dibuat.",
|
||||
"user_deleted": "User Dihapus",
|
||||
"user_deleted_desc": "User hotspot yang dipilih berhasil dihapus.",
|
||||
"user_updated": "User Diperbarui",
|
||||
"user_updated_desc": "Perubahan pada user \"{name}\" berhasil disimpan.",
|
||||
"session_removed": "Sesi Dihapus",
|
||||
"session_removed_desc": "Sesi hotspot aktif telah diputuskan.",
|
||||
"binding_added": "Binding Ditambahkan",
|
||||
"binding_added_desc": "IP Binding berhasil dibuat.",
|
||||
"binding_removed": "Binding Dihapus",
|
||||
"binding_removed_desc": "IP Binding berhasil dihapus.",
|
||||
"rule_added": "Aturan Ditambahkan",
|
||||
"rule_added_desc": "Aturan Walled Garden berhasil ditambahkan.",
|
||||
"rule_removed": "Aturan Dihapus",
|
||||
"rule_removed_desc": "Aturan Walled Garden berhasil dihapus.",
|
||||
"cookie_removed": "Cookie Dihapus",
|
||||
"cookie_removed_desc": "Cookie hotspot berhasil dihapus.",
|
||||
"package_saved": "Paket Disimpan",
|
||||
"package_saved_desc": "Paket quick print berhasil diperbarui.",
|
||||
"package_deleted": "Paket Dihapus",
|
||||
"package_deleted_desc": "Paket quick print berhasil dihapus.",
|
||||
"vouchers_generated": "Voucher Dibuat",
|
||||
"vouchers_generated_desc": "{qty} user berhasil dibuat.",
|
||||
"schedule_added": "Jadwal Ditambahkan",
|
||||
"schedule_added_desc": "Tugas berhasil dijadwalkan.",
|
||||
"schedule_updated": "Jadwal Diperbarui",
|
||||
"schedule_updated_desc": "Perubahan pada tugas berhasil disimpan.",
|
||||
"schedule_deleted": "Jadwal Dihapus",
|
||||
"schedule_deleted_desc": "Tugas yang dijadwalkan berhasil dihapus.",
|
||||
"router_added": "Router Ditambahkan",
|
||||
"router_added_desc": "Sesi {name} berhasil dibuat.",
|
||||
"router_updated": "Router Diperbarui",
|
||||
"router_updated_desc": "Pengaturan untuk {name} berhasil disimpan.",
|
||||
"router_deleted": "Router Dihapus",
|
||||
"router_deleted_desc": "Sesi berhasil dihapus.",
|
||||
"password_updated": "Kata Sandi Diperbarui",
|
||||
"password_updated_desc": "Kata sandi administrator berhasil diubah.",
|
||||
"settings_saved": "Pengaturan Disimpan",
|
||||
"settings_saved_desc": "Preferensi global berhasil diperbarui.",
|
||||
"restore_success": "Restore Berhasil",
|
||||
"restore_success_desc": "Sesi dan pengaturan berhasil dipulihkan dari cadangan.",
|
||||
"restore_failed": "Gagal Memulihkan",
|
||||
"no_file_selected": "Tidak ada file cadangan yang dipilih.",
|
||||
"invalid_file_type": "Tipe file tidak valid. Silakan unggah file .mivo atau .json.",
|
||||
"invalid_file_type_mivo": "Tipe file tidak valid. Silakan unggah file .mivo.",
|
||||
"file_empty": "File yang diunggah kosong.",
|
||||
"file_corrupted": "File rusak atau bukan cadangan Mivo yang valid.",
|
||||
"logo_uploaded": "Logo Diunggah",
|
||||
"logo_uploaded_desc": "Logo baru berhasil ditambahkan.",
|
||||
"logo_deleted": "Logo Dihapus",
|
||||
"logo_deleted_desc": "Logo berhasil dihapus.",
|
||||
"template_created": "Templat Dibuat",
|
||||
"template_created_desc": "Templat voucher \"{name}\" berhasil ditambahkan.",
|
||||
"template_updated": "Templat Diperbarui",
|
||||
"template_updated_desc": "Perubahan pada \"{name}\" berhasil disimpan.",
|
||||
"template_deleted": "Templat Dihapus",
|
||||
"template_deleted_desc": "Templat berhasil dihapus.",
|
||||
"test_alert": "Tes Alert Mivo",
|
||||
"test_alert_desc": "Jika Anda melihat ini, sistem alert baru berfungsi dengan sempurna! 🚀",
|
||||
"cors_rule_added": "Aturan CORS Ditambahkan",
|
||||
"cors_rule_added_desc": "Aturan CORS untuk {origin} berhasil dibuat.",
|
||||
"cors_rule_updated": "Aturan CORS Diperbarui",
|
||||
"cors_rule_updated_desc": "Perubahan pada aturan CORS untuk {origin} berhasil disimpan.",
|
||||
"cors_rule_deleted": "Aturan CORS Dihapus",
|
||||
"cors_rule_deleted_desc": "Aturan CORS berhasil dihapus."
|
||||
}
|
||||
}
|
||||
26
public/web.config
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<rewrite>
|
||||
<rules>
|
||||
<rule name="Imported Rule 1" stopProcessing="true">
|
||||
<match url="^" ignoreCase="false" />
|
||||
<conditions logicalGrouping="MatchAll">
|
||||
<add input="{HTTP_AUTHORIZATION}" ignoreCase="false" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
|
||||
<add input="{URL}" pattern="(.+)/$" ignoreCase="false" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="{C:1}" redirectType="Permanent" />
|
||||
</rule>
|
||||
<rule name="Imported Rule 2" stopProcessing="true">
|
||||
<match url="^" ignoreCase="false" />
|
||||
<conditions logicalGrouping="MatchAll">
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="index.php" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||