v0.4.1: app filtering + clickable dashboard stat cards
Add filter bar (Mind/Futó/Leállítva/Telepíthető) to Alkalmazások page with URL-based filter persistence. Dashboard stat cards are now clickable links that navigate to the filtered stacks view. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,18 +7,18 @@
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-running">
|
||||
<a href="/stacks?filter=running" class="stat-card stat-running">
|
||||
<div class="stat-value">{{.RunningCount}}</div>
|
||||
<div class="stat-label">Futó alkalmazás</div>
|
||||
</div>
|
||||
<div class="stat-card stat-stopped">
|
||||
</a>
|
||||
<a href="/stacks?filter=stopped" class="stat-card stat-stopped">
|
||||
<div class="stat-value">{{.StoppedCount}}</div>
|
||||
<div class="stat-label">Leállítva</div>
|
||||
</div>
|
||||
<div class="stat-card stat-total">
|
||||
</a>
|
||||
<a href="/stacks" class="stat-card stat-total">
|
||||
<div class="stat-value">{{.TotalCount}}</div>
|
||||
<div class="stat-label">Összes alkalmazás</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{if .SystemInfo.TotalMemMB}}
|
||||
|
||||
@@ -8,9 +8,16 @@
|
||||
</div>
|
||||
<div id="sync-toast" class="sync-toast" style="display:none"></div>
|
||||
|
||||
<div class="filter-bar" id="filter-bar">
|
||||
<button class="filter-btn active" data-filter="all">Mind <span class="filter-count" id="count-all"></span></button>
|
||||
<button class="filter-btn" data-filter="running">Futó <span class="filter-count" id="count-running"></span></button>
|
||||
<button class="filter-btn" data-filter="stopped">Leállítva <span class="filter-count" id="count-stopped"></span></button>
|
||||
<button class="filter-btn" data-filter="available">Telepíthető <span class="filter-count" id="count-available"></span></button>
|
||||
</div>
|
||||
|
||||
<div class="stack-grid">
|
||||
{{range .Stacks}}
|
||||
<div class="stack-detail-card stack-state-{{stateColor .State}}"{{if not .Protected}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
||||
<div class="stack-detail-card stack-state-{{stateColor .State}}" data-filter-state="{{filterCategory .State .Deployed}}"{{if not .Protected}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
||||
<div class="stack-detail-header">
|
||||
<div class="stack-title-row">
|
||||
<img class="stack-logo-lg" src="{{logoURL .Meta.Slug}}"
|
||||
@@ -72,5 +79,55 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var cards = document.querySelectorAll('.stack-detail-card[data-filter-state]');
|
||||
var counts = {all: cards.length, running: 0, stopped: 0, available: 0};
|
||||
|
||||
cards.forEach(function(card) {
|
||||
var cat = card.getAttribute('data-filter-state');
|
||||
if (counts[cat] !== undefined) counts[cat]++;
|
||||
});
|
||||
|
||||
['all', 'running', 'stopped', 'available'].forEach(function(f) {
|
||||
var el = document.getElementById('count-' + f);
|
||||
if (el) el.textContent = '(' + counts[f] + ')';
|
||||
});
|
||||
|
||||
function applyFilter(filter) {
|
||||
cards.forEach(function(card) {
|
||||
if (filter === 'all' || card.getAttribute('data-filter-state') === filter) {
|
||||
card.style.display = '';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.filter-btn').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-filter') === filter);
|
||||
});
|
||||
|
||||
var url = new URL(window.location);
|
||||
if (filter === 'all') {
|
||||
url.searchParams.delete('filter');
|
||||
} else {
|
||||
url.searchParams.set('filter', filter);
|
||||
}
|
||||
history.replaceState(null, '', url);
|
||||
}
|
||||
|
||||
document.querySelectorAll('.filter-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
applyFilter(this.getAttribute('data-filter'));
|
||||
});
|
||||
});
|
||||
|
||||
var urlFilter = new URLSearchParams(window.location.search).get('filter');
|
||||
if (urlFilter && ['running', 'stopped', 'available'].indexOf(urlFilter) !== -1) {
|
||||
applyFilter(urlFilter);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
{{template "layout_end" .}}
|
||||
{{end}}
|
||||
|
||||
@@ -822,6 +822,54 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
|
||||
border-color: rgba(218, 54, 51, 0.3);
|
||||
}
|
||||
|
||||
/* --- Filter bar --- */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
border-color: var(--accent-blue);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: var(--accent-blue);
|
||||
border-color: var(--accent-blue);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-count {
|
||||
opacity: 0.7;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Clickable stat cards */
|
||||
a.stat-card {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
a.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Clickable cards */
|
||||
[data-href] { cursor: pointer; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user