_list
This commit is contained in:
76
main.go
76
main.go
@@ -18,6 +18,7 @@ import (
|
|||||||
type ShortLinks struct {
|
type ShortLinks struct {
|
||||||
tmpl *template.Template
|
tmpl *template.Template
|
||||||
help *template.Template
|
help *template.Template
|
||||||
|
list *template.Template
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
r *rand.Rand
|
r *rand.Rand
|
||||||
@@ -38,10 +39,16 @@ type suggestResponse struct {
|
|||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewShortLinks(db *sql.DB, domainAliases map[string]string, writableDomains map[string]bool) (*ShortLinks, error) {
|
type link struct {
|
||||||
tmpl := template.New("index.html")
|
Short string `json:"short"`
|
||||||
|
Long string `json:"long"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Generated bool `json:"generated"`
|
||||||
|
}
|
||||||
|
|
||||||
tmpl, err := tmpl.ParseFiles("static/index.html")
|
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")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("static/index.html: %w", err)
|
return nil, fmt.Errorf("static/index.html: %w", err)
|
||||||
}
|
}
|
||||||
@@ -51,6 +58,11 @@ func NewShortLinks(db *sql.DB, domainAliases map[string]string, writableDomains
|
|||||||
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")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("static/list.html: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
oai, err := newOAIClientFromEnv()
|
oai, err := newOAIClientFromEnv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("newOAIClientFromEnv: %w", err)
|
return nil, fmt.Errorf("newOAIClientFromEnv: %w", err)
|
||||||
@@ -59,6 +71,7 @@ func NewShortLinks(db *sql.DB, domainAliases map[string]string, writableDomains
|
|||||||
sl := &ShortLinks{
|
sl := &ShortLinks{
|
||||||
tmpl: tmpl,
|
tmpl: tmpl,
|
||||||
help: help,
|
help: help,
|
||||||
|
list: list,
|
||||||
mux: http.NewServeMux(),
|
mux: http.NewServeMux(),
|
||||||
db: db,
|
db: db,
|
||||||
r: rand.New(rand.NewSource(uint64(time.Now().UnixNano()))),
|
r: rand.New(rand.NewSource(uint64(time.Now().UnixNano()))),
|
||||||
@@ -69,8 +82,9 @@ func NewShortLinks(db *sql.DB, domainAliases map[string]string, writableDomains
|
|||||||
}
|
}
|
||||||
|
|
||||||
sl.mux.HandleFunc("GET /{$}", sl.serveRoot)
|
sl.mux.HandleFunc("GET /{$}", sl.serveRoot)
|
||||||
sl.mux.HandleFunc("GET /_help", sl.serveHelp)
|
|
||||||
sl.mux.HandleFunc("GET /_favicon.png", sl.serveFavicon)
|
sl.mux.HandleFunc("GET /_favicon.png", sl.serveFavicon)
|
||||||
|
sl.mux.HandleFunc("GET /_help", sl.serveHelp)
|
||||||
|
sl.mux.HandleFunc("GET /_list", sl.serveList)
|
||||||
sl.mux.HandleFunc("GET /{short}", sl.serveShort)
|
sl.mux.HandleFunc("GET /{short}", sl.serveShort)
|
||||||
sl.mux.HandleFunc("POST /{$}", sl.serveSet)
|
sl.mux.HandleFunc("POST /{$}", sl.serveSet)
|
||||||
sl.mux.HandleFunc("QUERY /{$}", sl.serveSuggest)
|
sl.mux.HandleFunc("QUERY /{$}", sl.serveSuggest)
|
||||||
@@ -91,26 +105,26 @@ func (sl *ShortLinks) serveRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sl.isWritable(r.Host) {
|
if sl.isWritable(r.Host) {
|
||||||
sl.serveRootWithPath(w, r, "")
|
sl.serveRootWithShort(w, r, r.Form.Get("short"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.SplitN(r.Host, ".", 2)
|
parts := strings.SplitN(r.Host, ".", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
sl.serveRootWithPath(w, r, "")
|
sl.serveRootWithShort(w, r, r.Form.Get("short"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
long, err := sl.getLong(parts[0], sl.getDomain(parts[1]))
|
long, err := sl.getLong(parts[0], sl.getDomain(parts[1]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sl.serveRootWithPath(w, r, "")
|
sl.serveRootWithShort(w, r, r.Form.Get("short"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, long, http.StatusTemporaryRedirect)
|
http.Redirect(w, r, long, http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sl *ShortLinks) serveRootWithPath(w http.ResponseWriter, r *http.Request, path string) {
|
func (sl *ShortLinks) serveRootWithShort(w http.ResponseWriter, r *http.Request, short string) {
|
||||||
err := sl.initRequest(w, r)
|
err := sl.initRequest(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendError(w, http.StatusBadRequest, "init request: %s", err)
|
sendError(w, http.StatusBadRequest, "init request: %s", err)
|
||||||
@@ -123,9 +137,9 @@ func (sl *ShortLinks) serveRootWithPath(w http.ResponseWriter, r *http.Request,
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = sl.tmpl.Execute(w, map[string]any{
|
err = sl.tmpl.Execute(w, map[string]any{
|
||||||
"path": path,
|
"short": short,
|
||||||
"host": sl.getDomain(r.Host),
|
"host": sl.getDomain(r.Host),
|
||||||
"long": r.Form.Get("long"),
|
"long": r.Form.Get("long"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendError(w, http.StatusInternalServerError, "error executing template: %s", err)
|
sendError(w, http.StatusInternalServerError, "error executing template: %s", err)
|
||||||
@@ -144,7 +158,7 @@ func (sl *ShortLinks) serveShort(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
long, err := sl.getLong(short, sl.getDomain(r.Host))
|
long, err := sl.getLong(short, sl.getDomain(r.Host))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sl.serveRootWithPath(w, r, short)
|
sl.serveRootWithShort(w, r, short)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +317,44 @@ func (sl *ShortLinks) serveFavicon(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.ServeFile(w, r, "static/favicon.png")
|
http.ServeFile(w, r, "static/favicon.png")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sl *ShortLinks) serveList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := sl.initRequest(w, r)
|
||||||
|
if err != nil {
|
||||||
|
sendError(w, http.StatusBadRequest, "init request: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := sl.db.Query("SELECT short, long, domain, generated FROM links WHERE domain = $1 ORDER BY short ASC", sl.getDomain(r.Host))
|
||||||
|
if err != nil {
|
||||||
|
sendError(w, http.StatusInternalServerError, "select links: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
links := []link{}
|
||||||
|
for rows.Next() {
|
||||||
|
link := link{}
|
||||||
|
err := rows.Scan(&link.Short, &link.Long, &link.Domain, &link.Generated)
|
||||||
|
if err != nil {
|
||||||
|
sendError(w, http.StatusInternalServerError, "scan link: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
link.URL = fmt.Sprintf("https://%s/%s", link.Domain, link.Short)
|
||||||
|
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sl.list.Execute(w, map[string]any{
|
||||||
|
"links": links,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
sendError(w, http.StatusInternalServerError, "error executing template: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (sl *ShortLinks) getDomain(host string) string {
|
func (sl *ShortLinks) getDomain(host string) string {
|
||||||
if alias, ok := sl.domainAliases[host]; ok {
|
if alias, ok := sl.domainAliases[host]; ok {
|
||||||
return alias
|
return alias
|
||||||
|
|||||||
@@ -310,8 +310,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container" style="width: min(500px, calc(100vw - 10px))">
|
<div id="container" style="width: min(500px, calc(100vw - 10px))">
|
||||||
<sl-input id="short" value="{{ .path }}" label="{{ .host }}/">
|
<sl-input id="short" value="{{ .short }}" label="{{ .host }}/">
|
||||||
<sl-icon id="short-icon" name="type" slot="suffix"></sl-icon>
|
<sl-icon id="short-icon" name="type" slot="suffix"></sl-icon>
|
||||||
</sl-input>
|
</sl-input>
|
||||||
|
|
||||||
|
|||||||
127
static/list.html
Normal file
127
static/list.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
:not(:defined) {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font: 12px var(--sl-font-mono);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:first-child {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: var(--sl-color-neutral-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--sl-color-primary-700);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/png" href="_favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="_favicon.png" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
media="(prefers-color-scheme:light)"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.18.0/cdn/themes/light.css"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
media="(prefers-color-scheme:dark)"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.18.0/cdn/themes/dark.css"
|
||||||
|
onload="document.documentElement.classList.add('sl-theme-dark');"
|
||||||
|
/>
|
||||||
|
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.18.0/cdn/shoelace-autoloader.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.getElementById('search').addEventListener('sl-input', () => {
|
||||||
|
const search = document.getElementById('search').value;
|
||||||
|
const rows = document.querySelectorAll('tr');
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const short = row.children[0].textContent;
|
||||||
|
const long = row.children[1].textContent;
|
||||||
|
|
||||||
|
console.log(search, short, long);
|
||||||
|
|
||||||
|
if (search == undefined || search == '' || short.includes(search) || long.includes(search)) {
|
||||||
|
row.style.visibility = 'visible';
|
||||||
|
row.classList.remove(`bg${(i + 1) % 2}`);
|
||||||
|
row.classList.add(`bg${i % 2}`);
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
row.style.visibility = 'collapse';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('search').dispatchEvent(new Event('sl-input'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container" style="width: min(500px, calc(100vw - 10px))">
|
||||||
|
<sl-input id="search"></sl-input>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{{ range .links }}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<a href="{{ .URL }}">{{ .Short }}</a>
|
||||||
|
<sl-copy-button value="{{ .URL }}" style="color: var(--sl-color-neutral-400);"></sl-copy-button>
|
||||||
|
<a href="./?short={{ .Short }}">
|
||||||
|
<sl-icon name="pencil" style="color: var(--sl-color-neutral-400);"></sl-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<a href="{{ .Long }}">{{ .Long }}</a>
|
||||||
|
<sl-copy-button value="{{ .Long }}" style="color: var(--sl-color-neutral-400);"></sl-copy-button>
|
||||||
|
<a href="./?long={{ .Long }}">
|
||||||
|
<sl-icon name="plus-circle" style="color: var(--sl-color-neutral-400);"></sl-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user