Use tree for list
This commit is contained in:
72
main.go
72
main.go
@@ -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{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
132
static/list.html
132
static/list.html
@@ -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>
|
||||||
Reference in New Issue
Block a user