made dashboard look nicer

This commit is contained in:
rocord01 2025-12-20 23:14:48 -05:00
parent 4c200d7591
commit 29b85bc38c
6 changed files with 889 additions and 131 deletions

View file

@ -1,9 +1,5 @@
{ {
"COLS": [ "Normal Announcements": [
"Normal Announcements",
"Emergency Announcements"
],
"ROWS": [
{ {
"text": "Live Page", "text": "Live Page",
"btnClass": "btn-primary", "btnClass": "btn-primary",
@ -15,7 +11,10 @@
"timeout": 30000, "timeout": 30000,
"cid": "Live Page" "cid": "Live Page"
} }
}, }
],
"Emergency Announcements": [
{ {
"text": "Emergency Announcement", "text": "Emergency Announcement",
"btnClass": "btn-danger", "btnClass": "btn-danger",
@ -28,7 +27,7 @@
"cid": "Emergency Announcement" "cid": "Emergency Announcement"
} }
}, },
{},
{ {
"text": "Tornado Alert", "text": "Tornado Alert",
"btnClass": "btn-warning", "btnClass": "btn-warning",
@ -43,4 +42,5 @@
} }
} }
] ]
} }

View file

@ -9,18 +9,18 @@ const buttonsCfg = require('./buttons.json');
const contexts = {}; const contexts = {};
// Generate contexts from buttonsCfg // Generate contexts from buttonsCfg
if (buttonsCfg && buttonsCfg.ROWS) { Object.keys(buttonsCfg).forEach(category => {
buttonsCfg.ROWS.forEach(button => { buttonsCfg[category].forEach(button => {
if (button.name && button.context) { if (button.name && button.context) {
contexts[button.name] = { contexts[button.name] = {
context: button.context.context, context: button.context.context,
timeout: button.context.timeout, timeout: button.context.timeout,
cid: button.context.cid, cid: button.context.cid,
...(button.context.dial && { dial: button.context.dial }) ...(button.context.dial && { dial: button.context.dial })
}; };
} }
}); });
} });
console.log('Generated contexts:', contexts); console.log('Generated contexts:', contexts);
@ -95,6 +95,10 @@ app.get('/', (req, res) => {
res.render('index', { session: req.session, phones: phonesCfg, buttons: require("./buttons.json") }); res.render('index', { session: req.session, phones: phonesCfg, buttons: require("./buttons.json") });
}); });
app.get('/login', (req, res) => {
res.render('login', { session: req.session });
});
app.post('/trig', async (req, res) => { app.post('/trig', async (req, res) => {
console.log('Triggering call with data:', req.body); console.log('Triggering call with data:', req.body);
trigCall(req.body.pageType, req.body.phone); trigCall(req.body.pageType, req.body.phone);

699
pnpm-lock.yaml Normal file
View file

@ -0,0 +1,699 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
dotenv:
specifier: ^17.2.3
version: 17.2.3
ejs:
specifier: ^3.1.10
version: 3.1.10
express:
specifier: ^5.2.1
version: 5.2.1
express-session:
specifier: ^1.18.2
version: 1.18.2
packages:
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
body-parser@2.2.1:
resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==}
engines: {node: '>=18'}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
content-disposition@1.0.1:
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
engines: {node: '>=18'}
content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
cookie-signature@1.0.7:
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
cookie-signature@1.2.2:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
engines: {node: '>=6.6.0'}
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dotenv@17.2.3:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
ejs@3.1.10:
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
engines: {node: '>=0.10.0'}
hasBin: true
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
express-session@1.18.2:
resolution: {integrity: sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==}
engines: {node: '>= 0.8.0'}
express@5.2.1:
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
engines: {node: '>= 18'}
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
finalhandler@2.1.1:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
iconv-lite@0.7.1:
resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==}
engines: {node: '>=0.10.0'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
jake@10.9.4:
resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==}
engines: {node: '>=10'}
hasBin: true
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
merge-descriptors@2.0.0:
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
engines: {node: '>=18'}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
on-headers@1.1.0:
resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
engines: {node: '>= 0.8'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
random-bytes@1.0.0:
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
engines: {node: '>= 0.8'}
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
raw-body@3.0.2:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
router@2.2.0:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
send@1.2.1:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
serve-static@2.2.1:
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
side-channel-map@1.0.1:
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
engines: {node: '>= 0.4'}
side-channel-weakmap@1.0.2:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'}
side-channel@1.1.0:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
type-is@2.0.1:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
uid-safe@2.1.5:
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
engines: {node: '>= 0.8'}
unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
snapshots:
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
negotiator: 1.0.0
async@3.2.6: {}
balanced-match@1.0.2: {}
body-parser@2.2.1:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.3
http-errors: 2.0.1
iconv-lite: 0.7.1
on-finished: 2.4.1
qs: 6.14.0
raw-body: 3.0.2
type-is: 2.0.1
transitivePeerDependencies:
- supports-color
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
call-bound@1.0.4:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
content-disposition@1.0.1: {}
content-type@1.0.5: {}
cookie-signature@1.0.7: {}
cookie-signature@1.2.2: {}
cookie@0.7.2: {}
debug@2.6.9:
dependencies:
ms: 2.0.0
debug@4.4.3:
dependencies:
ms: 2.1.3
depd@2.0.0: {}
dotenv@17.2.3: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
ee-first@1.1.1: {}
ejs@3.1.10:
dependencies:
jake: 10.9.4
encodeurl@2.0.0: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
escape-html@1.0.3: {}
etag@1.8.1: {}
express-session@1.18.2:
dependencies:
cookie: 0.7.2
cookie-signature: 1.0.7
debug: 2.6.9
depd: 2.0.0
on-headers: 1.1.0
parseurl: 1.3.3
safe-buffer: 5.2.1
uid-safe: 2.1.5
transitivePeerDependencies:
- supports-color
express@5.2.1:
dependencies:
accepts: 2.0.0
body-parser: 2.2.1
content-disposition: 1.0.1
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
debug: 4.4.3
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 2.1.1
fresh: 2.0.0
http-errors: 2.0.1
merge-descriptors: 2.0.0
mime-types: 3.0.2
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
proxy-addr: 2.0.7
qs: 6.14.0
range-parser: 1.2.1
router: 2.2.0
send: 1.2.1
serve-static: 2.2.1
statuses: 2.0.2
type-is: 2.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
filelist@1.0.4:
dependencies:
minimatch: 5.1.6
finalhandler@2.1.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
forwarded@0.2.0: {}
fresh@2.0.0: {}
function-bind@1.1.2: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
gopd@1.2.0: {}
has-symbols@1.1.0: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
http-errors@2.0.1:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.2
toidentifier: 1.0.1
iconv-lite@0.7.1:
dependencies:
safer-buffer: 2.1.2
inherits@2.0.4: {}
ipaddr.js@1.9.1: {}
is-promise@4.0.0: {}
jake@10.9.4:
dependencies:
async: 3.2.6
filelist: 1.0.4
picocolors: 1.1.1
math-intrinsics@1.1.0: {}
media-typer@1.1.0: {}
merge-descriptors@2.0.0: {}
mime-db@1.54.0: {}
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.2
ms@2.0.0: {}
ms@2.1.3: {}
negotiator@1.0.0: {}
object-inspect@1.13.4: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
on-headers@1.1.0: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
parseurl@1.3.3: {}
path-to-regexp@8.3.0: {}
picocolors@1.1.1: {}
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
qs@6.14.0:
dependencies:
side-channel: 1.1.0
random-bytes@1.0.0: {}
range-parser@1.2.1: {}
raw-body@3.0.2:
dependencies:
bytes: 3.1.2
http-errors: 2.0.1
iconv-lite: 0.7.1
unpipe: 1.0.0
router@2.2.0:
dependencies:
debug: 4.4.3
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
path-to-regexp: 8.3.0
transitivePeerDependencies:
- supports-color
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
send@1.2.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.1
mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
serve-static@2.2.1:
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 1.2.1
transitivePeerDependencies:
- supports-color
setprototypeof@1.2.0: {}
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-map@1.0.1:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-weakmap@1.0.2:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-map: 1.0.1
side-channel@1.1.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-list: 1.0.0
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
statuses@2.0.2: {}
toidentifier@1.0.1: {}
type-is@2.0.1:
dependencies:
content-type: 1.0.5
media-typer: 1.1.0
mime-types: 3.0.2
uid-safe@2.1.5:
dependencies:
random-bytes: 1.0.0
unpipe@1.0.0: {}
vary@1.1.2: {}
wrappy@1.0.2: {}

BIN
static/assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -4,94 +4,127 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/assets/css/bootstrap.min.css"> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/tuta-amb/fontawesome-pro@latest/web/css/all.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/tuta-amb/fontawesome-pro@latest/web/css/all.min.css">
<title>Paging Console</title> <title>Paging Console</title>
<style> <style type="text/tailwindcss">
.btn-tall { height: 152px; } @layer components {
.btn-short { height: 76px; } .btn {
@apply inline-flex items-center justify-center font-bold text-center whitespace-nowrap select-none border border-transparent py-3 px-6 rounded-xl text-lg transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 focus:ring-offset-2 shadow-md hover:shadow-lg transform hover:-translate-y-0.5 active:translate-y-0 active:shadow-sm;
}
.btn-primary {
@apply text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:ring-blue-500/50 border-blue-700;
}
.btn-secondary {
@apply text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:ring-gray-500/50 border-gray-700;
}
.btn-success {
@apply text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:ring-green-500/50 border-green-700;
}
.btn-danger {
@apply text-white bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 focus:ring-red-500/50 border-red-700;
}
.btn-warning {
@apply text-gray-900 bg-gradient-to-r from-yellow-400 to-yellow-500 hover:from-yellow-500 hover:to-yellow-600 focus:ring-yellow-400/50 border-yellow-500;
}
.btn-info {
@apply text-white bg-gradient-to-r from-cyan-500 to-cyan-600 hover:from-cyan-600 hover:to-cyan-700 focus:ring-cyan-400/50 border-cyan-600;
}
.btn-light {
@apply text-gray-800 bg-gradient-to-r from-gray-100 to-gray-200 hover:from-gray-200 hover:to-gray-300 focus:ring-gray-200/50 border-gray-300;
}
.btn-dark {
@apply text-white bg-gradient-to-r from-gray-800 to-gray-900 hover:from-gray-900 hover:to-black focus:ring-gray-800/50 border-gray-900;
}
.btn-tall {
@apply h-[160px] text-2xl;
}
.btn-short {
@apply h-[80px] text-xl;
}
.glass-panel {
@apply bg-white/80 backdrop-blur-md border border-white/20 shadow-xl rounded-2xl;
}
}
body {
background-color: #0f172a;
background-image: radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%), radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%), radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%);
background-attachment: fixed;
}
</style> </style>
</head> </head>
<body> <body class="text-gray-100 antialiased min-h-screen flex flex-col">
<div class="position-absolute top-0 start-0 m-3"> <nav class="w-full bg-gray-900/50 backdrop-blur-md border-b border-white/10 px-6 py-4 flex justify-between items-center sticky top-0 z-50 shadow-sm">
<select class="form-select" id="phoneSelect"> <div class="flex items-center gap-3">
<% Object.entries(phones).forEach(([name, number])=> { %> <img src="/assets/img/logo.png" alt="Logo" class="h-10 w-auto">
<option value="<%= number %>"> <h1 class="text-xl font-bold text-white tracking-wide">Paging Console</h1>
<%= name %> </div>
</option> <div class="flex items-center gap-4">
<% }); %> <div class="relative group">
</select> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
</div> <i class="fa-regular fa-phone text-gray-400 group-focus-within:text-blue-400 transition-colors"></i>
</div>
<select class="appearance-none bg-gray-800/90 hover:bg-gray-700 border border-transparent hover:border-blue-500/50 text-gray-200 py-2 pl-10 pr-8 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all cursor-pointer font-medium text-sm" id="phoneSelect">
<% Object.entries(phones).forEach(([name, number])=> { %>
<option value="<%= number %>">
<%= name %>
</option>
<% }); %>
</select>
<div class="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<i class="fa-solid fa-chevron-down text-xs text-gray-400"></i>
</div>
</div>
</div>
</nav>
<% <%
// determine columns and chunk rows // determine columns and chunk rows
const cols = (buttons && buttons.COLS) || ['']; const cols = Object.keys(buttons);
const numCols = Math.max(1, cols.length); const numCols = Math.max(1, cols.length);
const rowsFlat = (buttons && buttons.ROWS) || [];
const chunks = [];
for (let i = 0; i < rowsFlat.length; i += numCols) {
const slice = rowsFlat.slice(i, i + numCols);
// pad short slice with empty objects so layout stays consistent
while (slice.length < numCols) slice.push({});
chunks.push(slice);
}
%> %>
<div class="container-fluid d-flex justify-content-center align-items-center min-vh-100"> <main class="flex-grow flex justify-center items-start p-6 md:p-12">
<div> <div class="w-full max-w-7xl">
<table class="table table-borderless table-sm"> <div class="grid gap-8" style="grid-template-columns: repeat(<%= numCols %>, minmax(0, 1fr));">
<thead> <% cols.forEach(col => { %>
<tr> <div class="flex flex-col gap-4">
<% for (let c = 0; c < numCols; c++) { %> <div class="pb-2 border-b border-white/20 mb-2">
<th class="text-left p-2"> <h3 class="text-2xl font-bold text-white drop-shadow-md flex items-center gap-2">
<h3><%= cols[c] || '' %></h3> <%= col %>
</th> </h3>
<% } %> </div>
</tr>
</thead> <% buttons[col].forEach((cell) => {
<tbody> const ctx = cell.context || {};
<% chunks.forEach((row) => { %> %>
<tr> <button
<% for (let c = 0; c < numCols; c++) { class="btn <%= cell.btnClass || 'btn-secondary' %> w-full <%= cell.doubleHeight ? 'btn-tall' : 'btn-short' %> group relative overflow-hidden"
const cell = row[c] || {}; name="<%= cell.name || '' %>"
const isBlank = Object.keys(cell).length === 0; onclick="triggerAnnouncement(this)"
%> <% if (ctx.context) { %> data-context="<%= ctx.context %>" <% } %>
<td class="text-left p-2"> <% if (ctx.timeout) { %> data-timeout="<%= ctx.timeout %>" <% } %>
<% if (isBlank) { %> <% if (ctx.cid) { %> data-cid="<%= ctx.cid %>" <% } %>
<!-- blank space --> <% if (ctx.dial) { %> data-dial="<%= ctx.dial %>" <% } %>
<% } else { >
const ctx = cell.context || {}; <div class="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
%> <div class="relative z-10 flex flex-col items-center justify-center gap-2">
<button <% if (cell.faIcon) { %>
class="btn <%= cell.btnClass || 'btn-secondary' %> mb-1 w-100 <%= cell.doubleHeight ? 'btn-tall' : 'btn-short' %> fw-bold fs-4 <%= cell.faIcon ? 'd-flex justify-content-center align-items-center' : '' %>" <i class="<%= cell.faIcon %> text-3xl mb-1 opacity-90 group-hover:scale-110 transition-transform duration-300" <% if (cell.faStyle) { %> style="<%= cell.faStyle %>" <% } %>></i>
name="<%= cell.name || '' %>" <span class="tracking-wide"><%= cell.text || '' %></span>
onclick="triggerAnnouncement(this)" <% } else { %>
<% if (ctx.context) { %> data-context="<%= ctx.context %>" <% } %> <span class="tracking-wide"><%= cell.text || '' %></span>
<% if (ctx.timeout) { %> data-timeout="<%= ctx.timeout %>" <% } %> <% } %>
<% if (ctx.cid) { %> data-cid="<%= ctx.cid %>" <% } %> </div>
<% if (ctx.dial) { %> data-dial="<%= ctx.dial %>" <% } %> </button>
> <% }); %>
<% if (cell.faIcon) { %> </div>
<i class="<%= cell.faIcon %> me-2" <% if (cell.faStyle) { %> style="<%= cell.faStyle %>" <% } %>></i> <% }); %>
<span><%= cell.text || '' %></span> </div>
<% } else { %> </div>
<%= cell.text || '' %> </main>
<% } %>
</button>
<% } %>
</td>
<% } %>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
<script src="/assets/js/bootstrap.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/jquery.min.js"></script>
<script> <script>
function triggerAnnouncement(button) { function triggerAnnouncement(button) {
const ann = button.getAttribute('name'); const ann = button.getAttribute('name');

View file

@ -3,49 +3,71 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/css/bootstrap.min.css"> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/tuta-amb/fontawesome-pro@latest/web/css/all.min.css">
<title>Sign In</title> <title>Sign In</title>
<style>
body {
background-color: #0f172a;
background-image: radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%), radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%), radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%);
background-attachment: fixed;
}
</style>
</head> </head>
<body class="bg-light"> <body class="antialiased min-h-screen flex items-center justify-center p-4 text-gray-100">
<div class="container"> <div class="w-full max-w-md">
<div class="row justify-content-center align-items-center" style="min-height:80vh;"> <div class="bg-gray-800/80 backdrop-blur-lg shadow-2xl rounded-2xl overflow-hidden border border-white/10">
<div class="col-md-8 col-lg-5"> <div class="p-8 sm:p-10">
<div class="card shadow-sm"> <div class="text-center mb-8">
<div class="card-body p-4"> <div class="flex justify-center mb-6">
<h3 class="card-title text-center mb-3">Sign in</h3> <img src="/assets/img/logo.png" alt="Logo" class="h-20 w-auto">
<% if (typeof error !== 'undefined' && error) { %>
<div class="alert alert-danger" role="alert"><%= error %></div>
<% } %>
<form action="/login" method="post" novalidate>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input id="username" name="username" type="text" class="form-control" required autofocus
value="<%= typeof username !== 'undefined' ? username : '' %>">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input id="password" name="password" type="password" class="form-control" required>
</div>
<div class="mb-3 form-check">
<input id="remember" name="remember" type="checkbox" class="form-check-input">
<label class="form-check-label" for="remember">Remember me</label>
</div>
<button type="submit" class="btn btn-primary w-100">Sign in</button>
</form>
<p class="text-center text-muted small mt-3 mb-0">Return to <a href="/">home</a></p>
</div> </div>
<h3 class="text-3xl font-bold text-white tracking-tight">Paging Console</h3>
<p class="text-gray-400 mt-2">Please sign in to your account</p>
</div> </div>
<% if (typeof error !== 'undefined' && error) { %>
<div class="bg-red-900/50 border-l-4 border-red-500 text-red-200 p-4 rounded-r mb-6 flex items-start gap-3" role="alert">
<i class="fa-solid fa-circle-exclamation mt-1"></i>
<div><%= error %></div>
</div>
<% } %>
<form action="/login" method="post" novalidate class="space-y-6">
<div>
<label for="username" class="block text-sm font-semibold text-gray-300 mb-2">Username</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fa-regular fa-user text-gray-500"></i>
</div>
<input id="username" name="username" type="text" class="block w-full pl-10 pr-3 py-3 border border-gray-600 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all bg-gray-900/50 focus:bg-gray-900 text-white placeholder-gray-500" placeholder="Enter your username" required autofocus
value="<%= typeof username !== 'undefined' ? username : '' %>">
</div>
</div>
<div>
<label for="password" class="block text-sm font-semibold text-gray-300 mb-2">Password</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fa-regular fa-lock text-gray-500"></i>
</div>
<input id="password" name="password" type="password" class="block w-full pl-10 pr-3 py-3 border border-gray-600 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all bg-gray-900/50 focus:bg-gray-900 text-white placeholder-gray-500" placeholder="••••••••" required>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input id="remember" name="remember" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-600 rounded cursor-pointer bg-gray-700">
<label class="ml-2 block text-sm text-gray-400 cursor-pointer select-none" for="remember">Remember me</label>
</div>
</div>
<button type="submit" class="w-full flex justify-center py-3 px-4 border border-transparent rounded-xl shadow-lg text-sm font-bold text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transform transition-all hover:-translate-y-0.5 active:translate-y-0">
Sign in
</button>
</form>
</div> </div>
</div> </div>
</div> </div>
<script src="/assets/js/jquery.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>