feat(portal): kustomisasi banner, status, hero, sosial dari Pengaturan
All checks were successful
CI / test (push) Successful in 1m41s

- banner + warna, status jaringan, hero badge/title/subtitle
- link maps/instagram/facebook/tiktok di footer
- boolean portal_banner_enabled
- versi 0.6.14
This commit is contained in:
oxpoe
2026-11-06 11:13:44 +07:00
parent e2ebc079f1
commit 31e8f46e03
7 changed files with 116 additions and 6 deletions

View File

@@ -2,6 +2,15 @@
Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/). Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/).
## [0.6.14] — 2026-11-06 — "portal-custom"
### Ditambahkan
- **Kustomisasi Beranda & Portal Pelanggan** dari Pengaturan:
- banner pengumuman (info/warning/danger/success),
- status jaringan + keterangan,
- badge, judul, dan deskripsi hero,
- link Google Maps, Instagram, Facebook, TikTok di footer.
## [0.6.13] — 2026-11-06 — "webhook-shopeepay" ## [0.6.13] — 2026-11-06 — "webhook-shopeepay"
### Diperbaiki ### Diperbaiki

View File

@@ -11,7 +11,7 @@
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/lisensi-ISC-ADFF2F" alt="Lisensi: ISC"> <img src="https://img.shields.io/badge/lisensi-ISC-ADFF2F" alt="Lisensi: ISC">
<img src="https://img.shields.io/badge/versi-0.6.13_webhook--shopeepay-ADFF2F" alt="Versi 0.6.13 (webhook-shopeepay)"> <img src="https://img.shields.io/badge/versi-0.6.14_portal--custom-ADFF2F" alt="Versi 0.6.14 (portal-custom)">
<img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933" alt="Node.js >= 20"> <img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933" alt="Node.js >= 20">
<img src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white" alt="Docker"> <img src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white" alt="Docker">
<a href="https://demo-openisp.oxpoe.xyz"><img src="https://img.shields.io/badge/demo-live-ADFF2F?logo=googlechrome&logoColor=black" alt="Demo"></a> <a href="https://demo-openisp.oxpoe.xyz"><img src="https://img.shields.io/badge/demo-live-ADFF2F?logo=googlechrome&logoColor=black" alt="Demo"></a>

View File

@@ -1,6 +1,6 @@
{ {
"name": "open-isp", "name": "open-isp",
"version": "0.6.13", "version": "0.6.14",
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)", "description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
"main": "app-customer.js", "main": "app-customer.js",
"scripts": { "scripts": {

View File

@@ -5069,6 +5069,7 @@ router.post('/settings', requireAdminSession, express.urlencoded({ extended: tru
if (newSettings.auto_deactivate_enabled !== undefined) newSettings.auto_deactivate_enabled = (newSettings.auto_deactivate_enabled === 'true'); if (newSettings.auto_deactivate_enabled !== undefined) newSettings.auto_deactivate_enabled = (newSettings.auto_deactivate_enabled === 'true');
if (newSettings.auto_deactivate_months !== undefined) newSettings.auto_deactivate_months = parseInt(newSettings.auto_deactivate_months) || 3; if (newSettings.auto_deactivate_months !== undefined) newSettings.auto_deactivate_months = parseInt(newSettings.auto_deactivate_months) || 3;
newSettings.use_builtin_acs = (newSettings.use_builtin_acs === 'true' || newSettings.use_builtin_acs === true); newSettings.use_builtin_acs = (newSettings.use_builtin_acs === 'true' || newSettings.use_builtin_acs === true);
newSettings.portal_banner_enabled = (newSettings.portal_banner_enabled === 'true' || newSettings.portal_banner_enabled === true);
// Multi-Router Mode settings // Multi-Router Mode settings
if (!newSettings.multi_router_mode) newSettings.multi_router_mode = 'disabled'; if (!newSettings.multi_router_mode) newSettings.multi_router_mode = 'disabled';

View File

@@ -1 +1 @@
0.6.13 0.6.14

View File

@@ -392,6 +392,71 @@
</div> </div>
<!-- GenieACS & MikroTik --> <!-- GenieACS & MikroTik -->
<div class="card mb16" data-setcat="umum">
<div class="card-hd"><h6><i class="bi bi-window-desktop"></i> Kustomisasi Beranda &amp; Portal Pelanggan</h6></div>
<div class="card-bd settings-grid">
<div>
<h6 style="font-size:13px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px"><i class="bi bi-megaphone"></i> Banner &amp; Status</h6>
<div class="fg">
<label class="fl">Banner Pengumuman Portal</label>
<select class="fs" name="portal_banner_enabled">
<option value="false" <%= !settings.portal_banner_enabled ? 'selected' : '' %>>Nonaktif</option>
<option value="true" <%= settings.portal_banner_enabled ? 'selected' : '' %>>Aktif</option>
</select>
</div>
<div class="fg">
<label class="fl">Tipe Banner</label>
<select class="fs" name="portal_banner_type">
<option value="info" <%= (settings.portal_banner_type === 'info' || !settings.portal_banner_type) ? 'selected' : '' %>>Info (Biru)</option>
<option value="warning" <%= settings.portal_banner_type === 'warning' ? 'selected' : '' %>>Peringatan (Kuning)</option>
<option value="danger" <%= settings.portal_banner_type === 'danger' ? 'selected' : '' %>>Gangguan (Merah)</option>
<option value="success" <%= settings.portal_banner_type === 'success' ? 'selected' : '' %>>Promo (Hijau)</option>
</select>
</div>
<div class="fg">
<label class="fl">Teks Banner</label>
<textarea class="fc" name="portal_banner_text" rows="2" placeholder="Contoh: Maintenance jaringan Kamis 01.00-04.00 WIB."><%= settings.portal_banner_text || '' %></textarea>
</div>
<div class="fg">
<label class="fl">Status Jaringan</label>
<select class="fs" name="portal_network_status">
<option value="normal" <%= (settings.portal_network_status === 'normal' || !settings.portal_network_status) ? 'selected' : '' %>>🟢 Normal</option>
<option value="maintenance" <%= settings.portal_network_status === 'maintenance' ? 'selected' : '' %>>🟠 Pemeliharaan</option>
<option value="issue" <%= settings.portal_network_status === 'issue' ? 'selected' : '' %>>🔴 Ada Kendala</option>
</select>
</div>
<div class="fg">
<label class="fl">Keterangan Status (opsional)</label>
<input class="fc" name="portal_network_status_text" value="<%= settings.portal_network_status_text || '' %>" placeholder="Kosongkan untuk teks bawaan">
</div>
</div>
<div>
<h6 style="font-size:13px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px"><i class="bi bi-card-heading"></i> Hero &amp; Media Sosial</h6>
<div class="fg">
<label class="fl">Badge Kecil</label>
<input class="fc" name="portal_hero_badge" value="<%= settings.portal_hero_badge || '' %>" placeholder="Portal Pelanggan">
</div>
<div class="fg">
<label class="fl">Judul Utama</label>
<input class="fc" name="portal_hero_title" value="<%= settings.portal_hero_title || '' %>" placeholder="Kosongkan untuk teks bawaan">
</div>
<div class="fg">
<label class="fl">Deskripsi</label>
<textarea class="fc" name="portal_hero_subtitle" rows="2" placeholder="Kosongkan untuk teks bawaan"><%= settings.portal_hero_subtitle || '' %></textarea>
</div>
<div class="fg">
<label class="fl">Link Google Maps Kantor</label>
<input class="fc" name="social_maps_url" value="<%= settings.social_maps_url || '' %>" placeholder="https://maps.google.com/...">
</div>
<div class="form-row">
<div class="fg"><label class="fl">Instagram</label><input class="fc" name="social_instagram" value="<%= settings.social_instagram || '' %>" placeholder="https://instagram.com/..."></div>
<div class="fg"><label class="fl">Facebook</label><input class="fc" name="social_facebook" value="<%= settings.social_facebook || '' %>" placeholder="https://facebook.com/..."></div>
</div>
<div class="fg"><label class="fl">TikTok</label><input class="fc" name="social_tiktok" value="<%= settings.social_tiktok || '' %>" placeholder="https://tiktok.com/@..."></div>
</div>
</div>
</div>
<div data-setcat="jaringan"> <div data-setcat="jaringan">
<h6 style="font-size:13px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px"><i class="bi bi-router"></i> <%= t('admin.settings.external_api', 'API Eksternal') %></h6> <h6 style="font-size:13px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px"><i class="bi bi-router"></i> <%= t('admin.settings.external_api', 'API Eksternal') %></h6>

View File

@@ -31,6 +31,21 @@
</script> </script>
</head> </head>
<body> <body>
<%
const _portal = (typeof settings !== 'undefined' && settings) ? settings : {};
const _pbColors = {
info: { bg: 'rgba(59,130,246,.16)', border: '#3b82f6', fg: '#bfdbfe' },
warning: { bg: 'rgba(245,158,11,.16)', border: '#f59e0b', fg: '#fde68a' },
danger: { bg: 'rgba(239,68,68,.16)', border: '#ef4444', fg: '#fecaca' },
success: { bg: 'rgba(34,197,94,.16)', border: '#22c55e', fg: '#bbf7d0' }
};
const _pb = _pbColors[_portal.portal_banner_type] || _pbColors.info;
%>
<% if (_portal.portal_banner_enabled && _portal.portal_banner_text) { %>
<div style="background:<%= _pb.bg %>;border-bottom:1px solid <%= _pb.border %>;color:<%= _pb.fg %>;padding:9px 16px;text-align:center;font-size:13px;line-height:1.45">
<i class="bi bi-megaphone-fill"></i> <strong><%= _portal.portal_banner_text %></strong>
</div>
<% } %>
<!-- ═══ HEADER ═══ --> <!-- ═══ HEADER ═══ -->
<header class="site-header"> <header class="site-header">
@@ -69,9 +84,21 @@
<section class="auth-hero"> <section class="auth-hero">
<div class="container auth-grid"> <div class="container auth-grid">
<div class="auth-copy reveal"> <div class="auth-copy reveal">
<span class="eyebrow"><i class="bi bi-shield-lock-fill"></i> <%= t('landing.footer.portal','Portal Pelanggan') %></span> <span class="eyebrow"><i class="bi bi-shield-lock-fill"></i> <%= _portal.portal_hero_badge || t('landing.footer.portal','Portal Pelanggan') %></span>
<% if (_portal.portal_hero_title) { %>
<h1><%= _portal.portal_hero_title %></h1>
<% } else { %>
<h1><%= t('login.manage','Kelola layanan internet Anda') %> <span class="hl"><%= t('login.manage_hl','dalam satu tempat') %></span></h1> <h1><%= t('login.manage','Kelola layanan internet Anda') %> <span class="hl"><%= t('login.manage_hl','dalam satu tempat') %></span></h1>
<p class="lead"><%= t('login.manage_desc','Masuk untuk melihat tagihan, melakukan pembayaran, memantau koneksi, dan mengatur WiFi perangkat Anda.') %></p> <% } %>
<p class="lead"><%= _portal.portal_hero_subtitle || t('login.manage_desc','Masuk untuk melihat tagihan, melakukan pembayaran, memantau koneksi, dan mengatur WiFi perangkat Anda.') %></p>
<% if (_portal.portal_network_status) {
const _ns = { normal: ['#22c55e', 'Jaringan Normal'], maintenance: ['#f59e0b', 'Pemeliharaan Terjadwal'], issue: ['#ef4444', 'Sedang Ada Kendala Teknis'] }[_portal.portal_network_status] || ['#22c55e', 'Jaringan Normal'];
%>
<div style="display:inline-flex;align-items:center;gap:7px;margin-top:10px;padding:5px 11px;border-radius:999px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);font-size:12px;color:var(--muted)">
<span style="width:8px;height:8px;border-radius:50%;background:<%= _ns[0] %>;display:inline-block"></span>
<%= _portal.portal_network_status_text || _ns[1] %>
</div>
<% } %>
<ul class="auth-points"> <ul class="auth-points">
<li><i class="bi bi-check-circle-fill"></i> <%= t('login.chk_desc','Cek & bayar tagihan kapan saja') %></li> <li><i class="bi bi-check-circle-fill"></i> <%= t('login.chk_desc','Cek & bayar tagihan kapan saja') %></li>
<li><i class="bi bi-check-circle-fill"></i> <%= t('login.mon_desc','Pantau trafik dan status koneksi') %></li> <li><i class="bi bi-check-circle-fill"></i> <%= t('login.mon_desc','Pantau trafik dan status koneksi') %></li>
@@ -206,6 +233,14 @@
<p class="muted" style="font-size:14px;max-width:40ch">Micro Internet Provider dan Server Platform.</p> <p class="muted" style="font-size:14px;max-width:40ch">Micro Internet Provider dan Server Platform.</p>
<p class="muted" style="font-size:13px;margin-top:8px">Internet Freedom — We Are Open-Source.</p> <p class="muted" style="font-size:13px;margin-top:8px">Internet Freedom — We Are Open-Source.</p>
<p class="muted" style="font-size:12.5px;margin-top:12px"><% if (typeof settings !== 'undefined' && settings && settings.company_subheader) { %><%= settings.company_subheader %><% } else { %>Made with <i class="bi bi-heart-fill" style="color:#e5484d"></i> from Oxpoe in INA.<% } %></p> <p class="muted" style="font-size:12.5px;margin-top:12px"><% if (typeof settings !== 'undefined' && settings && settings.company_subheader) { %><%= settings.company_subheader %><% } else { %>Made with <i class="bi bi-heart-fill" style="color:#e5484d"></i> from Oxpoe in INA.<% } %></p>
<% if (_portal.social_maps_url || _portal.social_instagram || _portal.social_facebook || _portal.social_tiktok) { %>
<div style="display:flex;gap:10px;margin-top:12px;flex-wrap:wrap;font-size:16px">
<% if (_portal.social_maps_url) { %><a href="<%= _portal.social_maps_url %>" target="_blank" rel="noopener" title="Maps"><i class="bi bi-geo-alt-fill"></i></a><% } %>
<% if (_portal.social_instagram) { %><a href="<%= _portal.social_instagram %>" target="_blank" rel="noopener" title="Instagram"><i class="bi bi-instagram"></i></a><% } %>
<% if (_portal.social_facebook) { %><a href="<%= _portal.social_facebook %>" target="_blank" rel="noopener" title="Facebook"><i class="bi bi-facebook"></i></a><% } %>
<% if (_portal.social_tiktok) { %><a href="<%= _portal.social_tiktok %>" target="_blank" rel="noopener" title="TikTok"><i class="bi bi-tiktok"></i></a><% } %>
</div>
<% } %>
</div> </div>
<div> <div>
<h4>Portal</h4> <h4>Portal</h4>