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}