Skip to content

Commit 7ab376d

Browse files
authored
feat: add HTMX boosting for SPA-like navigation (#9)
* feat: add HTMX boosting for SPA-like navigation - Enable hx-boost on body for automatic AJAX navigation - Add main-content ID and HTMX target/select attributes for partial page updates - Add sidebar-nav-link class to navigation items - Implement client-side active state management on navigation - Disable boost on logout link to preserve full page reload - Format HTML with consistent indentation and spacing * Fixes Signed-off-by: Dan Webb <dan.webb@damacus.io> --------- Signed-off-by: Dan Webb <dan.webb@damacus.io>
1 parent 3d9ddbc commit 7ab376d

File tree

1 file changed

+83
-29
lines changed

1 file changed

+83
-29
lines changed

views/layouts/base.html

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{{ define "base" }}
22
<!DOCTYPE html>
33
<html lang="en" class="dark">
4+
45
<head>
56
<meta charset="UTF-8">
67
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -34,81 +35,102 @@
3435
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
3536

3637
<style>
37-
body { font-family: 'Inter', sans-serif; }
38+
body {
39+
font-family: 'Inter', sans-serif;
40+
}
41+
3842
/* Showing HTMX attributes for visualization only */
39-
.htmx-indicator { opacity: 0; transition: opacity 200ms ease-in; }
40-
.htmx-request .htmx-indicator { opacity: 1; }
41-
[x-cloak] { display: none !important; }
43+
.htmx-indicator {
44+
opacity: 0;
45+
transition: opacity 200ms ease-in;
46+
}
47+
48+
.htmx-request .htmx-indicator {
49+
opacity: 1;
50+
}
51+
52+
[x-cloak] {
53+
display: none !important;
54+
}
4255
</style>
4356
</head>
44-
<body class="bg-background text-zinc-100 h-screen w-screen flex overflow-hidden" x-data="{ collapsed: false }">
57+
58+
<body class="bg-background text-zinc-100 h-screen w-screen flex overflow-hidden" x-data="{ collapsed: false }"
59+
hx-boost="true" hx-target="#main-content" hx-select="#main-content" hx-swap="outerHTML">
4560

4661
<!-- SIDEBAR -->
47-
<div
48-
class="border-r border-border bg-surface flex flex-col transition-all duration-300 ease-in-out w-64"
49-
:class="collapsed ? '!w-20' : ''"
50-
>
62+
<div class="border-r border-border bg-surface flex flex-col transition-all duration-300 ease-in-out w-64"
63+
:class="collapsed ? '!w-20' : ''">
5164
<div class="p-6 flex items-center gap-3 border-b border-border/50 overflow-hidden whitespace-nowrap">
52-
<div class="w-8 h-8 bg-zinc-100 rounded-lg flex-shrink-0 flex items-center justify-center cursor-pointer" @click="collapsed = !collapsed">
65+
<div class="w-8 h-8 bg-zinc-100 rounded-lg flex-shrink-0 flex items-center justify-center cursor-pointer"
66+
@click="collapsed = !collapsed">
5367
<i data-lucide="box" class="text-black"></i>
5468
</div>
55-
<span class="font-bold text-lg tracking-tight transition-opacity duration-300 opacity-100" :class="collapsed ? '!opacity-0 !w-0' : ''">IronBuckets</span>
69+
<span class="font-bold text-lg tracking-tight transition-opacity duration-300 opacity-100"
70+
:class="collapsed ? '!opacity-0 !w-0' : ''">IronBuckets</span>
5671
</div>
5772

5873
<div class="flex-1 py-6 px-3 space-y-1 overflow-y-auto overflow-x-hidden">
5974

6075
<!-- Section Header -->
6176
<div class="px-3 text-xs font-semibold text-zinc-500 uppercase tracking-wider mb-2 transition-opacity duration-300 whitespace-nowrap opacity-100"
62-
:class="collapsed ? '!opacity-0 !h-0 !overflow-hidden !mb-0' : ''">
63-
Cluster
77+
:class="collapsed ? '!opacity-0 !h-0 !overflow-hidden !mb-0' : ''">
78+
Cluster
6479
</div>
6580

6681
<!-- Nav Items -->
67-
<a href="/" class="w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "overview" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
82+
<a href="/"
83+
class="sidebar-nav-link w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "overview" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
6884
<i data-lucide="activity" size="18" class="flex-shrink-0"></i>
6985
<span class="opacity-100" :class="collapsed ? '!opacity-0 !w-0' : ''">Overview</span>
7086
</a>
7187

72-
<a href="/drives" class="w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "drives" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
88+
<a href="/drives"
89+
class="sidebar-nav-link w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "drives" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
7390
<i data-lucide="hard-drive" size="18" class="flex-shrink-0"></i>
7491
<span class="opacity-100" :class="collapsed ? '!opacity-0 !w-0' : ''">Drives</span>
7592
</a>
7693

7794
<!-- Section Header -->
7895
<div class="px-3 text-xs font-semibold text-zinc-500 uppercase tracking-wider mb-2 mt-6 transition-opacity duration-300 whitespace-nowrap opacity-100"
79-
:class="collapsed ? '!opacity-0 !h-0 !overflow-hidden !mt-2 !mb-0' : ''">
80-
Access
96+
:class="collapsed ? '!opacity-0 !h-0 !overflow-hidden !mt-2 !mb-0' : ''">
97+
Access
8198
</div>
8299

83-
<a href="/users" class="w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "users" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
100+
<a href="/users"
101+
class="sidebar-nav-link w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "users" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
84102
<i data-lucide="user" size="18" class="flex-shrink-0"></i>
85103
<span class="opacity-100" :class="collapsed ? '!opacity-0 !w-0' : ''">Users</span>
86104
</a>
87105

88-
<a href="/groups" class="w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "groups" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
106+
<a href="/groups"
107+
class="sidebar-nav-link w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "groups" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
89108
<i data-lucide="users" size="18" class="flex-shrink-0"></i>
90109
<span class="opacity-100" :class="collapsed ? '!opacity-0 !w-0' : ''">Groups</span>
91110
</a>
92111

93-
<a href="/buckets" class="w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "buckets" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
112+
<a href="/buckets"
113+
class="sidebar-nav-link w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "buckets" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
94114
<i data-lucide="container" size="18" class="flex-shrink-0"></i>
95115
<span class="opacity-100" :class="collapsed ? '!opacity-0 !w-0' : ''">Buckets</span>
96116
</a>
97117

98118
<!-- Section Header -->
99119
<div class="px-3 text-xs font-semibold text-zinc-500 uppercase tracking-wider mb-2 mt-6 transition-opacity duration-300 whitespace-nowrap opacity-100"
100-
:class="collapsed ? '!opacity-0 !h-0 !overflow-hidden !mt-2 !mb-0' : ''">
101-
System
120+
:class="collapsed ? '!opacity-0 !h-0 !overflow-hidden !mt-2 !mb-0' : ''">
121+
System
102122
</div>
103123

104-
<a href="/settings" class="w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "settings" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
124+
<a href="/settings"
125+
class="sidebar-nav-link w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap {{ if eq .ActiveNav "settings" }}bg-white/10 text-white{{ else }}text-zinc-400 hover:text-white hover:bg-white/5{{ end }}">
105126
<i data-lucide="settings" size="18" class="flex-shrink-0"></i>
106127
<span class="opacity-100" :class="collapsed ? '!opacity-0 !w-0' : ''">Settings</span>
107128
</a>
108129

109130
<!-- Logout at bottom -->
110131
<div class="mt-auto pt-4 border-t border-border">
111-
<a href="/logout" class="w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap text-zinc-400 hover:text-red-400 hover:bg-red-500/10">
132+
<a href="/logout" hx-boost="false"
133+
class="w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap text-zinc-400 hover:text-red-400 hover:bg-red-500/10">
112134
<i data-lucide="log-out" size="18" class="flex-shrink-0"></i>
113135
<span class="opacity-100" :class="collapsed ? '!opacity-0 !w-0' : ''">Logout</span>
114136
</a>
@@ -117,17 +139,19 @@
117139
</div>
118140

119141
<!-- MAIN CONTENT -->
120-
<main class="flex-1 flex flex-col min-w-0 bg-background overflow-auto">
142+
<main id="main-content" class="flex-1 flex flex-col min-w-0 bg-background overflow-auto">
121143
<!-- Header -->
122-
<header class="h-16 border-b border-border flex items-center justify-between px-8 bg-background/50 backdrop-blur-sm sticky top-0 z-10">
144+
<header
145+
class="h-16 border-b border-border flex items-center justify-between px-8 bg-background/50 backdrop-blur-sm sticky top-0 z-10">
123146
<div class="flex items-center gap-2">
124147
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
125148
<span class="text-sm font-medium text-zinc-300">Cluster Online</span>
126-
<span class="text-xs text-zinc-500 ml-2" hx-get="/api/server/version" hx-trigger="load" hx-swap="innerHTML"></span>
149+
<span class="text-xs text-zinc-500 ml-2" hx-get="/api/server/version" hx-trigger="load"
150+
hx-swap="innerHTML"></span>
127151
</div>
128152
<!-- Right side items if needed -->
129153
<div class="flex items-center gap-4">
130-
<!-- Removed Restart Button, moved to Settings -->
154+
<!-- Removed Restart Button, moved to Settings -->
131155
</div>
132156
</header>
133157

@@ -145,10 +169,40 @@
145169
lucide.createIcons();
146170

147171
// Re-init icons on HTMX content swaps
148-
document.body.addEventListener('htmx:afterSwap', function(evt) {
172+
document.body.addEventListener('htmx:afterSwap', function (evt) {
149173
lucide.createIcons();
150174
});
175+
176+
// Update sidebar active state on navigation
177+
document.body.addEventListener('htmx:pushedIntoHistory', function (evt) {
178+
const path = window.location.pathname;
179+
const links = document.querySelectorAll('.sidebar-nav-link');
180+
181+
links.forEach(link => {
182+
const href = link.getAttribute('href');
183+
// Simple exact match or startswith for sub-paths if needed.
184+
// For now, exact match or /users matching /users/create is good practice,
185+
// but let's stick to exact logic or simple logic matching the Go template:
186+
// Go template used exact match mostly.
187+
188+
let isActive = false;
189+
if (href === '/' && path === '/') {
190+
isActive = true;
191+
} else if (href !== '/' && path.startsWith(href)) {
192+
isActive = true;
193+
}
194+
195+
if (isActive) {
196+
link.classList.remove('text-zinc-400', 'hover:text-white', 'hover:bg-white/5');
197+
link.classList.add('bg-white/10', 'text-white');
198+
} else {
199+
link.classList.add('text-zinc-400', 'hover:text-white', 'hover:bg-white/5');
200+
link.classList.remove('bg-white/10', 'text-white');
201+
}
202+
});
203+
});
151204
</script>
152205
</body>
206+
153207
</html>
154208
{{ end }}

0 commit comments

Comments
 (0)