mirror of
https://github.com/skidoodle/iphistory.git
synced 2026-04-28 07:47:35 +02:00
181 lines
4.8 KiB
Plaintext
181 lines
4.8 KiB
Plaintext
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"time"
|
||
)
|
||
|
||
func pathBuilder(page int, query string) string {
|
||
base := "/"
|
||
if page > 1 {
|
||
base = fmt.Sprintf("/p/%d", page)
|
||
}
|
||
if query != "" {
|
||
return fmt.Sprintf("%s?q=%s", base, query)
|
||
}
|
||
return base
|
||
}
|
||
|
||
func humanize(t time.Time) string {
|
||
duration := time.Since(t)
|
||
switch {
|
||
case duration < time.Minute:
|
||
return "just now"
|
||
case duration < time.Hour:
|
||
return fmt.Sprintf("%dm ago", int(duration.Minutes()))
|
||
case duration < time.Hour*24:
|
||
return fmt.Sprintf("%dh ago", int(duration.Hours()))
|
||
default:
|
||
return t.Format("Jan 02")
|
||
}
|
||
}
|
||
|
||
templ navLink(text string, page int, query string, active bool) {
|
||
if active {
|
||
<a
|
||
href={ templ.SafeURL(pathBuilder(page, query)) }
|
||
hx-get={ pathBuilder(page, query) }
|
||
hx-target="#main-content"
|
||
hx-push-url="true"
|
||
class="nav-link"
|
||
>{ text }</a>
|
||
} else {
|
||
<a class="nav-link disabled">{ text }</a>
|
||
}
|
||
}
|
||
|
||
templ MainContent(records []Record, query string, page int, hasMore bool) {
|
||
<title>
|
||
if query != "" {
|
||
IP History - { query }
|
||
} else if page > 1 {
|
||
IP History - Page { fmt.Sprint(page) }
|
||
} else {
|
||
IP History
|
||
}
|
||
</title>
|
||
<div id="main-content">
|
||
if query != "" {
|
||
<div class="search-meta">
|
||
<span>Showing results for "<strong>{ query }</strong>"</span>
|
||
<a
|
||
href="/"
|
||
hx-get="/"
|
||
hx-target="#main-content"
|
||
hx-push-url="true"
|
||
hx-on:click="document.querySelector('.search-input').value = ''"
|
||
>Clear Search</a>
|
||
</div>
|
||
}
|
||
if page == 1 && query == "" && len(records) > 0 {
|
||
<section class="current-ip-card">
|
||
<div>Current Public IP</div>
|
||
<div class="ip-display" onclick="copyToClipboard(this.innerText, this)">{ records[0].IP }</div>
|
||
<div>{ records[0].Timestamp.Format("Jan 02, 15:04:05 MST") } ({ humanize(records[0].Timestamp) })</div>
|
||
</section>
|
||
}
|
||
if len(records) > 0 {
|
||
<table class="history-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Timestamp</th>
|
||
<th scope="col">IP Address</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
for _, r := range records {
|
||
<tr>
|
||
<td>
|
||
{ r.Timestamp.Format("2006-01-02 15:04:05") }
|
||
<span class="time-relative">{ humanize(r.Timestamp) }</span>
|
||
</td>
|
||
<td class="copyable" onclick="copyToClipboard(this.innerText, this)">{ r.IP }</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
} else {
|
||
<div class="empty-state">
|
||
<p>No IP found matching your search.</p>
|
||
if query != "" {
|
||
<button
|
||
type="button"
|
||
class="btn-ghost"
|
||
hx-get="/"
|
||
hx-target="#main-content"
|
||
hx-push-url="true"
|
||
hx-on:click="document.querySelector('.search-input').value = ''"
|
||
>Return to Home</button>
|
||
}
|
||
</div>
|
||
}
|
||
<div class="pagination">
|
||
@navLink("← Previous", page-1, query, page > 1)
|
||
<span>Page { fmt.Sprint(page) }</span>
|
||
@navLink("Next →", page+1, query, hasMore)
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
templ Page(records []Record, query string, page int, hasMore bool) {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>IP History</title>
|
||
<link rel="icon" href="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
|
||
<script src="/assets/htmx.min.js" defer></script>
|
||
<link rel="stylesheet" type="text/css" href="/assets/style.css"/>
|
||
<script>
|
||
function copyToClipboard(text, el) {
|
||
navigator.clipboard.writeText(text);
|
||
el.classList.add("copied");
|
||
setTimeout(() => {
|
||
el.classList.remove("copied");
|
||
}, 1000);
|
||
}
|
||
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
|
||
e.preventDefault();
|
||
document.querySelector('.search-input').focus();
|
||
}
|
||
});
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<main class="container">
|
||
<header>
|
||
<a href="/" class="title-link"><h1>IP History</h1></a>
|
||
<p>A simple timeline of your public IP changes</p>
|
||
</header>
|
||
<form class="search-form" hx-get="/" hx-target="#main-content" hx-push-url="true" hx-sync="closest form:abort">
|
||
<div class="search-wrapper">
|
||
<input
|
||
type="text"
|
||
name="q"
|
||
class="search-input"
|
||
placeholder="Search IPs... (Press '/' to focus)"
|
||
value={ query }
|
||
autocomplete="off"
|
||
hx-get="/"
|
||
hx-trigger="keyup changed delay:300ms, search"
|
||
hx-target="#main-content"
|
||
/>
|
||
<button
|
||
type="button"
|
||
class="clear-input"
|
||
hx-get="/"
|
||
hx-target="#main-content"
|
||
hx-push-url="true"
|
||
onclick="this.previousElementSibling.value = ''; this.previousElementSibling.focus();"
|
||
>×</button>
|
||
</div>
|
||
</form>
|
||
@MainContent(records, query, page, hasMore)
|
||
</main>
|
||
</body>
|
||
</html>
|
||
}
|