history.templ RawHistoryFolder

1package systemviews
2
3import (
4	"fmt"
5	"strings"
6	"github.com/bnema/dumber/internal/domain/entity"
7)
8
9templ HistoryView(data historyRenderData) {
10	@Alert("success", data.Notice)
11	@Alert("error", data.Error)
12	@Section("sv-history-controls", "Manage history") {
13		@HistoryControls(data)
14	}
15	@Section("sv-history-domains", "Top domains") {
16		@HistoryDomains(data)
17	}
18	@Section("sv-history-timeline", "Recent visits") {
19		@HistoryTimeline(data)
20	}
21}
22
23templ HistoryControls(data historyRenderData) {
24	<form class="sv-toolbar sv-history-search" data-sv-action="history.search">
25		<label class="sv-search-label">
26			<span>Search</span>
27			<input data-sv-history-search data-sv-autofocus name="query" type="search" value={ data.Query } placeholder="Search title or URL" autocomplete="off" autofocus/>
28		</label>
29		<button class="sv-button" type="submit">@ButtonIconLabel(iconSearch, "Search")</button>
30		<button class="sv-button sv-button-secondary" type="button" data-sv-action="history.clear">@ButtonIconLabel(iconX, "Clear")</button>
31	</form>
32	if showHistoryFilters(data) {
33		<div class="sv-active-filters">
34			if data.Query != "" {
35				<span class="sv-filter-chip">Query: { data.Query }</span>
36			}
37			if data.DomainFilter != "" {
38				<span class="sv-filter-chip">Domain: { data.DomainFilter } <button type="button" data-sv-action="history.clearDomain" aria-label="Clear domain filter">@IconOnly(iconX, "Clear domain filter")</button></span>
39			}
40		</div>
41	}
42	<div class="sv-history-actions-panel">
43		<div class="sv-button-row" aria-label="History pagination">
44			<span class="sv-meta">{ historyShowingLabel(data) }</span>
45		</div>
46		<div class="sv-button-row" aria-label="History cleanup">
47			<span class="sv-meta">Clean up</span>
48			for _, item := range historyCleanupItems() {
49				<button type="button" class="sv-danger-link" data-sv-action="history.deleteRange" data-range={ item.RangeID } data-sv-confirm={ item.Confirm }>{ item.Label }</button>
50			}
51		</div>
52	</div>
53}
54
55templ HistoryDomains(data historyRenderData) {
56	if data.Loading {
57		@LoadingPanel("Loading top domains…")
58	} else if len(data.Domains) == 0 {
59		@EmptyState("No domain statistics yet")
60	} else {
61		<div class="sv-domain-list">
62			for _, domain := range data.Domains {
63				if domain != nil && domain.Domain != "" {
64					{{ domainKey := historyDomainActionKey(domain) }}
65					{{ displayDomain := historyDomainDisplayLabel(domain) }}
66					<span class="sv-domain-chip">
67						<span class="sv-domain-chip-main">
68							<button type="button" class="sv-domain-filter" data-sv-action="history.filterDomain" data-domain={ domainKey }>{ displayDomain }</button>
69							<span class="sv-domain-meta">{ fmt.Sprintf("%dp · %dv", domain.PageCount, domain.TotalVisits) }</span>
70						</span>
71						<button type="button" class="sv-domain-delete" data-sv-action="history.deleteDomain" data-domain={ domainKey } data-sv-confirm={ "Delete all history for " + displayDomain + "?" } aria-label={ "Delete history for " + displayDomain }><span aria-hidden="true">×</span><span class="sv-sr-only">Delete history for { displayDomain }</span></button>
72					</span>
73				}
74			}
75		</div>
76	}
77}
78
79templ HistoryTimeline(data historyRenderData) {
80	if data.Loading {
81		@LoadingPanel("Loading recent visits…")
82	} else {
83		<div class="sv-history-timeline-list" data-sv-history-timeline>
84			{{ groups := groupHistoryEntries(data.Entries) }}
85			if len(groups) == 0 {
86				@EmptyState(historyTimelineEmptyMessage(data))
87			} else {
88				@HistoryTimelineGroups(data.Entries, "")
89			}
90			@HistoryLoadMore(data)
91		</div>
92	}
93}
94
95templ HistoryTimelineAppend(data historyRenderData) {
96	@HistoryTimelineAppendGroups(data.Entries, data.AppendSkipFirstDate)
97	@HistoryLoadMore(data)
98}
99
100templ HistoryTimelineGroups(entries []*entity.HistoryEntry, skipFirstDate string) {
101	{{ groups := groupHistoryEntries(entries) }}
102	for _, group := range groups {
103		@HistoryTimelineGroup(group, true)
104	}
105}
106
107templ HistoryTimelineAppendGroups(entries []*entity.HistoryEntry, mergeFirstDate string) {
108	{{ groups := groupHistoryEntries(entries) }}
109	for index, group := range groups {
110		if index == 0 && group.Date == mergeFirstDate {
111			<div data-sv-history-merge-date={ group.Date }>
112				for _, entry := range group.Entries {
113					if entry != nil {
114						@HistoryItem(entry)
115					}
116				}
117			</div>
118		} else {
119			@HistoryTimelineGroup(group, true)
120		}
121	}
122}
123
124templ HistoryTimelineGroup(group historyTimelineGroup, showHeading bool) {
125	<section class="sv-timeline-group" data-sv-history-date={ group.Date }>
126		if showHeading {
127			<h3>{ group.Label }</h3>
128		}
129		<ul class="sv-list">
130			for _, entry := range group.Entries {
131				if entry != nil {
132					@HistoryItem(entry)
133				}
134			}
135		</ul>
136	</section>
137}
138
139templ HistoryLoadMore(data historyRenderData) {
140	<div class="sv-history-load-more" data-sv-history-load-more-container>
141		if showHistoryLoadMore(data) {
142			<button type="button" class="sv-button sv-button-secondary" data-sv-action="history.loadMore" data-before={ historyLoadMoreCursor(data) } data-sv-history-load-more>Load older visits</button>
143		} else if strings.TrimSpace(data.Query) == "" && countHistoryEntries(data.Entries) > 0 {
144			<p class="sv-meta">End of history</p>
145		}
146	</div>
147}
148
149templ HistoryItem(entry *entity.HistoryEntry) {
150	<li class="sv-list-row">
151		<article class="sv-history-item" data-sv-history-row data-history-id={ fmt.Sprintf("%d", entry.ID) }>
152			<div class="sv-history-content">
153				{{ faviconURL := historyItemFaviconURL(entry) }}
154				<span class={ historyFaviconClass(faviconURL) } aria-hidden="true">
155					<span class="sv-history-favicon-fallback">{ historyFaviconFallback(entry) }</span>
156					if faviconURL != "" {
157						<img src={ templ.SafeURL(faviconURL) } alt="" width="24" height="24" loading="lazy" decoding="async" referrerpolicy="same-origin" onerror="this.parentElement.classList.remove('sv-history-favicon-has-image'); this.remove()"/>
158					}
159				</span>
160				<div class="sv-history-main">
161					<a class="sv-link sv-history-open" href={ templ.SafeURL(sanitizeHref(entry.URL)) }>{ historyItemLabel(entry) }</a>
162					<p class="sv-history-url" title={ entry.URL }>{ historyItemURL(entry) }</p>
163					<p class="sv-history-meta">{ historyItemMeta(entry) }</p>
164				</div>
165			</div>
166			<div class="sv-history-row-actions">
167				<a class="sv-button sv-button-secondary" href={ templ.SafeURL(sanitizeHref(entry.URL)) }>@ButtonIconLabel(iconExternal, "Open")</a>
168				<button type="button" class="sv-button sv-button-secondary sv-button-danger sv-button-icon-only" data-sv-action="history.deleteEntry" data-id={ fmt.Sprintf("%d", entry.ID) } data-sv-confirm="Delete this history entry?" aria-label="Delete this history entry" disabled?={ entry.ID <= 0 }>@IconOnly(iconTrash, "Delete this history entry")</button>
169			</div>
170		</article>
171	</li>
172}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close