reponsive

This commit is contained in:
2026-04-02 15:41:23 +07:00
parent a78769cfde
commit 27c2a4c51e
2 changed files with 248 additions and 29 deletions

View File

@@ -28,6 +28,8 @@ class AccountManager {
this.accountServiceFilter = '';
this.userSearchTerm = '';
this.userRoleFilter = '';
this.mobileBreakpoint = 900;
this.boundResizeHandler = null;
this.configureNotifications();
this.initPromise = this.init();
this.pendingAccountAppId = undefined;
@@ -121,6 +123,7 @@ class AccountManager {
}
this.setupEventListeners();
this.setupResponsiveShell();
this.loadModals(); // Load modals từ file riêng
// Single-page navigation based on hash
this.handleRoute(location.hash || '#dashboard');
@@ -129,6 +132,9 @@ class AccountManager {
handleRoute(hash) {
const route = (hash || '#dashboard').replace('#', '') || 'dashboard';
if (this.isMobileViewport()) {
this.closeMobileNav();
}
this.renderView(route);
}
@@ -180,6 +186,74 @@ class AccountManager {
});
}
isMobileViewport() {
return window.matchMedia(`(max-width: ${this.mobileBreakpoint}px)`).matches;
}
setupResponsiveShell() {
const menuBtn = document.getElementById('mobileMenuBtn');
const backdrop = document.getElementById('sidebarBackdrop');
if (menuBtn && !menuBtn.dataset.boundClick) {
menuBtn.addEventListener('click', () => this.toggleMobileNav());
menuBtn.dataset.boundClick = 'true';
}
if (backdrop && !backdrop.dataset.boundClick) {
backdrop.addEventListener('click', () => this.closeMobileNav());
backdrop.dataset.boundClick = 'true';
}
document.querySelectorAll('[data-nav]').forEach(link => {
if (!link.dataset.boundMobileClose) {
link.addEventListener('click', () => {
if (this.isMobileViewport()) {
this.closeMobileNav();
}
});
link.dataset.boundMobileClose = 'true';
}
});
if (!this.boundResizeHandler) {
this.boundResizeHandler = () => {
if (!this.isMobileViewport()) {
this.closeMobileNav();
}
};
window.addEventListener('resize', this.boundResizeHandler);
}
if (!this.isMobileViewport()) {
this.closeMobileNav();
}
}
toggleMobileNav() {
if (document.body.classList.contains('mobile-nav-open')) {
this.closeMobileNav();
return;
}
this.openMobileNav();
}
openMobileNav() {
if (!this.isMobileViewport()) return;
document.body.classList.add('mobile-nav-open');
const menuBtn = document.getElementById('mobileMenuBtn');
if (menuBtn) {
menuBtn.setAttribute('aria-expanded', 'true');
}
}
closeMobileNav() {
document.body.classList.remove('mobile-nav-open');
const menuBtn = document.getElementById('mobileMenuBtn');
if (menuBtn) {
menuBtn.setAttribute('aria-expanded', 'false');
}
}
async fetchApplications() {
const res = await fetch(`${this.apiBase}/applications`);
const data = await res.json();
@@ -284,6 +358,7 @@ class AccountManager {
// Close with Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeMobileNav();
this.closeModals();
}
});
@@ -306,6 +381,7 @@ class AccountManager {
// Account table row clicks
this.setupAccountRowListeners();
this.setupFilters();
this.setupResponsiveShell();
}
setupFormListeners() {
@@ -390,15 +466,15 @@ class AccountManager {
renderDashboard() {
return `
<div class="flex-1 flex flex-col p-6 space-y-6 min-h-0 overflow-auto">
<div class="dashboard-page flex-1 flex flex-col p-4 md:p-6 space-y-6 min-h-0 overflow-auto">
<!-- Title and Stats -->
<div class="flex items-end justify-between shrink-0">
<div class="dashboard-header flex items-end justify-between shrink-0">
<div>
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight leading-none">System Overview</h1>
<p class="text-xs text-on-surface-variant font-medium mt-1">Account & Service Management</p>
</div>
<div class="flex gap-2">
<a href="./accounts.html" class="py-1.5 px-3 bg-primary text-on-primary rounded-lg text-xs font-bold flex items-center gap-1.5 shadow-sm active:scale-95 duration-100">
<div class="dashboard-actions flex gap-2">
<a href="#accounts" class="py-1.5 px-3 bg-primary text-on-primary rounded-lg text-xs font-bold flex items-center gap-1.5 shadow-sm active:scale-95 duration-100">
<span class="material-symbols-outlined text-sm">add</span>
Add Account
</a>
@@ -406,7 +482,7 @@ class AccountManager {
</div>
<!-- Metric Grid -->
<div class="grid grid-cols-4 gap-4 shrink-0">
<div class="dashboard-stats grid grid-cols-4 gap-4 shrink-0">
<div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col">
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Applications</span>
<div class="flex items-baseline justify-between">
@@ -461,7 +537,7 @@ class AccountManager {
</div>
` : `
<div class="flex-1 flex items-center justify-center text-center">
<p class="text-sm text-on-surface-variant">No accounts yet. <a href="./accounts.html" class="text-primary font-bold">Create one</a></p>
<p class="text-sm text-on-surface-variant">No accounts yet. <a href="#accounts" class="text-primary font-bold">Create one</a></p>
</div>
`}
</div>
@@ -475,9 +551,9 @@ class AccountManager {
const pageInfo = this.getPaged(filteredAccounts, this.accountPage, this.accountPageSize);
this.accountPage = pageInfo.current;
return `
<div class="p-4 md:p-6 flex flex-col h-full overflow-hidden">
<div class="accounts-page p-4 md:p-6 flex flex-col h-full overflow-hidden">
<!-- Page Header -->
<div class="flex items-center justify-between mb-4 shrink-0">
<div class="page-header flex items-center justify-between mb-4 shrink-0">
<div>
<h1 class="text-xl font-extrabold text-on-surface tracking-tight">Accounts Management</h1>
<p class="text-[10px] text-on-surface-variant uppercase font-semibold tracking-widest mt-0.5">Administrative Access Control</p>
@@ -489,7 +565,7 @@ class AccountManager {
</div>
<!-- Filters -->
<div class="flex items-center gap-3 mb-4">
<div class="page-filters flex items-center gap-3 mb-4">
<div class="flex items-center gap-1.5">
<span class="text-[10px] font-bold uppercase text-on-surface-variant">Service</span>
<select id="serviceFilter" class="bg-surface-container-low border-slate-200 rounded-md text-[11px] py-1 px-2 pr-6 focus:ring-1 focus:ring-primary shadow-sm">
@@ -506,7 +582,7 @@ class AccountManager {
<!-- Accounts Table -->
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
${pageInfo.data.length > 0 ? `
<div class="overflow-y-auto overflow-x-auto flex-1">
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1">
<table class="w-full text-left border-collapse w-full">
<thead class="sticky top-0 z-10">
<tr class="bg-slate-50 border-b border-slate-200">
@@ -545,7 +621,7 @@ class AccountManager {
</tbody>
</table>
</div>
<div class="flex items-center justify-between px-4 py-2 border-t border-slate-200 bg-slate-50 text-xs text-slate-600" id="accountsPager">
<div class="page-pager flex items-center justify-between px-4 py-2 border-t border-slate-200 bg-slate-50 text-xs text-slate-600" id="accountsPager">
<span>Showing ${pageInfo.start}-${pageInfo.end} of ${pageInfo.total}</span>
<div class="flex items-center gap-2">
<button class="account-page-btn px-2 py-1 border border-slate-200 rounded text-xs ${pageInfo.current === 1 ? 'opacity-50 cursor-not-allowed' : ''}" data-page="${pageInfo.current - 1}" ${pageInfo.current === 1 ? 'disabled' : ''}>Prev</button>
@@ -575,9 +651,9 @@ class AccountManager {
const pageInfo = this.getPaged(filteredApps, this.appPage, this.appPageSize);
this.appPage = pageInfo.current;
return `
<div class="flex flex-col p-6 overflow-hidden h-full">
<div class="apps-page flex flex-col p-4 md:p-6 overflow-hidden h-full">
<!-- Header Section -->
<div class="flex items-center justify-between gap-6 mb-6 shrink-0">
<div class="page-header flex items-center justify-between gap-6 mb-6 shrink-0">
<div>
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight">Applications</h1>
<p class="text-sm text-on-surface-variant">Manage and monitor active infrastructure services.</p>
@@ -589,7 +665,7 @@ class AccountManager {
</div>
<!-- Dashboard Stats -->
<div class="grid grid-cols-3 gap-4 mb-6 shrink-0">
<div class="apps-stats grid grid-cols-3 gap-4 mb-6 shrink-0">
<div class="bg-surface-container-lowest px-4 py-3 rounded-xl shadow-sm border border-outline-variant/5 flex items-center gap-4">
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-xl">lan</span>
@@ -621,11 +697,11 @@ class AccountManager {
<!-- Applications List -->
<div class="bg-surface-container-lowest rounded-xl shadow-sm border border-outline-variant/10 overflow-hidden flex flex-col flex-1 min-h-0">
<div class="px-4 py-3 border-b border-outline-variant/10 flex items-center gap-2 bg-surface-container-low/40">
<div class="page-filters px-4 py-3 border-b border-outline-variant/10 flex items-center gap-2 bg-surface-container-low/40">
<span class="text-[10px] font-bold uppercase text-on-surface-variant">Search</span>
<input id="appSearch" class="flex-1 bg-white border border-slate-200 rounded-md text-[11px] py-1 px-2 focus:ring-1 focus:ring-primary shadow-sm" placeholder="Search by name, type, description, url">
</div>
<div class="overflow-y-auto flex-1">
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-surface-container-lowest z-10">
<tr class="bg-surface-container-low/30 border-b border-outline-variant/10">
@@ -677,7 +753,7 @@ class AccountManager {
</tbody>
</table>
</div>
<div class="flex items-center justify-between px-4 py-2 border-t border-outline-variant/10 bg-surface-container-low/60 text-xs text-on-surface-variant" id="appsPager">
<div class="page-pager flex items-center justify-between px-4 py-2 border-t border-outline-variant/10 bg-surface-container-low/60 text-xs text-on-surface-variant" id="appsPager">
<span>Showing ${pageInfo.start}-${pageInfo.end} of ${pageInfo.total}</span>
<div class="flex items-center gap-2">
<button class="app-page-btn px-2 py-1 border border-slate-200 rounded text-xs ${pageInfo.current === 1 ? 'opacity-50 cursor-not-allowed' : ''}" data-page="${pageInfo.current - 1}" ${pageInfo.current === 1 ? 'disabled' : ''}>Prev</button>
@@ -1501,8 +1577,8 @@ class AccountManager {
const pageInfo = this.getPaged(filteredUsers, this.userPage, this.userPageSize);
this.userPage = pageInfo.current;
return `
<div class="p-6 w-full h-full flex flex-col overflow-hidden">
<div class="flex justify-between items-center mb-6 shrink-0">
<div class="users-page p-4 md:p-6 w-full h-full flex flex-col overflow-hidden">
<div class="page-header flex justify-between items-center mb-6 shrink-0">
<h1 class="text-2xl font-bold text-slate-900 dark:text-slate-50">Users Management</h1>
<button id="addUserBtn" class="bg-primary hover:bg-primary-dim text-on-primary font-bold py-2 px-4 rounded-lg transition-all active:scale-95 flex items-center gap-2">
<span class="material-symbols-outlined text-base">add</span>
@@ -1511,7 +1587,7 @@ class AccountManager {
</div>
<!-- Search and Filter -->
<div class="mb-4 flex gap-3 shrink-0">
<div class="users-controls mb-4 flex gap-3 shrink-0">
<div class="flex-1 relative">
<input type="text" id="userSearch" placeholder="Search users..." value="${this.userSearchTerm}" class="w-full px-4 py-2 border border-outline-variant/30 rounded-lg bg-surface-container-low dark:bg-slate-800 focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 material-symbols-outlined">search</span>
@@ -1524,7 +1600,7 @@ class AccountManager {
<!-- Users Table -->
<div class="flex-1 overflow-hidden border border-outline-variant/20 rounded-lg flex flex-col min-h-0">
<div class="overflow-auto flex-1">
<div class="table-wrap overflow-auto flex-1">
<table class="w-full text-sm">
<thead class="bg-slate-100 dark:bg-slate-800 border-b border-outline-variant/20 sticky top-0">
<tr>
@@ -1574,7 +1650,7 @@ class AccountManager {
</tbody>
</table>
</div>
<div class="flex items-center justify-between px-4 py-2 border-t border-outline-variant/20 bg-slate-50 dark:bg-slate-800/40 text-xs text-on-surface-variant" id="usersPager">
<div class="page-pager flex items-center justify-between px-4 py-2 border-t border-outline-variant/20 bg-slate-50 dark:bg-slate-800/40 text-xs text-on-surface-variant" id="usersPager">
<span>Showing ${pageInfo.start}-${pageInfo.end} of ${pageInfo.total}</span>
<div class="flex items-center gap-2">
<button class="user-page-btn px-2 py-1 border border-slate-200 rounded text-xs ${pageInfo.current === 1 ? 'opacity-50 cursor-not-allowed' : ''}" data-page="${pageInfo.current - 1}" ${pageInfo.current === 1 ? 'disabled' : ''}>Prev</button>

View File

@@ -43,11 +43,150 @@
.modal-backdrop.open .modal-content {
transform: scale(1);
}
body.app-shell {
overflow: hidden;
}
#mobileMenuBtn,
#sidebarBackdrop {
display: none;
}
@media (max-width: 900px) {
body.app-shell {
width: 100%;
min-height: 100dvh;
height: 100dvh;
overflow: hidden;
position: relative;
}
#mobileMenuBtn {
display: inline-flex;
align-items: center;
justify-content: center;
}
#sidebarBackdrop {
display: block;
position: fixed;
inset: 0;
z-index: 70;
background: rgba(15, 23, 42, 0.45);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out;
}
#appSidebar {
position: fixed;
inset: 0 auto 0 0;
height: 100dvh;
width: min(82vw, 16rem);
z-index: 80;
transform: translateX(-100%);
transition: transform 0.2s ease-in-out;
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.35);
}
body.mobile-nav-open #sidebarBackdrop {
opacity: 1;
pointer-events: auto;
}
body.mobile-nav-open #appSidebar {
transform: translateX(0);
}
#appMain {
width: 100%;
min-width: 0;
height: 100dvh;
}
#appMain > header {
padding-left: 0.75rem;
padding-right: 0.75rem;
gap: 0.5rem;
}
.topbar-actions {
gap: 0.5rem;
}
#profileBtn {
padding: 0.4rem 0.55rem;
}
#profileBtn .profile-meta {
display: none;
}
.dashboard-header,
.page-header,
.page-filters,
.users-controls {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.page-filters > div,
.users-controls > div,
.users-controls select {
width: 100%;
}
.dashboard-actions,
.page-header button,
.page-header > button {
width: 100%;
}
.dashboard-stats,
.apps-stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.table-wrap {
overflow-x: auto;
}
.table-wrap table {
min-width: 700px;
}
.page-pager {
gap: 0.5rem;
flex-direction: column;
align-items: flex-start;
}
.modal-backdrop {
align-items: flex-end;
}
.modal-backdrop .modal-content {
width: calc(100% - 1rem);
max-height: min(88dvh, 700px);
margin: 0.5rem;
overflow-y: auto;
border-radius: 0.9rem;
}
}
@media (max-width: 560px) {
.dashboard-stats,
.apps-stats {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body class="bg-background text-on-surface antialiased flex h-screen w-screen">
<body class="app-shell bg-background text-on-surface antialiased flex h-screen w-screen">
<!-- SideNavBar -->
<aside class="h-screen w-56 flex flex-col bg-slate-100 dark:bg-slate-900 font-manrope text-sm font-medium border-r border-outline-variant/10 shrink-0">
<aside id="appSidebar" class="h-screen w-56 flex flex-col bg-slate-100 dark:bg-slate-900 font-manrope text-sm font-medium border-r border-outline-variant/10 shrink-0">
<div class="flex flex-col h-full py-6">
<!-- Header -->
<div class="px-6 mb-8">
@@ -80,17 +219,21 @@
</div>
</aside>
<button id="sidebarBackdrop" type="button" aria-label="Close navigation menu"></button>
<!-- Main Content -->
<main class="flex-1 flex flex-col h-screen min-w-0">
<main id="appMain" class="flex-1 flex flex-col h-screen min-w-0">
<!-- TopAppBar -->
<header class="h-14 flex items-center justify-between px-6 bg-slate-50/80 dark:bg-slate-950/80 backdrop-blur-xl border-b border-outline-variant/10 shrink-0">
<div class="flex items-center gap-4 flex-1">
<button id="mobileMenuBtn" type="button" class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" aria-label="Open navigation menu" aria-controls="appSidebar" aria-expanded="false">
<span class="material-symbols-outlined">menu</span>
</button>
</div>
<div class="flex items-center gap-4">
<button id="profileBtn" type="button" class="flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" title="Edit profile">
<div class="topbar-actions flex items-center gap-4">
<button id="profileBtn" type="button" class="profile-btn flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" title="Edit profile">
<span class="material-symbols-outlined text-slate-600 dark:text-slate-400">account_circle</span>
<div class="flex flex-col">
<div class="profile-meta flex flex-col">
<span id="accountUsername" class="text-xs font-semibold text-slate-900 dark:text-slate-50">User Account</span>
<span id="accountRole" class="text-[10px] text-slate-500 dark:text-slate-400">Administrator</span>
</div>