Use tree for list

This commit is contained in:
Ian Gulliver
2024-12-05 17:26:00 -08:00
parent 3ca9db02ee
commit 638a80c6e4
3 changed files with 123 additions and 87 deletions

72
main.go
View File

@@ -39,26 +39,43 @@ type suggestResponse struct {
Domain string `json:"domain"` Domain string `json:"domain"`
} }
type link struct { type linkBase struct {
Short string `json:"short"` Short string `json:"short"`
Long string `json:"long"` Long string `json:"long"`
Domain string `json:"domain"` Domain string `json:"domain"`
URL string `json:"url"`
Generated bool `json:"generated"` Generated bool `json:"generated"`
URL string `json:"url"`
}
type link struct {
linkBase
History []linkHistory `json:"history"`
}
type linkHistory struct {
linkBase
Until time.Time `json:"until"`
} }
func NewShortLinks(db *sql.DB, domainAliases map[string]string, writableDomains map[string]bool) (*ShortLinks, error) { func NewShortLinks(db *sql.DB, domainAliases map[string]string, writableDomains map[string]bool) (*ShortLinks, error) {
tmpl, err := template.New("index.html").ParseFiles("static/index.html") funcMap := template.FuncMap{
"lower": strings.ToLower,
"join": strings.Join,
}
tmpl, err := template.New("index.html").Funcs(funcMap).ParseFiles("static/index.html")
if err != nil { if err != nil {
return nil, fmt.Errorf("static/index.html: %w", err) return nil, fmt.Errorf("static/index.html: %w", err)
} }
help, err := template.New("help.html").ParseFiles("static/help.html") help, err := template.New("help.html").Funcs(funcMap).ParseFiles("static/help.html")
if err != nil { if err != nil {
return nil, fmt.Errorf("static/help.html: %w", err) return nil, fmt.Errorf("static/help.html: %w", err)
} }
list, err := template.New("list.html").ParseFiles("static/list.html") list, err := template.New("list.html").Funcs(funcMap).ParseFiles("static/list.html")
if err != nil { if err != nil {
return nil, fmt.Errorf("static/list.html: %w", err) return nil, fmt.Errorf("static/list.html: %w", err)
} }
@@ -329,7 +346,34 @@ func (sl *ShortLinks) serveList(w http.ResponseWriter, r *http.Request) {
return return
} }
rows, err := sl.db.Query("SELECT short, long, domain, generated FROM links WHERE domain = $1 ORDER BY short ASC", sl.getDomain(r.Host)) rows, err := sl.db.Query(`
SELECT
short,
long,
domain,
generated,
CURRENT_TIMESTAMP as until,
0 as is_history
FROM links
WHERE domain = $1
UNION ALL
SELECT
short,
long,
domain,
generated,
until,
1 as is_history
FROM links_history
WHERE domain = $1
ORDER BY
short ASC,
is_history,
until DESC
`, sl.getDomain(r.Host))
if err != nil { if err != nil {
sendError(w, http.StatusInternalServerError, "select links: %s", err) sendError(w, http.StatusInternalServerError, "select links: %s", err)
return return
@@ -338,17 +382,25 @@ func (sl *ShortLinks) serveList(w http.ResponseWriter, r *http.Request) {
defer rows.Close() defer rows.Close()
links := []link{} links := []link{}
for rows.Next() { for rows.Next() {
link := link{} link := link{}
err := rows.Scan(&link.Short, &link.Long, &link.Domain, &link.Generated) hist := linkHistory{}
isHistory := false
err := rows.Scan(&link.Short, &link.Long, &link.Domain, &link.Generated, &hist.Until, &isHistory)
if err != nil { if err != nil {
sendError(w, http.StatusInternalServerError, "scan link: %s", err) sendError(w, http.StatusInternalServerError, "scan link: %s", err)
return return
} }
link.URL = fmt.Sprintf("https://%s/%s", link.Domain, link.Short) if !isHistory {
link.URL = fmt.Sprintf("https://%s/%s", link.Domain, link.Short)
links = append(links, link) links = append(links, link)
} else {
hist.linkBase = link.linkBase
links[len(links)-1].History = append(links[len(links)-1].History, hist)
}
} }
err = sl.list.Execute(w, map[string]any{ err = sl.list.Execute(w, map[string]any{

View File

@@ -303,8 +303,12 @@ document.addEventListener('DOMContentLoaded', async () => {
await setFromInputs(); await setFromInputs();
}); });
if (document.getElementById('long').value == '') {
document.getElementById('long').focus();
} else {
document.getElementById('short').focus();
}
document.getElementById('long').focus();
setInputIcons(); setInputIcons();
}); });
</script> </script>

View File

@@ -13,43 +13,18 @@ body {
align-items: center; align-items: center;
} }
table {
border-spacing: 0;
width: 100%;
table-layout: fixed;
}
.bg0 {
background-color: var(--sl-color-neutral-50);
}
.bg1 {
background-color: var(--sl-color-neutral-100);
}
td div {
display: flex;
flex-direction: row;
align-items: center;
padding: 5px 5px 5px 0;
overflow: clip;
text-wrap: nowrap;
}
td:first-child {
padding-right: 10px;
}
tr:hover {
background-color: var(--sl-color-neutral-200);
}
a { a {
text-decoration: none; text-decoration: none;
color: var(--sl-color-primary-700); color: var(--sl-color-primary-700);
} }
.long, .short { sl-tree-item {
display: flex;
flex-direction: row;
align-items: center;
}
.short, .long {
margin-left: 7px; margin-left: 7px;
} }
</style> </style>
@@ -69,31 +44,29 @@ a {
/> />
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.18.0/cdn/shoelace-autoloader.js"></script> <script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.18.0/cdn/shoelace-autoloader.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('search').addEventListener('sl-input', () => { await Promise.allSettled([
const search = document.getElementById('search').value; customElements.whenDefined('sl-tree'),
const rows = document.querySelectorAll('tr'); customElements.whenDefined('sl-tree-item'),
customElements.whenDefined('sl-copy-button'),
customElements.whenDefined('sl-tooltip'),
customElements.whenDefined('sl-icon')
]);
let i = 0; document.getElementById('search').addEventListener('sl-input', () => {
const search = document.getElementById('search').value.toLowerCase();
const rows = document.getElementById('links').children;
for (const row of rows) { for (const row of rows) {
const short = row.children[0].textContent; const query = row.getAttribute('data-query');
const long = row.children[1].textContent;
console.log(search, short, long); if (search == undefined || search == '' || query.includes(search)) {
if (search == undefined || search == '' || short.includes(search) || long.includes(search)) {
row.style.visibility = 'visible'; row.style.visibility = 'visible';
row.classList.remove(`bg${(i + 1) % 2}`);
row.classList.add(`bg${i % 2}`);
i++;
} else { } else {
row.style.visibility = 'collapse'; row.style.visibility = 'collapse';
} }
} }
}); });
document.getElementById('search').dispatchEvent(new Event('sl-input'));
}); });
</script> </script>
</head> </head>
@@ -103,36 +76,43 @@ document.addEventListener('DOMContentLoaded', () => {
<br /> <br />
<br /> <br />
<table> <sl-tree id="links">
<tbody> {{ range .links }}
{{ range .links }} <sl-tree-item data-query="{{ .Short | lower }}|{{ .Long | lower }}">
<tr>
<td> <sl-copy-button value="{{ .URL }}" style="color: var(--sl-color-neutral-400);"></sl-copy-button>
<div> <sl-tooltip content="Edit">
<sl-copy-button value="{{ .URL }}" style="color: var(--sl-color-neutral-400);"></sl-copy-button> <a href="./?short={{ .Short }}">
<sl-tooltip content="Edit"> <sl-icon name="pencil" label="Edit" style="color: var(--sl-color-neutral-400);"></sl-icon>
<a href="./?short={{ .Short }}"> </a>
<sl-icon name="pencil" label="Edit" style="color: var(--sl-color-neutral-400);"></sl-icon> </sl-tooltip>
</a> <a href="{{ .URL }}" class="short">{{ .Short }}</a>
</sl-tooltip>
<a href="{{ .URL }}" class="short">{{ .Short }}</a> <sl-tree-item>
</div> <sl-copy-button value="{{ .Long }}" style="color: var(--sl-color-neutral-400);"></sl-copy-button>
</td> <sl-tooltip content="Add">
<td> <a href="./?long={{ .Long }}">
<div> <sl-icon name="plus-circle" label="Add" style="color: var(--sl-color-neutral-400);"></sl-icon>
<sl-copy-button value="{{ .Long }}" style="color: var(--sl-color-neutral-400);"></sl-copy-button> </a>
<sl-tooltip content="Add"> </sl-tooltip>
<a href="./?long={{ .Long }}"> <a href="{{ .Long }}" class="long">{{ .Long }}</a>
<sl-icon name="plus-circle" label="Add" style="color: var(--sl-color-neutral-400);"></sl-icon> </sl-tree-item>
</a>
</sl-tooltip> {{ range .History }}
<a href="{{ .Long }}" class="long">{{ .Long }}</a> <sl-tree-item>
</div> <sl-copy-button value="{{ .Long }}" style="color: var(--sl-color-neutral-400);"></sl-copy-button>
</td> <sl-tooltip content="Restore">
</tr> <a href="./?short={{ .Short }}&long={{ .Long }}">
<sl-icon name="arrow-counterclockwise" label="Restore" style="color: var(--sl-color-neutral-400);"></sl-icon>
</a>
</sl-tooltip>
<a href="{{ .Long }}" class="long">{{ .Long }}</a>
</sl-tree-item>
{{ end }} {{ end }}
</tbody>
</table> </sl-tree-item>
{{ end }}
</sl-tree>
</div> </div>
</body> </body>
</html> </html>