v0.30.0 eb3068f

favorites.templ RawHistoryFolder

1package systemviews
2
3import (
4	"fmt"
5	"github.com/bnema/dumber/internal/domain/entity"
6)
7
8templ FavoritesView(data favoritesRenderData) {
9	@Alert("success", data.Notice)
10	@Alert("error", data.Error)
11	@Section("sv-favorites-controls", "Manage favorites") {
12		@FavoritesControls(data)
13	}
14	@Section("sv-favorites-list", "Favorites") {
15		@Meta(favoritesSummary(data))
16		if len(data.Favorites) == 0 {
17			@EmptyState("No favorites")
18		} else {
19			<ul class="sv-list">
20				for _, favorite := range data.Favorites {
21					if favorite != nil {
22						@FavoriteItem(favorite, data.Folders, data.Tags)
23					}
24				}
25			</ul>
26		}
27	}
28	@Section("sv-folders-list", "Folders") {
29		@Meta(foldersSummary(data))
30		@FolderCreateForm()
31		if len(data.Folders) == 0 {
32			@EmptyState("No folders")
33		} else {
34			<ul class="sv-list">
35				for _, folder := range data.Folders {
36					if folder != nil {
37						@FolderItem(folder)
38					}
39				}
40			</ul>
41		}
42	}
43	@Section("sv-tags-list", "Tags") {
44		@Meta(tagsSummary(data))
45		@TagCreateForm()
46		if len(data.Tags) == 0 {
47			@EmptyState("No tags")
48		} else {
49			<ul class="sv-list">
50				for _, tag := range data.Tags {
51					if tag != nil {
52						@TagItem(tag)
53					}
54				}
55			</ul>
56		}
57	}
58}
59
60templ FavoritesControls(data favoritesRenderData) {
61	<form class="sv-card-form" data-sv-action="favorite.create">
62		<label><span>URL</span><input name="url" type="url" placeholder="https://example.com" required/></label>
63		<label><span>Title</span><input name="title" type="text" placeholder="Optional title"/></label>
64		@FolderSelect("folder_id", data.Folders, nil, "Folder")
65		@FavoriteCreateTagPicker(data.Tags)
66		<button class="sv-button" type="submit">@ButtonIconLabel(iconPlus, "Add favorite")</button>
67	</form>
68	<div class="sv-filter-panel">
69		<div class="sv-button-row">
70			<span class="sv-meta">Folders</span>
71			<button type="button" class={ favoriteFilterClass(favoriteAllFilterActive(data)) } aria-pressed={ fmt.Sprintf("%t", favoriteAllFilterActive(data)) } data-sv-action="favorite.clearFilters">All</button>
72			<button type="button" class={ favoriteFilterClass(favoriteFolderFilterActive(data, entity.FolderID(0))) } aria-pressed={ fmt.Sprintf("%t", favoriteFolderFilterActive(data, entity.FolderID(0))) } data-sv-action="favorite.filterFolder" data-folder-id="root">Unfiled</button>
73			for _, folder := range data.Folders {
74				if folder != nil {
75					<button type="button" class={ favoriteFilterClass(favoriteFolderFilterActive(data, folder.ID)) } aria-pressed={ fmt.Sprintf("%t", favoriteFolderFilterActive(data, folder.ID)) } data-sv-action="favorite.filterFolder" data-folder-id={ fmt.Sprintf("%d", folder.ID) }>{ folderDisplayName(folder) }</button>
76				}
77			}
78		</div>
79		<div class="sv-button-row">
80			<span class="sv-meta">Tags</span>
81			for _, tag := range data.Tags {
82				if tag != nil {
83					<button type="button" class={ favoriteFilterClass(favoriteTagFilterActive(data, tag.ID)) } aria-pressed={ fmt.Sprintf("%t", favoriteTagFilterActive(data, tag.ID)) } data-sv-action="favorite.filterTag" data-tag-id={ fmt.Sprintf("%d", tag.ID) }>
84						<span class="sv-tag-dot" style={ "background:" + safeTagColor(tag.Color) }></span>{ tag.Name }
85					</button>
86				}
87			}
88		</div>
89	</div>
90	<p class="sv-meta sv-key-hints">Keys: <kbd>j</kbd>/<kbd>k</kbd> move, <kbd>Enter</kbd> open, <kbd>e</kbd> edit title, <kbd>d</kbd> delete focused favorite.</p>
91}
92
93templ FavoriteCreateTagPicker(tags []*entity.Tag) {
94	if len(tags) > 0 {
95		<fieldset class="sv-tag-picker">
96			<legend>Tags</legend>
97			<div class="sv-tag-controls">
98				for _, tag := range tags {
99					if tag != nil {
100						<label class="sv-tag-toggle">
101							<input type="checkbox" name="tags" value={ fmt.Sprintf("%d", tag.ID) }/>
102							<span><span class="sv-tag-dot" style={ "background:" + safeTagColor(tag.Color) }></span>{ tag.Name }</span>
103						</label>
104					}
105				}
106			</div>
107		</fieldset>
108	}
109}
110
111templ FavoriteItem(favorite *entity.Favorite, folders []*entity.Folder, tags []*entity.Tag) {
112	<li class="sv-list-row">
113		<article class="sv-favorite-item" data-sv-favorite-row data-favorite-id={ fmt.Sprintf("%d", favorite.ID) }>
114			<div class="sv-favorite-header">
115				<div>
116					<a class="sv-link sv-favorite-open" href={ templ.SafeURL(sanitizeHref(favorite.URL)) }>{ favoriteItemLabel(favorite) }</a>
117					<p class="sv-history-url">{ favorite.URL }</p>
118					if favoriteMetaText(favorite, folders) != "" {
119						<p class="sv-meta">{ favoriteMetaText(favorite, folders) }</p>
120					}
121				</div>
122				<div class="sv-history-row-actions">
123					<a class="sv-button sv-button-secondary" href={ templ.SafeURL(sanitizeHref(favorite.URL)) }>@ButtonIconLabel(iconExternal, "Open")</a>
124					<button type="button" class="sv-button sv-button-secondary sv-button-danger sv-button-icon-only" data-sv-action="favorite.delete" data-id={ fmt.Sprintf("%d", favorite.ID) } data-sv-confirm="Delete this favorite?" aria-label="Delete this favorite">@IconOnly(iconTrash, "Delete this favorite")</button>
125				</div>
126			</div>
127			@FavoriteEditForm(favorite, folders)
128			@FavoriteTagControls(favorite, tags)
129		</article>
130	</li>
131}
132
133templ FavoriteEditForm(favorite *entity.Favorite, folders []*entity.Folder) {
134	<form class="sv-inline-form" data-sv-action="favorite.update">
135		<input type="hidden" name="id" value={ fmt.Sprintf("%d", favorite.ID) }/>
136		<label><span>Title</span><input name="title" type="text" value={ favorite.Title }/></label>
137		<label><span>Favicon</span><input name="favicon_url" type="url" value={ favorite.FaviconURL }/></label>
138		@FolderSelect("folder_id", folders, favorite.FolderID, "Folder")
139		@ShortcutSelect(favorite.ShortcutKey)
140		<button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconSave, "Save")</button>
141	</form>
142}
143
144templ FavoriteTagControls(favorite *entity.Favorite, tags []*entity.Tag) {
145	if len(tags) > 0 {
146		<div class="sv-tag-controls">
147			<span class="sv-meta">Tags</span>
148			for _, tag := range tags {
149				if tag != nil {
150					<button type="button" class={ favoriteTagButtonClass(favorite, tag) } data-sv-action={ favoriteTagButtonAction(favorite, tag) } data-favorite-id={ fmt.Sprintf("%d", favorite.ID) } data-tag-id={ fmt.Sprintf("%d", tag.ID) }>
151						<span class="sv-tag-dot" style={ "background:" + safeTagColor(tag.Color) }></span>{ favoriteTagButtonLabel(favorite, tag) }
152					</button>
153				}
154			}
155		</div>
156	}
157}
158
159templ FolderCreateForm() {
160	<form class="sv-inline-form" data-sv-action="folder.create">
161		<label><span>Name</span><input name="name" type="text" required/></label>
162		<label><span>Icon</span><input name="icon" type="text" placeholder="Optional"/></label>
163		<button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconFolder, "Create folder")</button>
164	</form>
165}
166
167templ FolderItem(folder *entity.Folder) {
168	<li class="sv-list-row">
169		<form class="sv-inline-form" data-sv-action="folder.update">
170			<input type="hidden" name="id" value={ fmt.Sprintf("%d", folder.ID) }/>
171			<label><span>Name</span><input name="name" type="text" value={ folder.Name } required/></label>
172			<label><span>Icon</span><input name="icon" type="text" value={ folder.Icon }/></label>
173			<button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconSave, "Save")</button>
174			<button type="button" class="sv-button sv-button-secondary sv-button-danger sv-button-icon-only" data-sv-action="folder.delete" data-id={ fmt.Sprintf("%d", folder.ID) } data-sv-confirm="Delete this folder? Favorites will move out of it." aria-label="Delete this folder">@IconOnly(iconTrash, "Delete this folder")</button>
175		</form>
176	</li>
177}
178
179templ TagCreateForm() {
180	<form class="sv-inline-form" data-sv-action="tag.create">
181		<label><span>Name</span><input name="name" type="text" required/></label>
182		<label><span>Color</span><input name="color" type="text" placeholder="#808080"/></label>
183		<button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconTag, "Create tag")</button>
184	</form>
185}
186
187templ TagItem(tag *entity.Tag) {
188	<li class="sv-list-row">
189		<form class="sv-inline-form" data-sv-action="tag.update">
190			<input type="hidden" name="id" value={ fmt.Sprintf("%d", tag.ID) }/>
191			<span class="sv-tag-dot" style={ "background:" + safeTagColor(tag.Color) }></span>
192			<label><span>Name</span><input name="name" type="text" value={ tag.Name } required/></label>
193			<label><span>Color</span><input name="color" type="text" value={ tag.Color }/></label>
194			<button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconSave, "Save")</button>
195			<button type="button" class="sv-button sv-button-secondary sv-button-danger sv-button-icon-only" data-sv-action="tag.delete" data-id={ fmt.Sprintf("%d", tag.ID) } data-sv-confirm="Delete this tag?" aria-label="Delete this tag">@IconOnly(iconTrash, "Delete this tag")</button>
196		</form>
197	</li>
198}
199
200templ FolderSelect(name string, folders []*entity.Folder, selected *entity.FolderID, label string) {
201	<label><span>{ label }</span><select name={ name }>
202		<option value="">Unfiled</option>
203		for _, folder := range folders {
204			if folder != nil {
205				<option value={ fmt.Sprintf("%d", folder.ID) } selected?={ folderSelected(selected, folder.ID) }>{ folderDisplayName(folder) }</option>
206			}
207		}
208	</select></label>
209}
210
211templ ShortcutSelect(selected *int) {
212	<label><span>Shortcut</span><select name="shortcut_key">
213		<option value="">None</option>
214		for i := 1; i <= 9; i++ {
215			<option value={ fmt.Sprintf("%d", i) } selected?={ shortcutSelected(selected, i) }>{ fmt.Sprintf("%d", i) }</option>
216		}
217	</select></label>
218}

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