ported all templates to templ syntax.

This commit is contained in:
fiatjaf
2024-01-07 15:48:55 -03:00
parent b44771b0da
commit 7f0bb418b6
53 changed files with 2055 additions and 2478 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ node_modules/
static/tailwind-bundle.min.css
yarn.lock
package-lock.json
*_templ.go

43
archive.templ Normal file
View File

@@ -0,0 +1,43 @@
package main
templ archiveTemplate(params ArchivePageParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<title>{ params.Title }</title>
@headCommonTemplate(params.HeadParams)
</head>
<body class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black">
@topTemplate()
<div class="mx-auto block px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div class="flex w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 print:w-full sm:w-11/12 md:w-10/12 lg:w-9/12 lg:gap-48vw">
<div class="relative top-auto flex basis-1/4 items-center self-start sm:sticky sm:top-8 sm:mt-8 sm:items-start">
<div class="text-2xl">{ params.Title }</div>
</div>
<div class="w-full break-words break-all print:w-full sm:w-1/2">
<div class="mb-6 leading-5">
<h1 class="text-xl">{ params.Title }</h1>
</div>
<div class="mb-6 leading-5">
for _, v:= range params.Data {
<a class="block" href="/{params.PathPrefix}{v}">
{ v }
</a>
}
</div>
<div class="flex justify-between">
if params.PrevPage != 0 {
<a href="/{params.PaginationUrl}/{params.PrevPage}">&lt;&lt; Prev page</a>
}
if params.NextPage != 0 {
<a href="/{params.PaginationUrl}/{params.NextPage}">Next page &gt;&gt;</a>
}
</div>
</div>
</div>
</div>
@footerTemplate()
</body>
</html>
}

147
clients.templ Normal file
View File

@@ -0,0 +1,147 @@
package main
templ clientsTemplate(clients []ClientReference) {
<aside class="fixed bottom-0 left-0 top-auto mt-4 w-full basis-3/12 self-start transition-all duration-500 print:hidden sm:sticky sm:bottom-auto sm:left-auto sm:top-8 sm:w-auto">
<div class="absolute right-0 top-0 z-10 mb-4 h-10 w-10 text-center text-sm sm:relative sm:h-auto sm:w-auto">
<span class="hidden sm:block">Open in</span>
<div
_="on click
toggle .hidden on #open
toggle .hidden on #close
toggle .hidden on #gradient
toggle .overflow-hidden on <body />
toggle .hidden on <.client:not(:first-child) />
toggle .top_client_sticky on <.clients_wrapper > :first-child />
"
>
<div id="open" class="inline sm:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
viewBox="0 0 20 20"
class="m-auto mt-[28%] block h-1/2 w-1/2"
>
<path
fill="#fafafa"
fill-rule="evenodd"
d="M3.808.355h2.85a2.85 2.85 0 0 1 2.85 2.85v2.85a2.85 2.85 0 0 1-2.85 2.85h-2.85a2.85 2.85 0 0 1-2.85-2.85v-2.85a2.85 2.85 0 0 1 2.85-2.85Zm2.85 6.65a.95.95 0 0 0 .95-.95v-2.85a.95.95 0 0 0-.95-.95h-2.85a.95.95 0 0 0-.95.95v2.85c0 .525.425.95.95.95h2.85Zm0 3.8h-2.85a2.85 2.85 0 0 0-2.85 2.85v2.85a2.85 2.85 0 0 0 2.85 2.85h2.85a2.85 2.85 0 0 0 2.85-2.85v-2.85a2.85 2.85 0 0 0-2.85-2.85Zm0 6.65a.95.95 0 0 0 .95-.95v-2.85a.95.95 0 0 0-.95-.95h-2.85a.95.95 0 0 0-.95.95v2.85c0 .525.425.95.95.95h2.85Zm10.45-6.65h-2.85a2.85 2.85 0 0 0-2.85 2.85v2.85a2.85 2.85 0 0 0 2.85 2.85h2.85a2.85 2.85 0 0 0 2.85-2.85v-2.85a2.85 2.85 0 0 0-2.85-2.85Zm0 6.65a.95.95 0 0 0 .95-.95v-2.85a.95.95 0 0 0-.95-.95h-2.85a.95.95 0 0 0-.95.95v2.85c0 .525.425.95.95.95h2.85Zm0-17.1h-2.85a2.85 2.85 0 0 0-2.85 2.85v2.85a2.85 2.85 0 0 0 2.85 2.85h2.85a2.85 2.85 0 0 0 2.85-2.85v-2.85a2.85 2.85 0 0 0-2.85-2.85Zm0 6.65a.95.95 0 0 0 .95-.95v-2.85a.95.95 0 0 0-.95-.95h-2.85a.95.95 0 0 0-.95.95v2.85c0 .525.425.95.95.95h2.85Z"
clip-rule="evenodd"
></path>
</svg>
</div>
<div id="close" class="mt-4 hidden sm:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
width="31"
height="16"
fill="currentColor"
viewBox="0 0 31 16"
class="m-auto mt-[28%] block h-1/2 w-1/2"
>
<path
fill="#fafafa"
d="M30.207 3.016 16.744 14.983a1.496 1.496 0 0 1-1.974 0L1.307 3.016A1.496 1.496 0 0 1 3.28.772l12.476 11.085L28.233.772a1.496 1.496 0 1 1 1.974 2.244Z"
></path>
</svg>
</div>
</div>
</div>
<div
class="clients_wrapper | overflow-y-auto rounded-t-lg max-h-[55vh] sm:max-h-max"
_="on load
wait 50ms
get my children
get filterAndSort(it) then repeat for c in it call me.appendChild(c) end
get first in me
then tell it
remove .hidden
add .top_client
"
>
for _, client := range clients {
<div
data-platform={ client.Platform }
class="client | hidden w-full items-center border-b border-zinc-800 bg-zinc-700 first-of-type:rounded-t-lg first-of-type:border-0 first-of-type:bg-strongpink hover:bg-zinc-800 sm:mb-3 sm:flex sm:rounded-lg sm:border-0 sm:first-of-type:rounded-lg"
_="on load get localStorage['nj:{.ID}'] or 0 then set @count to it then set @title to `used ${it} times`
on click increment localStorage['nj:{client.ID}']"
>
<a
class="client block basis-full px-3 py-3 text-left text-[17px] font-normal leading-4 text-white no-underline sm:inline sm:py-1.5 sm:text-center sm:font-light"
href="{client.URL}"
>
<span class="ml-1.5 pr-2 inline basis-1/5 text-neutral-400 sm:hidden">Open in</span>
{ client.Name }
<span
class="type | float-right mr-4 text-xs uppercase text-neutral-400 sm:hidden"
>{ client.Platform }</span>
</a>
</div>
}
<div data-platform="dummy" class="client | hidden h-8 bg-zinc-700"></div>
</div>
<div
id="gradient"
class="fixed bottom-0 hidden h-10 w-full bg-gradient-to-t from-zinc-800 to-transparent pointer-events-auto"
></div>
</aside>
<style>
.top_client_sticky {
position: absolute;
}
.top_client span {
color: #FFFFFF;
}
.top_client a span.type {
display: none;
}
.clients_wrapper div {
&:nth-child(2) {
margin-top: 2.5rem;
}
}
</style>
<script>
function getPlatform() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
if (/android/i.test(userAgent)) {
return 'android';
} else if (/iPad|iPhone|iPod/i.test(userAgent) && !window.MSStream) {
return 'ios';
} else {
return 'web';
}
}
function filterAndSort(children) {
const platform = getPlatform();
const filteredElements = Array.from(children).filter((element) => {
const platformAttr = element.getAttribute('data-platform');
return platform === platformAttr || platformAttr === 'web' || platformAttr === 'native' || platformAttr === 'dummy';
});
Array.from(children).forEach((element) => {
if (!filteredElements.includes(element)) {
element.remove();
}
});
const sortedElements = filteredElements.sort(
(a, b) =>
parseInt(b.getAttribute('count')) - parseInt(a.getAttribute('count'))
);
// Assuming you want to re-insert the sorted elements into their parent container
const parent = children[0].parentNode;
parent.innerHTML = ''; // Clear the parent container
sortedElements.forEach((element) => {
parent.appendChild(element);
});
return sortedElements;
}
</script>
}

56
common.templ Normal file
View File

@@ -0,0 +1,56 @@
package main
templ authorHeaderTemplate(metadata Metadata) {
<header class="mb-4 max-w-full">
<a class="flex flex-wrap items-center" href={ templ.URL("/" + metadata.Npub()) }>
<div class="print:basis-1-12 imgclip mr-2 max-w-full basis-1/6 overflow-hidden sm:mr-4">
<img class="block h-auto w-full" src={ metadata.Picture }/>
</div>
<div class="block print:text-base sm:grow">
<div class="leading-4 sm:text-2xl">
{ metadata.Name }
<!---->
if metadata.Name != metadata.DisplayName {
<span class="text-stone-400 sm:text-xl">/ { metadata.DisplayName } </span>
}
</div>
<div class="text-sm leading-4 text-stone-400 sm:text-base">
{ metadata.NpubShort() }
</div>
</div>
</a>
</header>
}
templ lastNotesTemplate(lastNotes []EnhancedEvent) {
<aside>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<nav class="mb-6 leading-5">
<h2 class="text-2xl text-strongpink">Last Notes</h2>
for _, ee := range lastNotes {
<a
class="my-8 block no-underline hover:-ml-6 hover:border-l-05rem hover:border-solid hover:border-l-gray-100 hover:pl-4 dark:hover:border-l-zinc-700"
href={ templ.URL("/" + ee.Nevent()) }
>
<div
class="-ml-2.5 mb-1.5 flex flex-row flex-wrap border-b-4 border-solid border-b-gray-100 pb-1 pl-2.5 dark:border-b-neutral-800"
>
<div class="text-sm text-strongpink">{ ee.CreatedAtStr() }</div>
if ee.IsReply() {
<div class="ml-2 text-sm text-gray-300 dark:text-gray-400">- reply</div>
}
</div>
<div
class="mt-0.5 max-h-40 basis-full overflow-hidden hover:text-strongpink"
_="on load if my scrollHeight > my offsetHeight add .gradient"
dir="auto"
>
@templ.Raw(ee.Preview())
</div>
</a>
}
</nav>
</aside>
}

158
data.go
View File

@@ -3,129 +3,20 @@ package main
import (
"context"
"fmt"
"html"
"html/template"
"regexp"
"strconv"
"strings"
"time"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip10"
"github.com/nbd-wtf/go-nostr/nip19"
sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/texttheater/golang-levenshtein/levenshtein"
)
type EnhancedEvent struct {
event *nostr.Event
relays []string
}
func (ee EnhancedEvent) IsReply() bool {
return nip10.GetImmediateReply(ee.event.Tags) != nil
}
func (ee EnhancedEvent) Reply() *nostr.Tag {
return nip10.GetImmediateReply(ee.event.Tags)
}
func (ee EnhancedEvent) Preview() template.HTML {
lines := strings.Split(html.EscapeString(ee.event.Content), "\n")
var processedLines []string
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
processedLine := shortenNostrURLs(line)
processedLines = append(processedLines, processedLine)
}
return template.HTML(strings.Join(processedLines, "<br/>"))
}
func (ee EnhancedEvent) RssTitle() string {
regex := regexp.MustCompile(`(?i)<br\s?/?>`)
replacedString := regex.ReplaceAllString(string(ee.Preview()), " ")
words := strings.Fields(replacedString)
title := ""
for i, word := range words {
if len(title)+len(word)+1 <= 65 { // +1 for space
if title != "" {
title += " "
}
title += word
} else {
if i > 1 { // the first word len is > 65
title = title + " ..."
} else {
title = ""
}
break
}
}
content := ee.RssContent()
distance := levenshtein.DistanceForStrings([]rune(title), []rune(content), levenshtein.DefaultOptions)
similarityThreshold := 5
if distance <= similarityThreshold {
return ""
} else {
return title
}
}
func (ee EnhancedEvent) RssContent() string {
content := ee.event.Content
content = basicFormatting(html.EscapeString(content), true, false, false)
content = renderQuotesAsHTML(context.Background(), content, false)
if ee.IsReply() {
nevent, _ := nip19.EncodeEvent(ee.Reply().Value(), ee.relays, ee.event.PubKey)
neventShort := nevent[:8] + "…" + nevent[len(nevent)-4:]
content = "In reply to <a href='/" + nevent + "'>" + neventShort + "</a><br/>_________________________<br/><br/>" + content
}
return content
}
func (ee EnhancedEvent) Thumb() string {
imgRegex := regexp.MustCompile(`(https?://[^\s]+\.(?:png|jpe?g|gif|bmp|svg)(?:/[^\s]*)?)`)
matches := imgRegex.FindAllStringSubmatch(ee.event.Content, -1)
if len(matches) > 0 {
// The first match group captures the image URL
return matches[0][1]
}
return ""
}
func (ee EnhancedEvent) Npub() string {
npub, _ := nip19.EncodePublicKey(ee.event.PubKey)
return npub
}
func (ee EnhancedEvent) NpubShort() string {
npub := ee.Npub()
return npub[:8] + "…" + npub[len(npub)-4:]
}
func (ee EnhancedEvent) Nevent() string {
nevent, _ := nip19.EncodeEvent(ee.event.ID, ee.relays, ee.event.PubKey)
return nevent
}
func (ee EnhancedEvent) CreatedAtStr() string {
return time.Unix(int64(ee.event.CreatedAt), 0).Format("2006-01-02 15:04:05")
}
func (ee EnhancedEvent) ModifiedAtStr() string {
return time.Unix(int64(ee.event.CreatedAt), 0).Format("2006-01-02T15:04:05Z07:00")
}
type Data struct {
templateId TemplateID
event *nostr.Event
relays []string
npub string
npubShort string
nprofile string
nevent string
neventNaked string
@@ -151,47 +42,6 @@ type Data struct {
kind1311Metadata *Kind1311Metadata
}
type Kind1063Metadata struct {
Magnet string
Dim string
Size string
Summary string
Image string
URL string
AES256GCM string
M string
X string
I string
Blurhash string
Thumb string
}
type Kind30311Metadata struct {
Title string
Summary string
Image string
Status string
Host sdk.ProfileMetadata
HostNpub string
Tags []string
}
type Kind1311Metadata struct {
// ...
}
func (fm Kind1063Metadata) IsVideo() bool { return strings.Split(fm.M, "/")[0] == "video" }
func (fm Kind1063Metadata) IsImage() bool { return strings.Split(fm.M, "/")[0] == "image" }
func (fm Kind1063Metadata) DisplayImage() string {
if fm.Image != "" {
return fm.Image
} else if fm.IsImage() {
return fm.URL
} else {
return ""
}
}
func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, error) {
// code can be a nevent, nprofile, npub or nip05 identifier, in which case we try to fetch the associated event
event, relays, err := getEvent(ctx, code, nil)
@@ -218,10 +68,10 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e
relays: relays,
}
data.npub, _ = nip19.EncodePublicKey(event.PubKey)
data.npubShort = data.npub[:8] + "…" + data.npub[len(data.npub)-4:]
data.authorLong = data.npub // hopefully will be replaced later
data.authorShort = data.npubShort // hopefully will be replaced later
npub, _ := nip19.EncodePublicKey(event.PubKey)
npubShort := npub[:8] + "…" + npub[len(npub)-4:]
data.authorLong = npub // hopefully will be replaced later
data.authorShort = npubShort // hopefully will be replaced later
data.nevent, _ = nip19.EncodeEvent(event.ID, relaysForNip19, event.PubKey)
data.neventNaked, _ = nip19.EncodeEvent(event.ID, nil, event.PubKey)
data.naddr = ""

119
details.templ Normal file
View File

@@ -0,0 +1,119 @@
package main
import (
"strconv"
)
templ detailsTemplate(params DetailsParams) {
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
if params.Npub != "" {
<div class="mb-6 break-all leading-5">
<div class="text-sm text-strongpink">Author Public Key</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{ params.Npub }</span>
</div>
}
if params.FileMetadata != nil {
if params.FileMetadata.Summary != "" {
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Summary</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{ params.FileMetadata.Summary }</span>
</div>
}
if params.FileMetadata.Dim != "" {
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Dimension</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{ params.FileMetadata.Dim }</span>
</div>
}
if params.FileMetadata.Size != "" {
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Size</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{ params.FileMetadata.Size } bytes</span>
</div>
}
if params.FileMetadata.Magnet != "" {
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Magnet URL</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{ params.FileMetadata.Magnet }</span>
</div>
}
}
if len(params.SeenOn) != 0 {
<div class="mb-6 leading-5 text-neutral-500 dark:text-neutral-300 text-[16px]">
<div class="text-sm text-strongpink">Seen on</div>
for _, v := range params.SeenOn {
<a
href={ templ.URL("/r/" + v) }
class="underline-none pr-2 decoration-neutral-200 decoration-1 underline-offset-[6px] hover:underline"
>{ v }</a>
}
</div>
}
<!-- details hidden behind a toggle -->
if params.HideDetails {
<div class="mb-6 flex items-center print:hidden">
<input
type="checkbox"
id="advanced-switch"
class="hidden"
_="on load make a URLSearchParams from location.search then get it.get('details') then if it is 'yes' set my.checked to true then trigger switch on me end
on change or switch log my checked then if my checked is true
remove .hidden from #hidden-fields
tell the next <label /> from me
add .bg-strongpink .after:translate-x-full
remove .bg-gray-300 .dark:bg-zinc-800
end
otherwise
add .hidden to #hidden-fields
tell the next <label /> from me
remove .bg-strongpink .after:translate-x-full
add .bg-gray-300 .dark:bg-zinc-800
end
end
"
/>
<label
for="advanced-switch"
class="cursor-pointer leading-4 underline text-neutral-500 dark:text-neutral-300 text-[16px] decoration-neutral-200 dark:decoration-neutral-500 decoration-1 underline-offset-[6px]"
>Show more details</label>
</div>
}
<div id="hidden-fields" class={ templ.KV("hidden", params.HideDetails) }>
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Published at</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{ params.CreatedAt }</span>
</div>
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Kind type</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{ strconv.Itoa(params.Kind) }</span>
if params.KindNIP != "" {
<a
href={ templ.URL("https://github.com/nostr-protocol/nips/blob/master/" + params.KindNIP + ".md") }
class="underline decoration-neutral-200 dark:decoration-neutral-500 decoration-1 underline-offset-[6px] text-neutral-500 dark:text-neutral-300 text-[16px]"
>{ params.KindDescription }</a>
}
</div>
if params.Nevent != "" {
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Address Code</div>
<span class="text-[16px] text-neutral-500 dark:text-neutral-300">{ params.Nevent }</span>
</div>
}
<div class="-mx-4 my-8 bg-neutral-100 px-4 pb-4 leading-5 dark:bg-neutral-700">
<div
class="-mx-4 bg-neutral-300 px-4 py-1 text-neutral-100 dark:bg-neutral-800 dark:text-neutral-400"
>
Event JSON
</div>
<div class="mt-4 whitespace-pre-wrap break-all font-mono text-sm">
@templ.Raw(params.EventJSON)
</div>
</div>
if params.Nprofile != "" {
<div class="mb-6 break-all leading-5">
<div class="text-sm text-strongpink">Author Profile Code</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{ params.Nprofile }</span>
</div>
}
</div>
}

70
embedded_note.templ Normal file
View File

@@ -0,0 +1,70 @@
package main
templ embeddedNoteTemplate(params EmbeddedNoteParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link
rel="stylesheet"
type="text/css"
href="/njump/static/tailwind-bundle.min.css"
/>
</head>
<body class="relative bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black sm:items-center sm:justify-center">
<style> ::-webkit-scrollbar { display: none; } </style>
<div class="mx-auto w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 pb-4 pt-4 print:w-full sm:w-11/12 md:w-10/12 lg:w-9/12">
<a href={ "/" + params.Url } target="_new" class="no-underline">
<div class="w-full break-words">
@authorHeaderTemplate(params.Metadata)
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
<article class="prose-cite:text-sm prose mb-6 leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:mb-2 prose-blockquote:mt-2 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-neutral-200 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-blockquote:pt-0 prose-blockquote:font-light prose-blockquote:not-italic prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-neutral-700 sm:prose-a:text-justify">
if params.Subject != "" {
<h1 class="text-2xl">{ params.Subject }</h1>
}
<!-- main content -->
<div dir="auto">{ params.Content }</div>
<div class="mt-2 w-full text-right text-sm text-stone-400">
{ params.CreatedAt }
</div>
</article>
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
</div>
</a>
<div class="text-sm leading-3 text-neutral-400">
This note has been published on Nostr and is embedded via Njump,
<a href="/" target="_new" class="underline">learn more</a>
</div>
</div>
<svg width="0" height="0" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="svg-shape" clipPathUnits="objectBoundingBox">
<path
transform="scale(0.005, 0.005)"
d="M100,200c43.8,0,68.2,0,84.1-15.9C200,168.2,200,143.8,200,100s0-68.2-15.9-84.1C168.2,0,143.8,0,100,0S31.8,0,15.9,15.9C0,31.8,0,56.2,0,100s0,68.2,15.9,84.1C31.8,200,56.2,200,100,200z"
></path>
</clipPath>
</defs>
</svg>
<script>
window.addEventListener('load', function () {
var contentHeight = document.body.scrollHeight
window.parent.postMessage({height: contentHeight}, '*')
})
window.addEventListener('message', function (event) {
if (event.data.showGradient) {
var gradient = document.getElementById('bottom-gradient')
gradient.classList.remove('hidden')
}
if (event.data.setDarkMode) {
document.querySelector('html').classList.add('theme--dark')
}
})
</script>
<div id="bottom-gradient" class="pointer-events-none sticky bottom-0 left-0 hidden h-20 w-full bg-gradient-to-b from-transparent to-white dark:to-neutral-900"></div>
<a href="/" target="_new" class="fixed bottom-2 right-2 w-[100px]"><img src="/njump/static/logo.png"/></a>
</body>
</html>
}

126
embedded_profile.templ Normal file
View File

@@ -0,0 +1,126 @@
package main
templ embeddedProfileTemplate(params EmbeddedProfileParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link
rel="stylesheet"
type="text/css"
href="/njump/static/tailwind-bundle.min.css"
/>
</head>
<body
class="relative bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
<style> ::-webkit-scrollbar { display: none; } </style>
<div
class="mx-auto w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 pb-4 pt-4 print:w-full sm:w-11/12 md:w-10/12 lg:w-9/12"
>
<a href="/{params.Npub}" target="_new" class="no-underline">
<div class="w-full break-words">
<div class="w-full break-words print:w-full">
<header class="mb-4 max-w-full">
<div class="flex flex-wrap items-center">
<div
class="print:basis-1-12 imgclip mr-2 max-w-full basis-1/6 overflow-hidden sm:mr-4 sm:basis-[10%]"
>
<img
class="block h-auto w-full"
src="{params.Metadata.Picture}"
/>
</div>
<div class="block print:text-base">
<div class="text-2xl">{ params.Metadata.Name }</div>
if params.Metadata.Name != params.Metadata.DisplayName {
<div class="leading-4 text-stone-400">
{ params.Metadata.DisplayName }
</div>
}
</div>
</div>
</header>
if params.Metadata.Website != "" || params.RenderedAuthorAboutText != "" {
<div dir="auto">
{ params.RenderedAuthorAboutText }
</div>
<div class="-ml-4 mb-6 h-1.5 w-1/2 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
<div class="mb-6 leading-5">{ params.Metadata.Website }</div>
<div class="prose mb-6 leading-5 dark:prose-invert prose-headings:font-light sm:prose-a:text-justify">
{ params.RenderedAuthorAboutText }
</div>
}
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Public Key</div>
{ params.Npub }
</div>
<div class="mb-6 leading-5">
if params.Metadata.NIP05 != "" {
<div class="text-sm text-strongpink">NIP-05 Address</div>
{ params.Metadata.NIP05 }
}
</div>
<div class="mb-6 leading-5">
if params.Metadata.LUD16 != "" {
<div class="text-sm text-strongpink">NIP-57 Address</div>
{ params.Metadata.LUD16 }
}
</div>
if len(params.AuthorRelays) > 0 {
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Publishing to</div>
for index, element := range params.AuthorRelays {
<span class="mr-1 mt-2 inline-block max-w-full rounded-lg border border-slate-300 px-2 py-0.5">{ element }</span>
}
</div>
}
</div>
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
</div>
</a>
<div class="text-sm leading-3 text-neutral-400">
This note has been published on Nostr and is embedded via Njump,
<a href="/" target="_new" class="underline">learn more</a>
</div>
</div>
<svg width="0" height="0" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="svg-shape" clipPathUnits="objectBoundingBox">
<path
transform="scale(0.005, 0.005)"
d="M100,200c43.8,0,68.2,0,84.1-15.9C200,168.2,200,143.8,200,100s0-68.2-15.9-84.1C168.2,0,143.8,0,100,0S31.8,0,15.9,15.9C0,31.8,0,56.2,0,100s0,68.2,15.9,84.1C31.8,200,56.2,200,100,200z"
></path>
</clipPath>
</defs>
</svg>
<script>
window.addEventListener('load', function () {
var contentHeight = document.body.scrollHeight
window.parent.postMessage({height: contentHeight}, '*')
})
window.addEventListener('message', function (event) {
if (event.data.showGradient) {
var gradient = document.getElementById('bottom-gradient')
gradient.classList.remove('hidden')
}
if (event.data.setDarkMode) {
document.querySelector('html').classList.add('theme--dark')
}
})
</script>
<div
id="bottom-gradient"
class="pointer-events-none sticky bottom-0 left-0 hidden h-20 w-full bg-gradient-to-b from-transparent to-white dark:to-neutral-900"
></div>
<a href="/" target="_new" class="fixed bottom-2 right-2 w-[100px]">
<img
src="/njump/static/logo.png"
/>
</a>
</body>
</html>
}

35
error.templ Normal file
View File

@@ -0,0 +1,35 @@
package main
templ errorTemplate(params ErrorPageParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<title>Error</title>
@headCommonTemplate(params.HeadParams)
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
@topTemplate()
<div class="mx-auto mt-12 w-10/12 text-center lg:w-9/12">
<div class="mx-auto w-4/5 sm:w-3/5">
<div class="mt-4 text-2xl leading-6">
@templ.Raw(params.Message)
</div>
<div class="my-8 italic text-neutral-400 dark:text-neutral-500">
{ params.Errors }
</div>
<div>
Are you lost?
<a
href="/"
class="block leading-3 underline decoration-neutral-400 underline-offset-4"
>Go to the homepage</a>
</div>
</div>
</div>
@footerTemplate()
</body>
</html>
}

73
file_metadata.templ Normal file
View File

@@ -0,0 +1,73 @@
package main
templ fileMetadataTemplate(params FileMetadataPageParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<title>File Metadata</title>
@openGraphTemplate(params.OpenGraphParams)
@headCommonTemplate(params.HeadParams)
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
@topTemplate()
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div
class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<div class="w-full break-words print:w-full sm:w-3/4">
@authorHeaderTemplate(params.Metadata)
<div class="w-full text-right text-sm text-stone-400">
{ params.CreatedAt }
</div>
<div class="w-full text-right text-sm text-stone-400">
if params.ParentLink != "" {
in reply to
<span class="text-strongpink">
@templ.Raw(params.ParentLink)
</span>
}
</div>
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
<article
class="prose-cite:text-sm prose mb-6 leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-gray-100 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-zinc-800 sm:prose-a:text-justify"
>
if params.Subject != "" {
<h1 class="text-2xl">{ params.Subject }</h1>
} else {
<h1 class="hidden">
{ params.Metadata.ShortName() } on Nostr: { params.TitleizedContent }
</h1>
}
<!-- main content -->
if params.FileMetadata.Image != "" {
<img src={ params.FileMetadata.Image } alt={ params.Alt }/>
} else if params.IsImage {
<img src={ params.FileMetadata.URL } alt={ params.Alt }/>
} else if params.IsVideo {
<video
controls
width="100%%"
class="max-h-[90vh] bg-neutral-300 dark:bg-zinc-700"
>
<source src={ params.FileMetadata.URL } alt={ params.Alt }/>
</video>
}
<a
href={ templ.URL(params.FileMetadata.URL) }
target="_new"
class="not-prose mx-auto mb-3 block w-4/5 basis-full rounded-lg border-0 bg-strongpink px-4 py-2 text-center text-[17px] font-light text-white no-underline sm:w-2/6"
>Download file</a>
</article>
@detailsTemplate(params.DetailsParams)
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
</div>
@clientsTemplate(params.Clients)
</div>
</div>
@footerTemplate()
</body>
</html>
}

22
footer.templ Normal file
View File

@@ -0,0 +1,22 @@
package main
templ footerTemplate() {
<div
class="fixed -inset-x-24 inset-y-64 -z-10 h-full w-[200%] -rotate-12 overflow-hidden bg-gray-50 dark:bg-neutral-950 print:hidden"
></div>
<footer class="mb-4 mt-6 text-center text-sm text-gray-400">
powered by
<a class="text-gray-400 underline" href="/">njump</a> & open-sourced on
<a class="text-gray-400 underline" href="https://github.com/fiatjaf/njump">github</a>
</footer>
<svg width="0" height="0" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="svg-shape" clipPathUnits="objectBoundingBox">
<path
transform="scale(0.005, 0.005)"
d="M100,200c43.8,0,68.2,0,84.1-15.9C200,168.2,200,143.8,200,100s0-68.2-15.9-84.1C168.2,0,143.8,0,100,0S31.8,0,15.9,15.9C0,31.8,0,56.2,0,100s0,68.2,15.9,84.1C31.8,200,56.2,200,100,200z"
></path>
</clipPath>
</defs>
</svg>
}

9
go.mod
View File

@@ -3,7 +3,8 @@ module github.com/fiatjaf/njump
go 1.21.4
require (
github.com/PuerkitoBio/goquery v1.5.0
github.com/PuerkitoBio/goquery v1.8.1
github.com/a-h/templ v0.2.513
github.com/dgraph-io/badger/v4 v4.2.0
github.com/fiatjaf/eventstore v0.3.6
github.com/fiatjaf/khatru v0.2.1
@@ -32,7 +33,7 @@ require (
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/cascadia v1.0.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
@@ -57,8 +58,8 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect

45
go.sum
View File

@@ -1,12 +1,14 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/a-h/templ v0.2.513 h1:ZmwGAOx4NYllnHy+FTpusc4+c5msoMpPIYX0Oy3dNqw=
github.com/a-h/templ v0.2.513/go.mod h1:9gZxTLtRzM3gQxO8jr09Na0v8/jfliS97S9W5SScanM=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
@@ -122,8 +124,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
@@ -146,10 +148,13 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8=
github.com/nbd-wtf/emoji v0.0.3 h1:YtkT7MVPXvqU1SQjvC/CShlWexnREzqNCxmhUnL00CA=
@@ -223,12 +228,14 @@ github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
@@ -239,12 +246,11 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -254,6 +260,10 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -262,6 +272,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -273,15 +284,28 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -292,6 +316,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

70
head_common.templ Normal file
View File

@@ -0,0 +1,70 @@
package main
templ headCommonTemplate(params HeadParams) {
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
if params.Oembed != "" {
<link
rel="alternate"
type="application/json+oembed"
href={ params.Oembed + "&format=json" }
/>
<link rel="alternate" type="text/xml+oembed" href={ params.Oembed + "&format=xml" }/>
}
if params.IsProfile {
<link
rel="apple-touch-icon"
sizes="180x180"
href="/njump/static/favicon/profile/apple-touch-icon.png?v=2"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/njump/static/favicon/profile/favicon-32x32.png?v=2"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/njump/static/favicon/profile/favicon-16x16.png?v=2"
/>
} else {
<link
rel="apple-touch-icon"
sizes="180x180"
href="/njump/static/favicon/event/apple-touch-icon.png?v=2"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/njump/static/favicon/event/favicon-32x32.png?v=2"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/njump/static/favicon/event/favicon-16x16.png?v=2"
/>
}
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
if params.TailwindDebugStuff != "" {
@templ.Raw(params.TailwindDebugStuff)
} else {
<link
rel="stylesheet"
type="text/css"
href="/njump/static/tailwind-bundle.min.css"
/>
}
<style> @media print { @page { margin: 2cm 3cm; } } </style>
<meta name="theme-color" content="#e42a6d"/>
if params.NaddrNaked != "" {
<link rel="canonical" href={ "https://njump.me/" + params.NaddrNaked }/>
} else {
<link rel="canonical" href={ "https://njump.me/" + params.NeventNaked }/>
}
<script type="text/hyperscript">
on load get [navigator.userAgent.includes('Safari'), navigator.userAgent.includes('Chrome')] then if it[0] is true and it[1] is false add .safari to <body /> end
</script>
}

247
homepage.templ Normal file
View File

@@ -0,0 +1,247 @@
package main
templ homepageTemplate(params HomePageParams) {
<!DOCTYPE html>
<html class="theme--default font-light">
<meta charset="UTF-8"/>
<head>
<title>njump - the nostr static gateway</title>
<meta name="description" content=""/>
@head_commonTemplate(params.HeadCommonParams)
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
@topTemplate()
<div class="mx-auto sm:mt-8 block px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div class="flex w-full max-w-screen-xl justify-between gap-10 overflow-visible px-1 print:w-full sm:w-9/12 xl:w-3/5">
<div>
<h2 class="text-2xl text-strongpink">What is njump?</h2>
<p class="my-3 leading-5">
<i>njump</i> is a HTTP
<a class="underline" href="https://github.com/nostr-protocol/nostr">
Nostr
</a>
gateway that allows you to browse profiles, notes and relays; it is
an easy way to preview a resource and then open it with your
preferred client. The typical use of <i>njump</i> is to share a
resource outside the Nostr world, where the
<code>nostr:</code> schema is not (yet) working.
</p>
<p class="my-3 leading-5">
<i>njump</i> currently lives under { params.Host }, you can reach it
appending a Nostr
<a
class="underline"
href="https://github.com/nostr-protocol/nips/blob/master/19.md"
>
NIP-19
</a>
entity (<code>npub</code>, <code>nevent</code>, <code>naddr</code>,
etc) after the domain:
<span class="rounded bg-lavender px-1 dark:bg-garnet">
{ params.Host }/&lt;nip-19-entity&gt;
</span>.
</p>
<p class="my-3 leading-5">
For example, here's
<a
class="underline"
href="/npub1sn0wdenkukak0d9dfczzeacvhkrgz92ak56egt7vdgzn8pv2wfqqhrjdv9"
>
a user profile
</a>,
<a
class="underline"
href="/nevent1qqstnl4ddmhc0kzqpj7p543pvq9nvppc4laewc9x5ppucz7aagsa4dspzemhxue69uhhyetvv9ujumn0wd68ytnzv9hxgqgewaehxw309ac8junpd45kgtnxd9shg6npvchxxmmdqyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsds02v2"
>
a note
</a>
and a
<a
class="underline"
href="/naddr1qqxnzd3cxqmrzv3exgmr2wfeqy08wumn8ghj7mn0wd68yttsw43zuam9d3kx7unyv4ezumn9wshszyrhwden5te0dehhxarj9ekk7mf0qy88wumn8ghj7mn0wvhxcmmv9uq3zamnwvaz7tmwdaehgu3wwa5kuef0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qdergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsxpqqqp65wy2vhhv"
>
long blog post
</a>
.
</p>
<h2 class="text-xl text-strongpink">
Try it now, jump to some Nostr content
</h2>
<div
class="my-3 mb-8 rounded-lg bg-zinc-100 p-4 pb-3 dark:bg-neutral-900 sm:p-6 sm:pb-4"
>
<form
_="on submit halt the event's default then go to url `/${event.target.code.value}`"
>
<div
class="flex flex-wrap items-center justify-center sm:flex-nowrap sm:justify-normal"
>
<div class="mb-1.5 text-xl sm:mb-0">{ params.Host }/</div>
<input
name="code"
placeholder="paste a npub / nprofile / nevent / ..."
autofocus
class="ml-0 w-full basis-full rounded-lg border-0 bg-white p-2 text-base text-gray-700 placeholder:text-gray-300 focus:outline-0 dark:bg-zinc-900 dark:text-neutral-50 dark:placeholder:text-gray-400 sm:ml-1 sm:basis-11/12 sm:rounded-s-lg"
/>
<button
class="ml-0 w-full basis-full rounded-lg border-0 bg-strongpink p-2 text-base uppercase text-white sm:-ml-4 sm:basis-2/12 sm:rounded-s-lg"
>
View
</button>
</div>
</form>
<div class="mt-3 text-center text-sm sm:mt-1 sm:text-left">
or pick
<a
class="underline"
href="/random"
_="on click halt the event then fetch /random with method:'POST' then tell <input[name='code'] /> set @value to result"
>
some random content
</a>
</div>
</div>
<p class="my-3 leading-5">
There are several reasons to choose <i>njump</i> when sharing Nostr
content outside of Nostr:
</p>
<h2 class="mt-7 text-2xl text-strongpink">Clean, fast and solid</h2>
<p class="my-3 leading-5">
Pages by <i>njump</i> are extremely lightweight and fast to load
because there isn't any client side javascript involved; they are
minimalistic with the right attention to typography, focusing the
content without unnecessary details. Furthermore they are cached, so
when sharing a page you can expect the other part will load it
without any glitch in a fraction of second: the perfect tool to
onboard new users!
</p>
<h2 class="mt-7 text-2xl text-strongpink">Good preview</h2>
<p class="my-3 leading-5">
<i>njump</i> renders everything on the server-side, so it is able to
generate useful rich previews that work on Telegram, Discord,
Twitter and other places.
</p>
<p class="my-3 leading-5">
When opening the URL directly in a browser, visitors will find
referenced content like images, video and links to referenced Nostr
events displayed in a simple but effective way. It shows the note
parent, allowing the visitor to follow it up and it has custom CSS for
printing or exporting to PDF.
</p>
<h2 class="mt-7 text-2xl text-strongpink">Cooperative (jump-out)</h2>
<p class="my-3 leading-5">
<i>njump</i> is not interested capturing users at all, on the
contrary it invites them to "jump" to the Nostr resource by picking
from a list of web clients or with a <code>nostr:</code> prefix for native
clients. It even remembers the most used one for each visitor and
puts it on the top for fast clicking or tap.
</p>
<p class="my-3 leading-5">
<a class="underline" href="https://github.com/nostr-protocol/nips/blob/master/89.md">NIP-89</a>
support coming!
</p>
<h2 class="mt-7 text-2xl text-strongpink">
Search friendly (jump-in)
</h2>
<p class="my-3 leading-5">
This is crucial: <i>njump</i> pages are static so search engines can
index them, this means that <i>njump</i> can help others to discover
great content on Nostr, jump in and join us! <i>njump</i> is the
only nostr resource that has this explicit goal, if you care that a
good note can be found online use <i>njump</i> to share it, this way
you also help Nostr flourish.
</p>
<h2 class="mt-7 text-2xl text-strongpink">Share NIP-05 profiles</h2>
<p class="my-3 leading-5">
Now you can share your own profile with a pretty
<a
class="underline"
href="https://github.com/nostr-protocol/nips/blob/master/05.md"
>
NIP-05
</a>
inspired permalink:
<span class="rounded bg-lavender px-1 dark:bg-garnet">
{ params.Host }/&lt;nip-05&gt;
</span>
, for example:
<a class="underline" href="/nvk.org">https://{ params.Host }/nvk.org</a>
or
<a class="underline" href="/mike@mikedilger.com">
https://{ params.Host }/mike@mikedilger.com
</a>
.
</p>
<p class="my-3 leading-5">
A profile shows the basic metadata infos, the used "outbox" relays
and the last notes.
</p>
<h2 class="mt-7 text-2xl text-strongpink">
Share on Twitter and Telegram
</h2>
<p class="my-3 leading-5">
Now you can quickly and effortlessly share Nostr notes on Twitter
and Telegram, and maybe on many other "social platforms": just drop
a link, and njump will render the note text using the preview image
as a canvas, to maximize the sharing experience and utility.
<br/>
On Telegram we have also the Instant View to access long content
in-app!
</p>
<h2 class="mt-7 text-2xl text-strongpink">Relays view</h2>
<p class="my-3 leading-5">
You can have a view of the last content posted to a relay using
<span class="rounded bg-lavender px-1 dark:bg-garnet">
{ params.Host }/r/&lt;relay-host&gt;
</span>
, for example:
<a class="underline" href="/r/nostr.wine">
https://{ params.Host }/r/nostr.wine
</a>
</p>
<p class="my-3 leading-5">
Some basic infos (
<a
href="https://github.com/nostr-protocol/nips/blob/master/11.md"
>
NIP-11
</a>
) are available; We hope operators will start to make them more
personal and informative so users can have a way to evaluate if/when
to join a relay.
</p>
<h2 class="mt-7 text-2xl text-strongpink">Website widgets</h2>
<div class="my-3 leading-5">
You can embed notes, long form contents and profiles in a web page
with a simple script:
<br/>
<span class="rounded bg-lavender px-1 dark:bg-garnet">
&lt;script src="https://{ params.Host }/embed/&lt;nip-19-entity&gt;"
/&gt;
</span>
<div class="mt-4 gap-8 sm:flex">
<div class="mb-4 flex-auto sm:mb-0">
<script src="/embed/npub1sn0wdenkukak0d9dfczzeacvhkrgz92ak56egt7vdgzn8pv2wfqqhrjdv9"></script>
</div>
<div class="flex-auto">
<script src="/embed/naddr1qqxnzd3cxqmrzv3exgmr2wfeqy08wumn8ghj7mn0wd68yttsw43zuam9d3kx7unyv4ezumn9wshszyrhwden5te0dehhxarj9ekk7mf0qy88wumn8ghj7mn0wvhxcmmv9uq3zamnwvaz7tmwdaehgu3wwa5kuef0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qdergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsxpqqqp65wy2vhhv"></script>
</div>
</div>
</div>
<h2 class="mt-7 text-2xl text-strongpink">Inspector tool</h2>
<p class="my-3 leading-5">
You know, we are all programmers, including our moms, so for every
<i>njump</i> page you can toggle the "Show more details" switch and
inspect the full event JSON. Without installing other tools (like
<a class="underline" href="https://github.com/fiatjaf/nak">nak</a>)
this is probably the fastest way to access that.
</p>
</div>
</div>
</div>
@footerTemplate()
</body>
</html>
}

View File

@@ -1,7 +1,34 @@
export PATH := "./node_modules/.bin:" + env_var('PATH')
dev:
TAILWIND_DEBUG=true SKIP_LANGUAGE_MODEL=true go build -o /tmp/njump && /tmp/njump
fd --no-ignore-vcs 'go|templ|base.css' | entr -r bash -c 'TAILWIND_DEBUG=true SKIP_LANGUAGE_MODEL=true && templ generate && go build -o /tmp/njump && /tmp/njump'
build: templ tailwind
go build -o ./njump
deploy: templ tailwind
sed -i.bak "s#/tailwind-bundle.min.css#/tailwind-bundle.min.css?$(date +'%Y%m%d%H%M')#g" templates/head_common.html
GOOS=linux GOARCH=amd64 go build -o ./njump
mv -f templates/head_common.html.bak templates/head_common.html
rsync --progress njump njump:njump/njump-new
ssh njump 'systemctl stop njump'
ssh njump 'mv njump/njump-new njump/njump'
ssh njump 'systemctl start njump'
debug-build: templ tailwind
go build -tags=nocache -o ./tmp/main .
templ:
templ generate
prettier:
prettier -w templates/*.html
tailwind:
tailwind -i base.css -o static/tailwind-bundle.min.css --minify
test:
go test -tags=nocache
check-samples:
#!/usr/bin/env xonsh
@@ -21,27 +48,3 @@ check-samples:
for code in samples:
$(chromium @(base_url + '/' + code))
$(chromium @(base_url + '/njump/image/' + code))
build: tailwind
go build -o ./njump
deploy: tailwind
sed -i.bak "s#/tailwind-bundle.min.css#/tailwind-bundle.min.css?$(date +'%Y%m%d%H%M')#g" templates/head_common.html
GOOS=linux GOARCH=amd64 go build -o ./njump
mv -f templates/head_common.html.bak templates/head_common.html
rsync --progress njump njump:njump/njump-new
ssh njump 'systemctl stop njump'
ssh njump 'mv njump/njump-new njump/njump'
ssh njump 'systemctl start njump'
debug-build: tailwind
go build -tags=nocache -o ./tmp/main .
prettier:
prettier -w templates/*.html
tailwind:
tailwind -i tailwind.css -o static/tailwind-bundle.min.css --minify
test:
go test -tags=nocache

81
live_event.templ Normal file
View File

@@ -0,0 +1,81 @@
package main
templ liveEventTemplate(params LiveEventMessagePageParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<title>Stream: { params.LiveEvent.Title } by { params.LiveEvent.Host.Name }</title>
@openGraphTemplate(params.OpenGraphParams)
@headCommonTemplate(params.HeadParams)
</head>
<body class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black">
@topTemplate()
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw">
<div class="w-full break-words print:w-full sm:w-3/4">
@authorHeaderTemplate(params.Metadata)
<div class="w-full text-right text-sm text-stone-400">
{ params.CreatedAt }
</div>
<div class="w-full text-right text-sm text-stone-400">
if params.ParentLink != "" {
in reply to
<span class="text-strongpink">
@templ.Raw(params.ParentLink)
</span>
}
</div>
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
<article class="prose-cite:text-sm prose mb-6 max-w-full leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-gray-100 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-zinc-800 sm:prose-a:text-justify">
<h1 class="text-2xl">
<span class="mr-2">{ params.LiveEvent.Title }</span>
switch params.LiveEvent.Status {
case "ended":
<span class="whitespace-nowrap rounded bg-neutral-400 px-4 py-1 align-text-top text-base text-white dark:bg-neutral-700">Ended</span>
case "live":
<span class="whitespace-nowrap rounded bg-strongpink px-4 py-1 align-text-top text-base text-white">Live now!</span>
}
</h1>
<div class="mb-4">
if params.LiveEvent.HostNpub != "" {
Streaming hosted by
<a href="/{params.LiveEvent.HostNpub }">
{ params.LiveEvent.Host.Name }
</a>
}
</div>
<!-- main content -->
<div class="mb-4">
for _, v := range params.LiveEvent.Tags {
<span
class="mr-2 whitespace-nowrap rounded bg-neutral-200 px-2 dark:bg-neutral-700 dark:text-white"
>{ v }</span>
}
</div>
if params.LiveEvent.Summary != "" {
<div>{ params.LiveEvent.Summary }</div>
}
if params.LiveEvent.Image != "" {
<img
src={ params.LiveEvent.Image }
alt={ params.Alt }
_="on load
repeat until '{params.LiveEvent.Status }' == 'ended'
set @src to '{params.LiveEvent.Image }'
wait 5s
end
"
/>
}
</article>
@detailsTemplate(params.DetailsParams)
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
</div>
@clientsTemplate(params.Clients)
</div>
</div>
@footerTemplate()
</body>
</html>
}

52
live_event_message.templ Normal file
View File

@@ -0,0 +1,52 @@
package main
templ liveEventMessageTemplate(params LiveEventMessagePageParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<title>{ params.TitleizedContent }</title>
@openGraphTemplate(params.OpenGraphParams)
@headCommonTemplate(params.HeadParams)
</head>
<body class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black">
@topTemplate()
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw">
<div class="w-full break-words print:w-full sm:w-3/4">
@authorHeaderTemplate(params.Metadata)
<div class="w-full text-right text-sm text-stone-400">
{ params.CreatedAt }
</div>
<div class="w-full text-right text-sm text-stone-400">
if params.ParentLink != "" {
messaging during the live event
<span class="text-strongpink">
@templ.Raw(params.ParentLink)
</span>
}
</div>
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
<article
class="prose-cite:text-sm prose mb-6 leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-gray-100 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-zinc-800 sm:prose-a:text-justify"
>
if params.Subject != "" {
<h1 class="text-2xl">{ params.Subject }</h1>
} else {
<h1 class="hidden">
{ params.Metadata.ShortName() } on Nostr: { params.TitleizedContent }
</h1>
}
<!-- main content -->
@templ.Raw(params.Content)
</article>
@detailsTemplate(params.DetailsParams)
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
</div>
@clientsTemplate(params.Clients)
</div>
</div>
@footerTemplate()
</body>
</html>
}

View File

@@ -30,9 +30,6 @@ type Settings struct {
//go:embed static/*
var static embed.FS
//go:embed templates/*
var templates embed.FS
var (
s Settings
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()

52
note.templ Normal file
View File

@@ -0,0 +1,52 @@
package main
templ noteTemplate(params NotePageParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<title>{ params.TitleizedContent }</title>
@openGraphTemplate(params.OpenGraphParams)
@headCommonTemplate(params.HeadParams)
</head>
<body class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black">
@topTemplate()
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw">
<div class="w-full break-words print:w-full sm:w-3/4">
@authorHeaderTemplate(params.Metadata)
<div class="w-full text-right text-sm text-stone-400">
{ params.CreatedAt }
</div>
<div class="w-full text-right text-sm text-stone-400">
if params.ParentLink != "" {
in reply to
<span class="text-strongpink">
@templ.Raw(params.ParentLink)
</span>
}
</div>
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
<article class="prose-cite:text-sm prose mb-6 leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:mb-2 prose-blockquote:mt-2 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-neutral-200 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-blockquote:pt-0 prose-blockquote:font-light prose-blockquote:not-italic prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-neutral-700 sm:prose-a:text-justify">
if params.Subject != "" {
<h1 class="text-2xl">{ params.Subject }</h1>
} else {
<h1 class="hidden">
{ params.Metadata.ShortName() } on Nostr: { params.TitleizedContent }
</h1>
}
<!-- main content -->
<div dir="auto">
@templ.Raw(params.Content)
</div>
</article>
@detailsTemplate(params.DetailsParams)
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
</div>
@clientsTemplate(params.Clients)
</div>
</div>
@footerTemplate()
</body>
</html>
}

44
opengraph.templ Normal file
View File

@@ -0,0 +1,44 @@
package main
templ openGraphTemplate(params OpenGraphParams) {
if params.SingleTitle != "" {
<!-- we only display this on twitter as a single title -->
<meta name="twitter:title" content={ params.SingleTitle }/>
} else {
<!-- these are not shown by twitter at all, so let's not even give them -->
<meta property="og:site_name" content={ params.Superscript }/>
<meta property="og:title" content={ params.Subscript }/>
}
<!-- this is used for when we want to take over the entire screen on twitter,
mostly for the big "text-to-image" images -->
if params.BigImage != "" {
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:site" content="@nostrprotocol"/>
<meta property="og:image" content={ params.BigImage }/>
<meta name="twitter:image" content={ params.BigImage }/>
} else {
<!-- otherwise we tell twitter to display it as a normal text-based embed.
these distinctions don't seem to make any difference in other platforms,
maybe telegram -->
<meta name="twitter:card" content="summary"/>
if params.Image != "" {
<meta property="og:image" content={ params.Image }/>
<meta name="twitter:image" content={ params.ProxiedImage }/>
}
<!---->
if params.Video != "" {
<meta property="og:video" content={ params.Video }/>
<meta property="og:video:secure_url" content={ params.Video }/>
<meta property="og:video:type" content="video/{params.VideoType}"/>
}
}
<!-- now just display the short text if we have any (which we always should) -->
if params.Text != "" {
<meta property="og:description" content={ params.Text }/>
<meta name="twitter:description" content={ params.Text }/>
}
}
templ bigImagePrerender(bigImage string) {
<img src="{bigImage}" class="absolute left-[-999px] w-[100px]"/>
}

37
other.templ Normal file
View File

@@ -0,0 +1,37 @@
package main
import "strconv"
templ otherTemplate(params OtherPageParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<title>Nostr Event { strconv.Itoa(params.Kind) } - { params.KindDescription }</title>
@headCommonTemplate(params.HeadParams)
</head>
<body class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black">
@topTemplate()
<div class="mx-auto block px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div class="flex w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 print:w-full sm:w-11/12 md:w-10/12 lg:w-9/12 lg:gap-48vw">
<div class="w-full break-words print:w-full md:w-10/12 lg:w-9/12">
<header class="">
<div class="mb-4 text-2xl">{ params.KindDescription }</div>
</header>
if params.Alt != "" {
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<article class="prose-cite:text-sm prose mb-6 leading-5 dark:prose-invert">
{ params.Alt }
</article>
}
@detailsTemplate(params .DetailsParams)
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"></div>
</div>
</div>
</div>
@footerTemplate()
</body>
</html>
}

403
pages.go
View File

@@ -8,31 +8,9 @@ import (
"strings"
"github.com/nbd-wtf/go-nostr/nip11"
sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/tylermmorton/tmpl"
)
type TemplateID int
const (
Note TemplateID = iota
Profile
LongForm
TelegramInstantView
FileMetadata
LiveEvent
LiveEventMessage
Other
)
var (
//go:embed templates/opengraph.html
tmplOpenGraph string
OpenGraphTemplate = tmpl.MustCompile(&OpenGraphPartial{})
)
//tmpl:bind head_common.html
type OpenGraphPartial struct {
type OpenGraphParams struct {
SingleTitle string
// x (we will always render just the singletitle if we have that)
Superscript string
@@ -49,44 +27,7 @@ type OpenGraphPartial struct {
Text string
}
func (*OpenGraphPartial) TemplateText() string { return tmplOpenGraph }
var (
//go:embed templates/head_common.html
tmplHeadCommon string
HeadCommonTemplate = tmpl.MustCompile(&HeadCommonPartial{})
)
//tmpl:bind head_common.html
type HeadCommonPartial struct {
IsProfile bool
TailwindDebugStuff template.HTML
NaddrNaked string
NeventNaked string
Oembed string
}
func (*HeadCommonPartial) TemplateText() string { return tmplHeadCommon }
var (
//go:embed templates/top.html
tmplTop string
TopTemplate = tmpl.MustCompile(&TopPartial{})
)
//tmpl:bind top.html
type TopPartial struct{}
func (*TopPartial) TemplateText() string { return tmplTop }
var (
//go:embed templates/details.html
tmplDetails string
DetailsTemplate = tmpl.MustCompile(&DetailsPartial{})
)
//tmpl:bind details.html
type DetailsPartial struct {
type DetailsParams struct {
HideDetails bool
CreatedAt string
EventJSON template.HTML
@@ -103,41 +44,15 @@ type DetailsPartial struct {
LiveEvent *Kind30311Metadata
}
func (*DetailsPartial) TemplateText() string { return tmplDetails }
var (
//go:embed templates/clients.html
tmplClients string
ClientsTemplate = tmpl.MustCompile(&ClientsPartial{})
)
//tmpl:bind clients.html
type ClientsPartial struct {
Clients []ClientReference
type HeadParams struct {
IsProfile bool
TailwindDebugStuff template.HTML
NaddrNaked string
NeventNaked string
Oembed string
}
func (*ClientsPartial) TemplateText() string { return tmplClients }
var (
//go:embed templates/footer.html
tmplFooter string
FooterTemplate = tmpl.MustCompile(&FooterPartial{})
)
//tmpl:bind footer.html
type FooterPartial struct {
BigImage string
}
func (*FooterPartial) TemplateText() string { return tmplFooter }
var (
//go:embed templates/telegram_instant_view.html
tmplTelegramInstantView string
TelegramInstantViewTemplate = tmpl.MustCompile(&TelegramInstantViewPage{})
)
type TelegramInstantViewPage struct {
type TelegramInstantViewParams struct {
Video string
VideoType string
Image string
@@ -145,42 +60,22 @@ type TelegramInstantViewPage struct {
Content template.HTML
Description string
Subject string
Metadata sdk.ProfileMetadata
Metadata Metadata
AuthorLong string
CreatedAt string
ParentLink template.HTML
}
func (*TelegramInstantViewPage) TemplateText() string { return tmplTelegramInstantView }
var (
//go:embed templates/homepage.html
tmplHomePage string
HomePageTemplate = tmpl.MustCompile(&HomePage{})
)
type HomePage struct {
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
FooterPartial `tmpl:"footer"`
type HomePageParams struct {
HeadParams
Host string
Npubs []string
LastNotes []string
}
func (*HomePage) TemplateText() string { return tmplHomePage }
var (
//go:embed templates/archive.html
tmplArchive string
ArchiveTemplate = tmpl.MustCompile(&ArchivePage{})
)
type ArchivePage struct {
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
FooterPartial `tmpl:"footer"`
type ArchivePageParams struct {
HeadParams
Title string
PathPrefix string
@@ -191,64 +86,36 @@ type ArchivePage struct {
PrevPage int
}
func (*ArchivePage) TemplateText() string { return tmplArchive }
var (
//go:embed templates/other.html
tmplOther string
OtherTemplate = tmpl.MustCompile(&OtherPage{})
)
type OtherPage struct {
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
DetailsPartial `tmpl:"details"`
FooterPartial `tmpl:"footer"`
type OtherPageParams struct {
HeadParams
DetailsParams
Kind int
KindDescription string
Alt string
}
func (*OtherPage) TemplateText() string { return tmplOther }
var (
//go:embed templates/note.html
tmplNote string
NoteTemplate = tmpl.MustCompile(&NotePage{})
)
type NotePage struct {
OpenGraphPartial `tmpl:"opengraph"`
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
DetailsPartial `tmpl:"details"`
ClientsPartial `tmpl:"clients"`
FooterPartial `tmpl:"footer"`
type NotePageParams struct {
OpenGraphParams
HeadParams
DetailsParams
Content template.HTML
CreatedAt string
Metadata sdk.ProfileMetadata
Metadata Metadata
Npub string
NpubShort string
ParentLink template.HTML
SeenOn []string
Subject string
TitleizedContent string
Clients []ClientReference
}
func (*NotePage) TemplateText() string { return tmplNote }
var (
//go:embed templates/embedded_note.html
tmplEmbeddedNote string
EmbeddedNoteTemplate = tmpl.MustCompile(&EmbeddedNotePage{})
)
type EmbeddedNotePage struct {
type EmbeddedNoteParams struct {
Content template.HTML
CreatedAt string
Metadata sdk.ProfileMetadata
Metadata Metadata
Npub string
NpubShort string
SeenOn []string
@@ -256,27 +123,16 @@ type EmbeddedNotePage struct {
Url string
}
func (*EmbeddedNotePage) TemplateText() string { return tmplEmbeddedNote }
var (
//go:embed templates/profile.html
tmplProfile string
ProfileTemplate = tmpl.MustCompile(&ProfilePage{})
)
type ProfilePage struct {
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
DetailsPartial `tmpl:"details"`
ClientsPartial `tmpl:"clients"`
FooterPartial `tmpl:"footer"`
type ProfilePageParams struct {
HeadParams
DetailsParams
AuthorRelays []string
Content string
CreatedAt string
Domain string
LastNotes []EnhancedEvent
Metadata sdk.ProfileMetadata
Metadata Metadata
NormalizedAuthorWebsiteURL string
RenderedAuthorAboutText template.HTML
Nevent string
@@ -285,34 +141,15 @@ type ProfilePage struct {
IsReply string
Proxy string
Title string
Clients []ClientReference
}
func (*ProfilePage) TemplateText() string { return tmplProfile }
var (
//go:embed templates/_last_notes.html
tmplLastNotes string
LastNotesTemplate = tmpl.MustCompile(&LastNotesPage{})
)
type LastNotesPage struct {
LastNotes []EnhancedEvent
}
func (*LastNotesPage) TemplateText() string { return tmplLastNotes }
var (
//go:embed templates/embedded_profile.html
tmplEmbeddedProfile string
EmbeddedProfileTemplate = tmpl.MustCompile(&EmbeddedProfilePage{})
)
type EmbeddedProfilePage struct {
type EmbeddedProfileParams struct {
AuthorRelays []string
Content string
CreatedAt string
Domain string
Metadata sdk.ProfileMetadata
Metadata Metadata
NormalizedAuthorWebsiteURL string
RenderedAuthorAboutText template.HTML
Nevent string
@@ -322,25 +159,14 @@ type EmbeddedProfilePage struct {
Title string
}
func (*EmbeddedProfilePage) TemplateText() string { return tmplEmbeddedProfile }
var (
//go:embed templates/file_metadata.html
tmplFileMetadata string
FileMetadataTemplate = tmpl.MustCompile(&FileMetadataPage{})
)
type FileMetadataPage struct {
OpenGraphPartial `tmpl:"opengraph"`
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
DetailsPartial `tmpl:"details"`
ClientsPartial `tmpl:"clients"`
FooterPartial `tmpl:"footer"`
type FileMetadataPageParams struct {
OpenGraphParams
HeadParams
DetailsParams
Content template.HTML
CreatedAt string
Metadata sdk.ProfileMetadata
Metadata Metadata
Npub string
NpubShort string
ParentLink template.HTML
@@ -353,27 +179,18 @@ type FileMetadataPage struct {
FileMetadata Kind1063Metadata
IsImage bool
IsVideo bool
Clients []ClientReference
}
func (*FileMetadataPage) TemplateText() string { return tmplFileMetadata }
var (
//go:embed templates/live_event.html
tmplLiveEvent string
LiveEventTemplate = tmpl.MustCompile(&LiveEventPage{})
)
type LiveEventPage struct {
OpenGraphPartial `tmpl:"opengraph"`
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
DetailsPartial `tmpl:"details"`
ClientsPartial `tmpl:"clients"`
FooterPartial `tmpl:"footer"`
type LiveEventPageParams struct {
OpenGraphParams
HeadParams
DetailsParams
Content template.HTML
CreatedAt string
Metadata sdk.ProfileMetadata
Metadata Metadata
Npub string
NpubShort string
ParentLink template.HTML
@@ -384,27 +201,18 @@ type LiveEventPage struct {
Alt string
LiveEvent Kind30311Metadata
Clients []ClientReference
}
func (*LiveEventPage) TemplateText() string { return tmplLiveEvent }
var (
//go:embed templates/live_event_message.html
tmplLiveEventMessage string
LiveEventMessageTemplate = tmpl.MustCompile(&LiveEventMessagePage{})
)
type LiveEventMessagePage struct {
OpenGraphPartial `tmpl:"opengraph"`
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
DetailsPartial `tmpl:"details"`
ClientsPartial `tmpl:"clients"`
FooterPartial `tmpl:"footer"`
type LiveEventMessagePageParams struct {
OpenGraphParams
HeadParams
DetailsParams
Content template.HTML
CreatedAt string
Metadata sdk.ProfileMetadata
Metadata Metadata
Npub string
NpubShort string
ParentLink template.HTML
@@ -415,114 +223,41 @@ type LiveEventMessagePage struct {
Alt string
LiveEventMessage Kind1311Metadata
Clients []ClientReference
}
func (*LiveEventMessagePage) TemplateText() string { return tmplLiveEventMessage }
var (
//go:embed templates/relay.html
tmplRelay string
RelayTemplate = tmpl.MustCompile(&RelayPage{})
)
type RelayPage struct {
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
ClientsPartial `tmpl:"clients"`
FooterPartial `tmpl:"footer"`
type RelayPageParams struct {
HeadParams
Info *nip11.RelayInformationDocument
Hostname string
Proxy string
LastNotes []EnhancedEvent
ModifiedAt string
Clients []ClientReference
}
func (*RelayPage) TemplateText() string { return tmplRelay }
var (
//go:embed templates/sitemap.xml
tmplSitemap string
SitemapTemplate = tmpl.MustCompile(&SitemapPage{})
)
type SitemapPage struct {
Host string
ModifiedAt string
// for the profile sitemap
Npub string
// for the relay sitemap
RelayHostname string
Info *nip11.RelayInformationDocument
// for the profile and relay sitemaps
LastNotes []EnhancedEvent
// for the archive sitemap
PathPrefix string
Data []string
type ErrorPageParams struct {
HeadParams
Message string
Errors string
}
func (*SitemapPage) TemplateText() string { return tmplSitemap }
var (
//go:embed templates/rss.xml
tmplRSS string
RSSTemplate = tmpl.MustCompile(&RSSPage{})
)
type RSSPage struct {
Host string
ModifiedAt string
Title string
// for the profile RSS
Npub string
Metadata sdk.ProfileMetadata
// for the relay RSS
RelayHostname string
Info *nip11.RelayInformationDocument
// for the profile and relay RSSs
LastNotes []EnhancedEvent
// for the archive RSS
PathPrefix string
Data []string
}
func (*RSSPage) TemplateText() string { return tmplRSS }
var (
//go:embed templates/error.html
tmplError string
ErrorTemplate = tmpl.MustCompile(&ErrorPage{})
)
type ErrorPage struct {
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
FooterPartial `tmpl:"footer"`
Message template.HTML
Errors string
}
func (e *ErrorPage) TemplateText() string {
func (e *ErrorPageParams) MessageHTML() string {
if e.Message != "" {
return tmplError
return "&lt;error omitted&gt;"
}
switch {
case strings.Contains(e.Errors, "invalid checksum"):
e.Message = "It looks like you entered an invalid event code.<br> Check if you copied it fully, a good idea is compare the first and the last characters."
return "It looks like you entered an invalid event code.<br> Check if you copied it fully, a good idea is compare the first and the last characters."
case strings.Contains(e.Errors, "couldn't find this"):
e.Message = "Can't find the event in the relays. Try getting an `nevent1` code with relay hints."
case strings.Contains(e.Errors, "invalid bech32 string length"), strings.Contains(e.Errors, "invalid separator"):
e.Message = "You have typed a wrong event code, we need a URL path that starts with /npub1, /nprofile1, /nevent1, /naddr1, or something like /name@domain.com (or maybe just /domain.com) or an event id as hex (like /aef8b32af...)"
return "Can't find the event in the relays. Try getting an `nevent1` code with relay hints."
case strings.Contains(e.Errors, "invalid bech32 string length"),
strings.Contains(e.Errors, "invalid separator"),
strings.Contains(e.Errors, "not part of charset"):
return "You have typed a wrong event code, we need a URL path that starts with /npub1, /nprofile1, /nevent1, /naddr1, or something like /name@domain.com (or maybe just /domain.com) or an event id as hex (like /aef8b32af...)"
default:
e.Message = "I can't give any suggestions to solve the problem.<br> Please tag <a href='/dtonon.com'>daniele</a> and <a href='/fiatjaf.com'>fiatjaf</a> and complain!"
return "I can't give any suggestions to solve the problem.<br> Please tag <a href='/dtonon.com'>daniele</a> and <a href='/fiatjaf.com'>fiatjaf</a> and complain!"
}
return tmplError
}

156
profile.templ Normal file
View File

@@ -0,0 +1,156 @@
package main
import "fmt"
templ profileTemplate(params ProfilePageParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<title>{ params.Metadata.Name } / { params.Metadata.DisplayName } is on Nostr</title>
<meta
name="description"
content={ fmt.Sprintf("%s is %s's public key on Nostr", params.Npub, params.Metadata.ShortName()) }
/>
<meta property="og:title" content={ params.Title }/>
<meta property="og:site_name" content={ params.Npub }/>
if params.Metadata.Picture != "" {
<meta property="og:image" content={ params.Metadata.Picture }/>
<meta property="twitter:image" content={ params.Proxy + params.Metadata.Picture }/>
}
if params.Metadata.About != "" {
<meta property="og:description" content={ params.Metadata.About }/>
}
<meta name="twitter:card" content="summary"/>
<link rel="canonical" href={ "https://njump.me/" + params.Npub }/>
<link
rel="sitemap"
type="application/xml"
title={ "Sitemap for " + params.Npub }
href={ "/" + params.Npub + ".xml" }
/>
<link
rel="alternate"
type="application/atom+xml"
title="RSS"
href={ "/" + params.Npub + ".rss" }
/>
@headCommonTemplate(params.HeadParams)
</head>
<body class="mb-16 bg-white text-gray-600 print:text-black dark:bg-neutral-900 dark:text-neutral-50">
@topTemplate()
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw">
<header
class="relative top-auto flex basis-1/4 items-center self-start sm:sticky sm:top-8 sm:mt-8 sm:block sm:items-start"
>
<div
class="hidden basis-[64%] items-center overflow-hidden text-left"
_="on load or scroll from window or resize from window get #profile_name then measure its top, height then if top is less than height / -2 or height is 0 remove .hidden otherwise add .hidden"
>
<div class="mb-3 sm:text-center">
<div class="text-2xl">{ params.Metadata.Name }</div>
if params.Metadata.Name != params.Metadata.DisplayName {
<div class="text-base text-stone-400">
{ params.Metadata.DisplayName }
</div>
}
</div>
</div>
<div
class="imgclip max-w-[40%] basis-2/5 overflow-hidden sm:max-w-full sm:basis-auto"
>
<img class="block h-auto w-full" src={ params.Metadata.Picture }/>
</div>
</header>
<div class="w-full break-words print:w-full sm:w-1/2">
<header class="mb-6 hidden leading-5 sm:flex sm:items-center">
<h1>
<div id="profile_name" class="text-2xl">{ params.Metadata.Name }</div>
if params.Metadata.Name != params.Metadata.DisplayName {
<div class="text-xl text-stone-400">
{ params.Metadata.DisplayName }
</div>
}
</h1>
</header>
<div class="-ml-4 mb-6 h-1.5 w-1/2 bg-zinc-100 sm:-ml-2.5 dark:bg-zinc-700"></div>
if params.Metadata.Website != "" {
<div class="mb-6 leading-5">
<a
class="border-b-2 border-b-gray-300 pb-0.5 hover:text-strongpink"
href={ templ.URL(params.NormalizedAuthorWebsiteURL) }
>{ params.Metadata.Website }</a>
</div>
}
if params.RenderedAuthorAboutText != "" {
<div
class="prose mb-6 leading-5 dark:prose-invert prose-headings:font-light sm:prose-a:text-justify"
dir="auto"
>
@templ.Raw(params.RenderedAuthorAboutText)
</div>
}
if params.Metadata.Website != "" || params.RenderedAuthorAboutText != "" {
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 sm:-ml-2.5 dark:bg-zinc-700"></div>
}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Public Key</div>
{ params.Npub }
</div>
<div class="mb-6 leading-5">
if params.Metadata.NIP05 != "" {
<div class="text-sm text-strongpink">NIP-05 Address</div>
{ params.Metadata.NIP05 }
}
</div>
<div class="mb-6 leading-5">
if params.Metadata.LUD16 != "" {
<div class="text-sm text-strongpink">NIP-57 Address</div>
{ params.Metadata.LUD16 }
}
</div>
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Profile Code</div>
{ params.Nprofile }
</div>
if len(params.AuthorRelays) != 0 {
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Publishing to</div>
for _, relay := range params.AuthorRelays {
<a
href={ templ.URL("/r/" + relay) }
class="mr-1 mt-2 inline-block max-w-full rounded-lg border border-slate-300 px-2 py-0.5 hover:border hover:border-solid hover:border-strongpink hover:bg-strongpink hover:text-white"
>
{ relay }
</a>
}
</div>
}
@detailsTemplate(params.DetailsParams)
<div
_={ "init fetch /profile-last-notes/" + params.Npub + " then put the result into me end" }
>
if len(params.LastNotes) != 0 {
<aside>
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 sm:-ml-2.5 dark:bg-zinc-700"></div>
<nav class="mb-6 leading-5">
<h2 class="text-2xl text-strongpink">Last Notes</h2>
for _, ee := range params.LastNotes {
<a class="my-8 block no-underline hover:-ml-6 hover:border-l-05rem hover:border-solid hover:border-l-gray-100 hover:pl-4 dark:hover:border-l-zinc-700" href={ templ.URL("/" + ee.Nevent()) }>
{ ee.Nevent() }
</a>
}
</nav>
</aside>
}
</div>
<div class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 sm:-ml-2.5 dark:bg-zinc-700"></div>
</div>
@clientsTemplate(params.Clients)
</div>
</div>
@footerTemplate()
</body>
</html>
}

145
relay.templ Normal file
View File

@@ -0,0 +1,145 @@
package main
templ relayTemplate(params RelayPageParams) {
<!DOCTYPE html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8"/>
<head>
<title>Nostr Relay { params.Hostname } - { params.Info.Name }</title>
<meta property="og:title" content={ params.Hostname + " - Nostr Relay" }/>
<meta name="twitter:title" content={ params.Hostname + " - Nostr Relay" }/>
<meta property="og:site_name" content={ params.Hostname + " - Nostr Relay" }/>
if params.Info.Icon != "" {
<meta property="og:image" content={ params.Info.Icon }/>
<meta name="twitter:image" content={ params.Proxy + params.Info.Icon }/>
}
if params.Info.Description != "" {
<meta property="og:description" content={ params.Info.Description }/>
<meta name="twitter:description" content={ params.Info.Description }/>
}
<meta name="twitter:card" content="summary"/>
<link
rel="sitemap"
type="application/xml"
title={ "Sitemap for " + params.Hostname }
href={ "/r/" + params.Hostname + ".xml" }
/>
<link
rel="alternate"
type="application/atom+xml"
title="RSS"
href={ "/r/" + params.Hostname + ".rss" }
/>
@headCommonTemplate(params.HeadParams)
)
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
@topTemplate()
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div
class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 print:w-full sm:flex md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<div
class="relative top-auto flex basis-1/4 items-center self-start sm:sticky sm:top-8 sm:mt-8 sm:items-start"
>
<div
class="hidden basis-2/3 items-center overflow-hidden text-left text-2xl sm:break-all"
_="on load or scroll from window or resize from window get #relay_name then measure its top, height then if top is less than height / -2 or height is 0 add .flex then remove .hidden otherwise remove .flex then add .hidden"
>
{ params.Info.Name }
</div>
<div
class="imgclip max-w-full basis-2/5 overflow-hidden sm:basis-auto"
>
<img class="block h-auto w-full" src={ params.Info.Icon }/>
</div>
</div>
<div class="w-full break-words print:w-full sm:w-1/2">
<header class="mb-6 hidden leading-5 sm:flex sm:items-center">
<h1>
<div id="relay_name" class="text-2xl">{ params.Info.Name }</div>
</h1>
</header>
<div
class="-ml-4 mb-6 h-1.5 w-1/2 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<div class="mb-6 leading-5">
<a
class="border-b-2 border-b-gray-300 pb-0.5 hover:text-strongpink"
href={ templ.URL("https://" + params.Hostname) }
target="_blank"
_="on mouseenter set my innerText to my.innerText.replace('wss://', 'https://')
on mouseleave set my innerText to my.innerText.replace('https://', 'wss://')"
>{ "wss://" + params.Hostname }</a>
</div>
<div
class="prose mb-6 leading-5 dark:prose-invert prose-headings:font-light sm:prose-a:text-justify"
dir="auto"
>
{ params.Info.Description }
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/2 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
if params.Info.PubKey != "" {
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Public Key</div>
{ params.Info.PubKey }
</div>
}
<!---->
if params.Info.Contact != "" {
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Contact</div>
<a href={ templ.URL(params.Info.Contact) }>{ params.Info.Contact }</a>
</div>
}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<aside>
<div class="mb-6 leading-5">
<h2 class="text-2xl text-strongpink">Last Notes</h2>
for _, ee := range params.LastNotes {
<a
class="my-8 block no-underline hover:-ml-6 hover:border-l-05rem hover:border-solid hover:border-l-gray-100 hover:pl-4 dark:hover:border-l-zinc-700"
href={ templ.URL("/" + ee.Nevent()) }
>
<div
class="-ml-2.5 mb-1.5 flex flex-row border-b-4 border-solid border-b-gray-100 pb-1 pl-2.5 dark:border-b-neutral-800"
>
<div class="text-sm text-strongpink">
{ ee.CreatedAtStr() }
</div>
<br/>
if ee.IsReply() {
<div class="ml-2 text-xs text-gray-300 dark:text-gray-400">
- reply
</div>
}
<div class="ml-auto text-xs text-zinc-700 dark:text-neutral-50">
by
<a class="rounded bg-lavender px-1 hover:bg-strongpink hover:text-white dark:bg-garnet dark:hover:bg-strongpink" href={ templ.URL("/" + ee.Npub()) }>{ ee.NpubShort() }</a>
</div>
</div>
<div
class="mt-0.5 max-h-40 basis-full overflow-hidden hover:text-strongpink"
_="on load if my scrollHeight > my offsetHeight add .gradient"
dir="auto"
>
@templ.Raw(ee.Preview())
</div>
</a>
}
</div>
</aside>
</div>
@clientsTemplate(params.Clients)
</div>
</div>
@footerTemplate()
</body>
</html>
}

View File

@@ -1,5 +1,5 @@
module.exports = {
content: ['./templates/*.html', './*.go'],
content: ['./*.go', './*.templ'],
darkMode: ['class', '.theme--dark'],
theme: {
extend: {

View File

@@ -0,0 +1,51 @@
package main
templ telegramInstantViewTemplate(params TelegramInstantViewParams) {
<meta charset="UTF-8"/>
<!-- check https://nikstar.me/post/instant-view/ for more information on how this was set up -->
<!-- required stuff so telegram treats us like a medium.com article -->
<meta property="al:android:app_name" content="Medium"/>
<meta property="article:published_time" content={ params.CreatedAt }/>
<!-- stuff that goes in the actual telegram message preview -->
<meta property="og:site_name" content={ params.AuthorLong }/>
if params.Description != "" {
<meta property="og:description" content={ params.Description }/>
}
<!---->
if params.Image != "" {
<meta property="og:image" content={ params.Image }/>
}
<!---->
if params.Video != "" {
<meta property="og:video" content={ params.Video }/>
<meta property="og:video:secure_url" content={ params.Video }/>
<meta property="og:video:type" content={ "video/" + params.VideoType }/>
}
<!-- stuff that affects the content inside the preview window -->
<meta name="author" content={ params.Metadata.ShortName }/>
<meta name="telegram:channel" content="@nostr_protocol"/>
<!-- basic content of the preview window -->
<article>
<h1>
if params.Subject != "" {
{ params.Subject }
} else {
<a href={ "/" + params.Metadata.Npub }>{ params.Metadata.ShortName }</a> on Nostr:
}
</h1>
if params.ParentLink != "" {
<aside>in reply to { params.ParentLink }</aside>
}
<!---->
if params.Summary != "" {
<aside>{ params.Summary }</aside>
}
<!---->
{ params.Content }
if params.Subject != "" {
<aside>
<a href={ "/" + params.Metadata.Npub }>{ params.Metadata.ShortName }</a>
</aside>
}
</article>
}

174
template_types.go Normal file
View File

@@ -0,0 +1,174 @@
package main
import (
"context"
"html"
"html/template"
"regexp"
"strings"
"time"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip10"
"github.com/nbd-wtf/go-nostr/nip19"
sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/texttheater/golang-levenshtein/levenshtein"
)
type Metadata struct {
sdk.ProfileMetadata
}
func (m Metadata) Npub() string {
npub, _ := nip19.EncodePublicKey(m.PubKey)
return npub
}
func (m Metadata) NpubShort() string {
npub := m.Npub()
return npub[:8] + "…" + npub[len(npub)-4:]
}
type EnhancedEvent struct {
event *nostr.Event
relays []string
}
func (ee EnhancedEvent) IsReply() bool {
return nip10.GetImmediateReply(ee.event.Tags) != nil
}
func (ee EnhancedEvent) Reply() *nostr.Tag {
return nip10.GetImmediateReply(ee.event.Tags)
}
func (ee EnhancedEvent) Preview() template.HTML {
lines := strings.Split(html.EscapeString(ee.event.Content), "\n")
var processedLines []string
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
processedLine := shortenNostrURLs(line)
processedLines = append(processedLines, processedLine)
}
return template.HTML(strings.Join(processedLines, "<br/>"))
}
func (ee EnhancedEvent) RssTitle() string {
regex := regexp.MustCompile(`(?i)<br\s?/?>`)
replacedString := regex.ReplaceAllString(string(ee.Preview()), " ")
words := strings.Fields(replacedString)
title := ""
for i, word := range words {
if len(title)+len(word)+1 <= 65 { // +1 for space
if title != "" {
title += " "
}
title += word
} else {
if i > 1 { // the first word len is > 65
title = title + " ..."
} else {
title = ""
}
break
}
}
content := ee.RssContent()
distance := levenshtein.DistanceForStrings([]rune(title), []rune(content), levenshtein.DefaultOptions)
similarityThreshold := 5
if distance <= similarityThreshold {
return ""
} else {
return title
}
}
func (ee EnhancedEvent) RssContent() string {
content := ee.event.Content
content = basicFormatting(html.EscapeString(content), true, false, false)
content = renderQuotesAsHTML(context.Background(), content, false)
if ee.IsReply() {
nevent, _ := nip19.EncodeEvent(ee.Reply().Value(), ee.relays, ee.event.PubKey)
neventShort := nevent[:8] + "…" + nevent[len(nevent)-4:]
content = "In reply to <a href='/" + nevent + "'>" + neventShort + "</a><br/>_________________________<br/><br/>" + content
}
return content
}
func (ee EnhancedEvent) Thumb() string {
imgRegex := regexp.MustCompile(`(https?://[^\s]+\.(?:png|jpe?g|gif|bmp|svg)(?:/[^\s]*)?)`)
matches := imgRegex.FindAllStringSubmatch(ee.event.Content, -1)
if len(matches) > 0 {
// The first match group captures the image URL
return matches[0][1]
}
return ""
}
func (ee EnhancedEvent) Npub() string {
npub, _ := nip19.EncodePublicKey(ee.event.PubKey)
return npub
}
func (ee EnhancedEvent) NpubShort() string {
npub := ee.Npub()
return npub[:8] + "…" + npub[len(npub)-4:]
}
func (ee EnhancedEvent) Nevent() string {
nevent, _ := nip19.EncodeEvent(ee.event.ID, ee.relays, ee.event.PubKey)
return nevent
}
func (ee EnhancedEvent) CreatedAtStr() string {
return time.Unix(int64(ee.event.CreatedAt), 0).Format("2006-01-02 15:04:05")
}
func (ee EnhancedEvent) ModifiedAtStr() string {
return time.Unix(int64(ee.event.CreatedAt), 0).Format("2006-01-02T15:04:05Z07:00")
}
type Kind1063Metadata struct {
Magnet string
Dim string
Size string
Summary string
Image string
URL string
AES256GCM string
M string
X string
I string
Blurhash string
Thumb string
}
type Kind30311Metadata struct {
Title string
Summary string
Image string
Status string
Host sdk.ProfileMetadata
HostNpub string
Tags []string
}
type Kind1311Metadata struct {
// ...
}
func (fm Kind1063Metadata) IsVideo() bool { return strings.Split(fm.M, "/")[0] == "video" }
func (fm Kind1063Metadata) IsImage() bool { return strings.Split(fm.M, "/")[0] == "image" }
func (fm Kind1063Metadata) DisplayImage() string {
if fm.Image != "" {
return fm.Image
} else if fm.IsImage() {
return fm.URL
} else {
return ""
}
}

View File

@@ -1,32 +0,0 @@
{{if not (eq 0 (len .LastNotes))}}
<aside>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<nav class="mb-6 leading-5">
<h2 class="text-2xl text-strongpink">Last Notes</h2>
{{range $i, $ee := .LastNotes}}
<a
class="my-8 block no-underline hover:-ml-6 hover:border-l-05rem hover:border-solid hover:border-l-gray-100 hover:pl-4 dark:hover:border-l-zinc-700"
href="/{{$ee.Nevent}}"
>
<div
class="-ml-2.5 mb-1.5 flex flex-row flex-wrap border-b-4 border-solid border-b-gray-100 pb-1 pl-2.5 dark:border-b-neutral-800"
>
<div class="text-sm text-strongpink">{{$ee.CreatedAtStr}}</div>
{{if $ee.IsReply}}
<div class="ml-2 text-sm text-gray-300 dark:text-gray-400">- reply</div>
{{end}}
</div>
<div
class="mt-0.5 max-h-40 basis-full overflow-hidden hover:text-strongpink"
_="on load if my scrollHeight > my offsetHeight add .gradient"
dir="auto"
>
{{$ee.Preview}}
</div>
</a>
{{end}}
</nav>
</aside>
{{end}}

View File

@@ -1,53 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>{{.Title}}</title>
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div
class="mx-auto block px-4 sm:flex sm:items-center sm:justify-center sm:px-0"
>
<div
class="flex w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 print:w-full sm:w-11/12 md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<div
class="relative top-auto flex basis-1/4 items-center self-start sm:sticky sm:top-8 sm:mt-8 sm:items-start"
>
<div class="text-2xl">{{.Title}}</div>
</div>
<div class="w-full break-words break-all print:w-full sm:w-1/2">
<div class="mb-6 leading-5">
<h1 class="text-xl">{{.Title}}</h1>
</div>
<div class="mb-6 leading-5">
{{range $element := .Data }}
<a class="block" href="/{{$.PathPrefix}}{{$element}}">
{{$element}}
</a>
{{end}}
</div>
<div class="flex justify-between">
{{if not (eq .PrevPage 0)}}
<a href="/{{.PaginationUrl}}/{{.PrevPage}}">&lt;&lt; Prev page</a>
{{end}} {{if not (eq .NextPage 0)}}
<a href="/{{.PaginationUrl}}/{{.NextPage}}">Next page &gt;&gt;</a>
</div>
{{end}}
</div>
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -1,155 +0,0 @@
<aside
class="fixed bottom-0 left-0 top-auto mt-4 w-full basis-3/12 self-start transition-all duration-500 print:hidden sm:sticky sm:bottom-auto sm:left-auto sm:top-8 sm:w-auto"
>
<div
class="absolute right-0 top-0 z-10 mb-4 h-10 w-10 text-center text-sm sm:relative sm:h-auto sm:w-auto"
>
<span class="hidden sm:block">Open in</span>
<div
_="on click
toggle .hidden on #open
toggle .hidden on #close
toggle .hidden on #gradient
toggle .overflow-hidden on <body />
toggle .hidden on <.client:not(:first-child) />
toggle .top_client_sticky on <.clients_wrapper > :first-child />
"
>
<div id="open" class="inline sm:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
viewBox="0 0 20 20"
class="m-auto mt-[28%] block h-1/2 w-1/2"
>
<path
fill="#fafafa"
fill-rule="evenodd"
d="M3.808.355h2.85a2.85 2.85 0 0 1 2.85 2.85v2.85a2.85 2.85 0 0 1-2.85 2.85h-2.85a2.85 2.85 0 0 1-2.85-2.85v-2.85a2.85 2.85 0 0 1 2.85-2.85Zm2.85 6.65a.95.95 0 0 0 .95-.95v-2.85a.95.95 0 0 0-.95-.95h-2.85a.95.95 0 0 0-.95.95v2.85c0 .525.425.95.95.95h2.85Zm0 3.8h-2.85a2.85 2.85 0 0 0-2.85 2.85v2.85a2.85 2.85 0 0 0 2.85 2.85h2.85a2.85 2.85 0 0 0 2.85-2.85v-2.85a2.85 2.85 0 0 0-2.85-2.85Zm0 6.65a.95.95 0 0 0 .95-.95v-2.85a.95.95 0 0 0-.95-.95h-2.85a.95.95 0 0 0-.95.95v2.85c0 .525.425.95.95.95h2.85Zm10.45-6.65h-2.85a2.85 2.85 0 0 0-2.85 2.85v2.85a2.85 2.85 0 0 0 2.85 2.85h2.85a2.85 2.85 0 0 0 2.85-2.85v-2.85a2.85 2.85 0 0 0-2.85-2.85Zm0 6.65a.95.95 0 0 0 .95-.95v-2.85a.95.95 0 0 0-.95-.95h-2.85a.95.95 0 0 0-.95.95v2.85c0 .525.425.95.95.95h2.85Zm0-17.1h-2.85a2.85 2.85 0 0 0-2.85 2.85v2.85a2.85 2.85 0 0 0 2.85 2.85h2.85a2.85 2.85 0 0 0 2.85-2.85v-2.85a2.85 2.85 0 0 0-2.85-2.85Zm0 6.65a.95.95 0 0 0 .95-.95v-2.85a.95.95 0 0 0-.95-.95h-2.85a.95.95 0 0 0-.95.95v2.85c0 .525.425.95.95.95h2.85Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div id="close" class="mt-4 hidden sm:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
width="31"
height="16"
fill="currentColor"
viewBox="0 0 31 16"
class="m-auto mt-[28%] block h-1/2 w-1/2"
>
<path
fill="#fafafa"
d="M30.207 3.016 16.744 14.983a1.496 1.496 0 0 1-1.974 0L1.307 3.016A1.496 1.496 0 0 1 3.28.772l12.476 11.085L28.233.772a1.496 1.496 0 1 1 1.974 2.244Z"
/>
</svg>
</div>
</div>
</div>
<div
class="clients_wrapper | overflow-y-auto rounded-t-lg max-h-[55vh] sm:max-h-max"
_="on load
wait 50ms
get my children
get filterAndSort(it) then repeat for c in it call me.appendChild(c) end
get first in me
then tell it
remove .hidden
add .top_client
"
>
{{range $index, $client := .Clients}}
<div
data-platform="{{$client.Platform}}"
class="client | hidden w-full items-center border-b border-zinc-800 bg-zinc-700 first-of-type:rounded-t-lg first-of-type:border-0 first-of-type:bg-strongpink hover:bg-zinc-800 sm:mb-3 sm:flex sm:rounded-lg sm:border-0 sm:first-of-type:rounded-lg"
_="on load get localStorage['nj:{{.ID}}'] or 0 then set @count to it then set @title to `used ${it} times`
on click increment localStorage['nj:{{$client.ID}}']"
>
<a
class="client block basis-full px-3 py-3 text-left text-[17px] font-normal leading-4 text-white no-underline sm:inline sm:py-1.5 sm:text-center sm:font-light"
href="{{$client.URL}}"
>
<span
class="ml-1.5 pr-2 inline basis-1/5 text-neutral-400 sm:hidden"
>Open in</span
>
{{$client.Name}}
<span
class="type | float-right mr-4 text-xs uppercase text-neutral-400 sm:hidden"
>{{$client.Platform}}</span
>
</a>
</div>
{{end}}
<div data-platform="dummy" class="client | hidden h-8 bg-zinc-700" ></div>
</div>
<div
id="gradient"
class="fixed bottom-0 hidden h-10 w-full bg-gradient-to-t from-zinc-800 to-transparent pointer-events-auto"
></div>
</aside>
<style>
.top_client_sticky {
position: absolute;
}
.top_client span {
color: #FFFFFF;
}
.top_client a span.type {
display: none;
}
.clients_wrapper div {
&:nth-child(2) {
margin-top: 2.5rem;
}
}
</style>
<script>
function getPlatform() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
if (/android/i.test(userAgent)) {
return 'android';
} else if (/iPad|iPhone|iPod/i.test(userAgent) && !window.MSStream) {
return 'ios';
} else {
return 'web';
}
}
function filterAndSort(children) {
const platform = getPlatform();
const filteredElements = Array.from(children).filter((element) => {
const platformAttr = element.getAttribute('data-platform');
return platform === platformAttr || platformAttr === 'web' || platformAttr === 'native' || platformAttr === 'dummy';
});
Array.from(children).forEach((element) => {
if (!filteredElements.includes(element)) {
element.remove();
}
});
const sortedElements = filteredElements.sort(
(a, b) =>
parseInt(b.getAttribute('count')) - parseInt(a.getAttribute('count'))
);
// Assuming you want to re-insert the sorted elements into their parent container
const parent = children[0].parentNode;
parent.innerHTML = ''; // Clear the parent container
sortedElements.forEach((element) => {
parent.appendChild(element);
});
return sortedElements;
}
</script>

View File

@@ -1,151 +0,0 @@
<!-- always visible details -->
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
{{ if not (eq "" .Npub) }}
<div class="mb-6 break-all leading-5">
<div class="text-sm text-strongpink">Author Public Key</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{{.Npub}}</span>
</div>
{{ end }}
<!------>
{{ if not (eq nil .FileMetadata) }}
<!---->
{{ if not (eq "" .FileMetadata.Summary) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Summary</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]"
>{{.FileMetadata.Summary}}</span
>
</div>
{{ end }}
<!---->
{{ if not (eq "" .FileMetadata.Dim) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Dimension</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]"
>{{.FileMetadata.Dim}}</span
>
</div>
{{ end }}
<!---->
{{ if not (eq "" .FileMetadata.Size) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Size</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]"
>{{.FileMetadata.Size}} bytes</span
>
</div>
{{ end }}
<!---->
{{ if not (eq "" .FileMetadata.Magnet) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Magnet URL</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]"
>{{.FileMetadata.Magnet}}</span
>
</div>
{{ end }}
<!---->
{{ end }}
<!------>
{{ if not (eq 0 (len .SeenOn)) }}
<div class="mb-6 leading-5 text-neutral-500 dark:text-neutral-300 text-[16px]">
<div class="text-sm text-strongpink">Seen on</div>
{{ range .SeenOn }}<a
href="/r/{{.}}"
class="underline-none pr-2 decoration-neutral-200 decoration-1 underline-offset-[6px] hover:underline"
>{{.}}</a
>
{{ end }}
</div>
{{ end }}
<!-- details hidden behind a toggle -->
{{ if .HideDetails }}
<div class="mb-6 flex items-center print:hidden">
<input
type="checkbox"
id="advanced-switch"
class="hidden"
_="on load make a URLSearchParams from location.search then get it.get('details') then if it is 'yes' set my.checked to true then trigger switch on me end
on change or switch log my checked then if my checked is true
remove .hidden from #hidden-fields
tell the next <label /> from me
add .bg-strongpink .after:translate-x-full
remove .bg-gray-300 .dark:bg-zinc-800
end
otherwise
add .hidden to #hidden-fields
tell the next <label /> from me
remove .bg-strongpink .after:translate-x-full
add .bg-gray-300 .dark:bg-zinc-800
end
end
"
/>
<label
for="advanced-switch"
class="after:content-[&quot;&quot;] relative mr-2 inline-block h-5 w-9 cursor-pointer rounded-full bg-gray-300 -indent-96 after:absolute after:inset-0.5 after:h-4 after:w-4 after:rounded-2xl after:bg-zinc-50 after:transition dark:bg-zinc-800 dark:after:bg-gray-700"
>&nbsp;</label
>
<label
for="advanced-switch"
class="cursor-pointer text-sm leading-4 underline text-neutral-500 dark:text-neutral-300 text-[16px] decoration-neutral-200 dark:decoration-neutral-500 decoration-1 underline-offset-[6px]"
>Show more details</label
>
</div>
{{ end }}
<div id="hidden-fields" class="{{if .HideDetails}}hidden{{end}}">
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Published at</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{{.CreatedAt}}</span>
</div>
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Kind type</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{{.Kind}}</span>
{{ if not (eq .KindNIP "")}} -
<a
href="https://github.com/nostr-protocol/nips/blob/master/{{.KindNIP}}.md"
class="text-neutral-500 underline decoration-neutral-200 dark:decoration-neutral-500 decoration-1 underline-offset-[6px] text-neutral-500 dark:text-neutral-300 text-[16px]"
>{{.KindDescription}}</a
>
{{ end }}
</div>
{{ if not (eq "" .Nevent) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Address Code</div>
<span class="text-[16px] text-neutral-500 dark:text-neutral-300"
>{{.Nevent}}</span
>
</div>
{{ end }}
<!---->
<div class="-mx-4 my-8 bg-neutral-100 px-4 pb-4 leading-5 dark:bg-neutral-700">
<div
class="-mx-4 bg-neutral-300 px-4 py-1 text-neutral-100 dark:bg-neutral-800 dark:text-neutral-400"
>
Event JSON
</div>
<div class="mt-4 whitespace-pre-wrap break-all font-mono text-sm">
{{- .EventJSON}}
</div>
</div>
{{ if not (eq "" .Nprofile) }}
<div class="mb-6 break-all leading-5">
<div class="text-sm text-strongpink">Author Profile Code</div>
<span class="text-neutral-500 dark:text-neutral-300 text-[16px]">{{.Nprofile}}</span>
</div>
{{ end }}
</div>

View File

@@ -1,116 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
type="text/css"
href="/njump/static/tailwind-bundle.min.css"
/>
</head>
<body
class="relative bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black sm:items-center sm:justify-center"
>
<style>
::-webkit-scrollbar {
display: none;
}
</style>
<div
class="mx-auto w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 pb-4 pt-4 print:w-full sm:w-11/12 md:w-10/12 lg:w-9/12"
>
<a href="/{{ .Url }}" target="_new" class="no-underline">
<div class="w-full break-words">
<header class="mb-4 max-w-full">
<div class="flex flex-wrap items-center">
<div
class="print:basis-1-12 imgclip mr-2 max-w-full basis-[12%] overflow-hidden sm:mr-4 sm:basis-[8%]"
>
<img class="block h-auto w-full" src="{{.Metadata.Picture}}" />
</div>
<div class="block print:text-base sm:grow">
<div class="leading-4">
{{.Metadata.Name}}
<!---->
{{if not (eq .Metadata.Name .Metadata.DisplayName)}}
<span class="text-stone-400"
>/ {{.Metadata.DisplayName}}</span
>
{{end}}
</div>
<div class="text-sm leading-4 text-stone-400 sm:text-base">
{{.NpubShort}}
</div>
</div>
</div>
</header>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<article
class="prose-cite:text-sm prose mb-6 leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:mb-2 prose-blockquote:mt-2 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-neutral-200 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-blockquote:pt-0 prose-blockquote:font-light prose-blockquote:not-italic prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-neutral-700 sm:prose-a:text-justify"
>
{{ if (not (eq "" .Subject))}}
<h1 class="text-2xl">{{.Subject}}</h1>
{{ end }}
<!-- main content -->
<div dir="auto">{{ .Content }}</div>
<div class="mt-2 w-full text-right text-sm text-stone-400">
{{.CreatedAt}}
</div>
</article>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
</div>
</a>
<div class="text-sm leading-3 text-neutral-400">
This note has been published on Nostr and is embedded via Njump,
<a href="/" target="_new" class="underline">learn more</a>
</div>
</div>
<svg width="0" height="0" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="svg-shape" clipPathUnits="objectBoundingBox">
<path
transform="scale(0.005, 0.005)"
d="M100,200c43.8,0,68.2,0,84.1-15.9C200,168.2,200,143.8,200,100s0-68.2-15.9-84.1C168.2,0,143.8,0,100,0S31.8,0,15.9,15.9C0,31.8,0,56.2,0,100s0,68.2,15.9,84.1C31.8,200,56.2,200,100,200z"
/>
</clipPath>
</defs>
</svg>
<script>
window.addEventListener('load', function () {
var contentHeight = document.body.scrollHeight
window.parent.postMessage({height: contentHeight}, '*')
})
window.addEventListener('message', function (event) {
if (event.data.showGradient) {
var gradient = document.getElementById('bottom-gradient')
gradient.classList.remove('hidden')
}
if (event.data.setDarkMode) {
document.querySelector('html').classList.add('theme--dark')
}
})
</script>
<div
id="bottom-gradient"
class="pointer-events-none sticky bottom-0 left-0 hidden h-20 w-full bg-gradient-to-b from-transparent to-white dark:to-neutral-900"
></div>
<a href="/" target="_new" class="fixed bottom-2 right-2 w-[100px]"
><img src="/njump/static/logo.png"
/></a>
</body>
</html>

View File

@@ -1,144 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
type="text/css"
href="/njump/static/tailwind-bundle.min.css"
/>
</head>
<body
class="relative bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
<style>
::-webkit-scrollbar {
display: none;
}
</style>
<div
class="mx-auto w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 pb-4 pt-4 print:w-full sm:w-11/12 md:w-10/12 lg:w-9/12"
>
<a href="/{{.Npub}}" target="_new" class="no-underline">
<div class="w-full break-words">
<div class="w-full break-words print:w-full">
<header class="mb-4 max-w-full">
<div class="flex flex-wrap items-center">
<div
class="print:basis-1-12 imgclip mr-2 max-w-full basis-1/6 overflow-hidden sm:mr-4 sm:basis-[10%]"
>
<img
class="block h-auto w-full"
src="{{.Metadata.Picture}}"
/>
</div>
<div class="block print:text-base">
<div class="text-2xl">{{.Metadata.Name}}</div>
{{ if not (eq .Metadata.Name .Metadata.DisplayName) }}
<div class="leading-4 text-stone-400">
{{.Metadata.DisplayName}}
</div>
{{ end }}
</div>
</div>
</header>
<div dir="auto">
{{ if or (not (eq "" .Metadata.Website)) (not (eq ""
.RenderedAuthorAboutText)) }}
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/2 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<div class="mb-6 leading-5">{{.Metadata.Website}}</div>
<div
class="prose mb-6 leading-5 dark:prose-invert prose-headings:font-light sm:prose-a:text-justify"
>
{{.RenderedAuthorAboutText}}
</div>
{{ end }}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Public Key</div>
{{.Npub}}
</div>
<div class="mb-6 leading-5">
{{ if not (eq "" .Metadata.NIP05) }}
<div class="text-sm text-strongpink">NIP-05 Address</div>
{{.Metadata.NIP05}} {{ end }}
</div>
<div class="mb-6 leading-5">
{{ if not (eq "" .Metadata.LUD16) }}
<div class="text-sm text-strongpink">NIP-57 Address</div>
{{.Metadata.LUD16}} {{ end }}
</div>
<!-- <div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Profile Code</div>
{{.Nprofile}}
</div> -->
{{ if not (eq 0 (len .AuthorRelays)) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Publishing to</div>
{{range $index, $element := .AuthorRelays}}
<span
class="mr-1 mt-2 inline-block max-w-full rounded-lg border border-slate-300 px-2 py-0.5"
>{{$element}}</span
>
{{end}}
</div>
{{ end }}
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
</div>
</a>
<div class="text-sm leading-3 text-neutral-400">
This note has been published on Nostr and is embedded via Njump,
<a href="/" target="_new" class="underline">learn more</a>
</div>
</div>
<svg width="0" height="0" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="svg-shape" clipPathUnits="objectBoundingBox">
<path
transform="scale(0.005, 0.005)"
d="M100,200c43.8,0,68.2,0,84.1-15.9C200,168.2,200,143.8,200,100s0-68.2-15.9-84.1C168.2,0,143.8,0,100,0S31.8,0,15.9,15.9C0,31.8,0,56.2,0,100s0,68.2,15.9,84.1C31.8,200,56.2,200,100,200z"
/>
</clipPath>
</defs>
</svg>
<script>
window.addEventListener('load', function () {
var contentHeight = document.body.scrollHeight
window.parent.postMessage({height: contentHeight}, '*')
})
window.addEventListener('message', function (event) {
if (event.data.showGradient) {
var gradient = document.getElementById('bottom-gradient')
gradient.classList.remove('hidden')
}
if (event.data.setDarkMode) {
document.querySelector('html').classList.add('theme--dark')
}
})
</script>
<div
id="bottom-gradient"
class="pointer-events-none sticky bottom-0 left-0 hidden h-20 w-full bg-gradient-to-b from-transparent to-white dark:to-neutral-900"
></div>
<a href="/" target="_new" class="fixed bottom-2 right-2 w-[100px]"
><img src="/njump/static/logo.png"
/></a>
</body>
</html>

View File

@@ -1,33 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>Error</title>
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div class="mx-auto mt-12 w-10/12 text-center lg:w-9/12">
<div class="mx-auto w-4/5 sm:w-3/5">
<div class="mt-4 text-2xl leading-6">{{.Message}}</div>
<div class="my-8 italic text-neutral-400 dark:text-neutral-500">
{{.Errors}}
</div>
<div>
Are you lost?
<a
href="/"
class="block leading-3 underline decoration-neutral-400 underline-offset-4"
>Go to the homepage</a
>
</div>
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -1,104 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>File Metadata</title>
{{template "opengraph" .OpenGraphPartial}}
<!---->
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div
class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<div class="w-full break-words print:w-full sm:w-3/4">
<header class="mb-4 max-w-full">
<a class="flex flex-wrap items-center" href="/{{.Npub}}">
<div
class="print:basis-1-12 imgclip mr-2 max-w-full basis-1/6 overflow-hidden sm:mr-4"
>
<img class="block h-auto w-full" src="{{.Metadata.Picture}}" />
</div>
<div class="block print:text-base sm:grow">
<div class="leading-4 sm:text-2xl">
{{.Metadata.Name}}
<!---->
{{if not (eq .Metadata.Name .Metadata.DisplayName)}}
<span class="text-stone-400 sm:text-xl"
>/ {{.Metadata.DisplayName}}</span
>
{{end}}
</div>
<div class="text-sm leading-4 text-stone-400 sm:text-base">
{{.NpubShort}}
</div>
</div>
</a>
</header>
<div class="w-full text-right text-sm text-stone-400">
{{.CreatedAt}}
</div>
<div class="w-full text-right text-sm text-stone-400">
{{ if not (eq "" .ParentLink) }} in reply to
<span class="text-strongpink">{{ .ParentLink }}</span> {{ end }}
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<article
class="prose-cite:text-sm prose prose mb-6 leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-gray-100 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-zinc-800 sm:prose-a:text-justify"
>
{{ if (not (eq "" .Subject))}}
<h1 class="text-2xl">{{.Subject}}</h1>
{{ else }}
<h1 class="hidden">
{{.Metadata.ShortName}} on Nostr: {{.TitleizedContent}}
</h1>
{{ end }}
<!-- main content -->
{{ if (not (eq "" .FileMetadata.Image))}}
<img src="{{ .FileMetadata.Image }}" alt="{{ .Alt }}" />
{{ else if .IsImage }}
<img src="{{ .FileMetadata.URL }}" alt="{{ .Alt }}" />
{{ else if .IsVideo }}
<video
controls
width="100%%"
class="max-h-[90vh] bg-neutral-300 dark:bg-zinc-700"
>
<source src="{{ .FileMetadata.URL }}" alt="{{ .Alt }}" />
</video>
{{ end }}
<a
href="{{ .FileMetadata.URL }}"
target="_new"
class="not-prose mx-auto mb-3 block w-4/5 basis-full rounded-lg border-0 bg-strongpink px-4 py-2 text-center text-[17px] font-light text-white no-underline sm:w-2/6"
>Download file</a
>
</article>
{{template "details" .DetailsPartial}}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
</div>
{{template "clients" .ClientsPartial}}
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -1,24 +0,0 @@
<div
class="fixed -inset-x-24 inset-y-64 -z-10 h-full w-[200%] -rotate-12 overflow-hidden bg-gray-50 dark:bg-neutral-950 print:hidden"
></div>
<footer class="mb-4 mt-6 text-center text-sm text-gray-400">
powered by
<a class="text-gray-400 underline" href="/">njump</a> & open-sourced on
<a class="text-gray-400 underline" href="https://github.com/fiatjaf/njump"
>github</a
>
</footer>
<svg width="0" height="0" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="svg-shape" clipPathUnits="objectBoundingBox">
<path
transform="scale(0.005, 0.005)"
d="M100,200c43.8,0,68.2,0,84.1-15.9C200,168.2,200,143.8,200,100s0-68.2-15.9-84.1C168.2,0,143.8,0,100,0S31.8,0,15.9,15.9C0,31.8,0,56.2,0,100s0,68.2,15.9,84.1C31.8,200,56.2,200,100,200z"
/>
</clipPath>
</defs>
</svg>
<img src="{{.BigImage}}" class="absolute left-[-999px] w-[100px]" />

View File

@@ -1,76 +0,0 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{ if not (eq "" .Oembed) }}
<link
rel="alternate"
type="application/json+oembed"
href="{{.Oembed}}&format=json"
/>
<link rel="alternate" type="text/xml+oembed" href="{{.Oembed}}&format=xml" />
{{ end }}
<!---->
{{if .IsProfile}}
<link
rel="apple-touch-icon"
sizes="180x180"
href="/njump/static/favicon/profile/apple-touch-icon.png?v=2"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/njump/static/favicon/profile/favicon-32x32.png?v=2"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/njump/static/favicon/profile/favicon-16x16.png?v=2"
/>
{{else}}
<link
rel="apple-touch-icon"
sizes="180x180"
href="/njump/static/favicon/event/apple-touch-icon.png?v=2"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/njump/static/favicon/event/favicon-32x32.png?v=2"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/njump/static/favicon/event/favicon-16x16.png?v=2"
/>
{{ end }}
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
{{ if not (eq "" .TailwindDebugStuff) }} {{ .TailwindDebugStuff }} {{ else }}
<link
rel="stylesheet"
type="text/css"
href="/njump/static/tailwind-bundle.min.css"
/>
{{ end }}
<style>
@media print {
@page {
margin: 2cm 3cm;
}
}
</style>
<meta name="theme-color" content="#e42a6d" />
{{ if not (eq "" .NaddrNaked) }}
<link rel="canonical" href="https://njump.me/{{.NaddrNaked}}" />
{{ else }}
<link rel="canonical" href="https://njump.me/{{.NeventNaked}}" />
{{ end }}
<script type="text/hyperscript">
on load get [navigator.userAgent.includes('Safari'), navigator.userAgent.includes('Chrome')] then if it[0] is true and it[1] is false add .safari to <body /> end
</script>

View File

@@ -1,253 +0,0 @@
<!doctype html>
<html class="theme--default font-light">
<meta charset="UTF-8" />
<head>
<title>njump - the nostr static gateway</title>
<meta name="description" content="" />
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div
class="mx-auto sm:mt-8 block px-4 sm:flex sm:items-center sm:justify-center sm:px-0"
>
<div
class="flex w-full max-w-screen-xl justify-between gap-10 overflow-visible px-1 print:w-full sm:w-9/12 xl:w-3/5"
>
<div>
<h2 class="text-2xl text-strongpink">What is njump?</h2>
<p class="my-3 leading-5">
<i>njump</i> is a HTTP
<a class="underline" href="https://github.com/nostr-protocol/nostr"
>Nostr</a
>
gateway that allows you to browse profiles, notes and relays; it is
an easy way to preview a resource and then open it with your
preferred client. The typical use of <i>njump</i> is to share a
resource outside the Nostr world, where the
<code>nostr:</code> schema is not (yet) working.
</p>
<p class="my-3 leading-5">
<i>njump</i> currently lives under {{ .Host }}, you can reach it
appending a Nostr
<a
class="underline"
href="https://github.com/nostr-protocol/nips/blob/master/19.md"
>NIP-19</a
>
entity (<code>npub</code>, <code>nevent</code>, <code>naddr</code>,
etc) after the domain:
<span class="rounded bg-lavender px-1 dark:bg-garnet"
>{{ .Host }}/&lt;nip-19-entity&gt;</span
>.
</p>
<p class="my-3 leading-5">
For example, here's
<a
class="underline"
href="/npub1sn0wdenkukak0d9dfczzeacvhkrgz92ak56egt7vdgzn8pv2wfqqhrjdv9"
>a user profile</a
>,
<a
class="underline"
href="/nevent1qqstnl4ddmhc0kzqpj7p543pvq9nvppc4laewc9x5ppucz7aagsa4dspzemhxue69uhhyetvv9ujumn0wd68ytnzv9hxgqgewaehxw309ac8junpd45kgtnxd9shg6npvchxxmmdqyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsds02v2"
>a note</a
>
and a
<a
class="underline"
href="/naddr1qqxnzd3cxqmrzv3exgmr2wfeqy08wumn8ghj7mn0wd68yttsw43zuam9d3kx7unyv4ezumn9wshszyrhwden5te0dehhxarj9ekk7mf0qy88wumn8ghj7mn0wvhxcmmv9uq3zamnwvaz7tmwdaehgu3wwa5kuef0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qdergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsxpqqqp65wy2vhhv"
>long blog post</a
>.
</p>
<h2 class="text-xl text-strongpink">
Try it now, jump to some Nostr content
</h2>
<div
class="my-3 mb-8 rounded-lg bg-zinc-100 p-4 pb-3 dark:bg-neutral-900 sm:p-6 sm:pb-4"
>
<form
_="on submit halt the event's default then go to url `/${event.target.code.value}`"
>
<div
class="flex flex-wrap items-center justify-center sm:flex-nowrap sm:justify-normal"
>
<div class="mb-1.5 text-xl sm:mb-0">{{ .Host }}/</div>
<input
name="code"
placeholder="paste a npub / nprofile / nevent / ..."
autofocus
class="ml-0 w-full basis-full rounded-lg border-0 bg-white p-2 text-base text-gray-700 placeholder:text-gray-300 focus:outline-0 dark:bg-zinc-900 dark:text-neutral-50 dark:placeholder:text-gray-400 sm:ml-1 sm:basis-11/12 sm:rounded-s-lg"
/>
<button
class="ml-0 w-full basis-full rounded-lg border-0 bg-strongpink p-2 text-base uppercase text-white sm:-ml-4 sm:basis-2/12 sm:rounded-s-lg"
>
View
</button>
</div>
</form>
<div class="mt-3 text-center text-sm sm:mt-1 sm:text-left">
or pick
<a
class="underline"
href="/random"
_="on click halt the event then fetch /random with method:'POST' then tell <input[name='code'] /> set @value to result"
>some random content</a
>
</div>
</div>
<p class="my-3 leading-5">
There are several reasons to choose <i>njump</i> when sharing Nostr
content outside of Nostr:
</p>
<h2 class="mt-7 text-2xl text-strongpink">Clean, fast and solid</h2>
<p class="my-3 leading-5">
Pages by <i>njump</i> are extremely lightweight and fast to load
because there isn't any client side javascript involved; they are
minimalistic with the right attention to typography, focusing the
content without unnecessary details. Furthermore they are cached, so
when sharing a page you can expect the other part will load it
without any glitch in a fraction of second: the perfect tool to
onboard new users!
</p>
<h2 class="mt-7 text-2xl text-strongpink">Good preview</h2>
<p class="my-3 leading-5">
<i>njump</i> renders everything on the server-side, so it is able to
generate useful rich previews that work on Telegram, Discord,
Twitter and other places.
</p>
<p class="my-3 leading-5">
When opening the URL directly in a browser, visitors will find
referenced content like images, video and links to referenced Nostr
events displayed in a simple but effective way. It shows the note
parent, allowing the visitor to follow it up and it has custom CSS
for printing or exporting to PDF.
</p>
<h2 class="mt-7 text-2xl text-strongpink">Cooperative (jump-out)</h2>
<p class="my-3 leading-5">
<i>njump</i> is not interested capturing users at all, on the
contrary it invites them to "jump" to the Nostr resource by picking
from a list of web clients or with a <code>nostr:</code> for native
clients. It even remembers the most used one for each visitor and
puts it on the top for fast clicking or tap.
</p>
<p class="my-3 leading-5">
<a
class="underline"
href="https://github.com/nostr-protocol/nips/blob/master/89.md"
>NIP-89</a
>
support coming!
</p>
<h2 class="mt-7 text-2xl text-strongpink">
Search friendly (jump-in)
</h2>
<p class="my-3 leading-5">
This is crucial: <i>njump</i> pages are static so search engines can
index them, this means that <i>njump</i> can help others to discover
great content on Nostr, jump in and join us! <i>njump</i> is the
only nostr resource that has this explicit goal, if you care that a
good note can be found online use <i>njump</i> to share it, this way
you also help Nostr flourish.
</p>
<h2 class="mt-7 text-2xl text-strongpink">Share NIP-05 profiles</h2>
<p class="my-3 leading-5">
Now you can share your own profile with a pretty
<a
class="underline"
href="https://github.com/nostr-protocol/nips/blob/master/05.md"
>NIP-05</a
>
inspired permalink:
<span class="rounded bg-lavender px-1 dark:bg-garnet"
>{{ .Host }}/&lt;nip-5&gt;</span
>, for example:
<a class="underline" href="/nvk.org">https://{{ .Host }}/nvk.org</a>
or
<a class="underline" href="/mike@mikedilger.com"
>https://{{ .Host }}/mike@mikedilger.com</a
>.
</p>
<p class="my-3 leading-5">
A profile shows the basic metadata infos, the used "outbox" relays
and the last notes.
</p>
<h2 class="mt-7 text-2xl text-strongpink">
Share on Twitter and Telegram
</h2>
<p class="my-3 leading-5">
Now you can quickly and effortlessly share Nostr notes on Twitter
and Telegram, and maybe on many other "social platforms": just drop
a link, and njump will render the note text using the preview image
as a canvas, to maximize the sharing experience and utility.<br />
On Telegram we have also the Instant View to access long content
in-app!
</p>
<h2 class="mt-7 text-2xl text-strongpink">Relays view</h2>
<p class="my-3 leading-5">
You can have a view of the last content posted to a relay using
<span class="rounded bg-lavender px-1 dark:bg-garnet"
>{{ .Host }}/r/&lt;relay-host&gt;</span
>, for example:
<a class="underline" href="/r/nostr.wine"
>https://{{ .Host }}/r/nostr.wine</a
>
</p>
<p class="my-3 leading-5">
Some basic infos (<a
href="https://github.com/nostr-protocol/nips/blob/master/11.md"
>NIP-11</a
>) are available; We hope operators will start to make them more
personal and informative so users can have a way to evaluate if/when
to join a relay.
</p>
<h2 class="mt-7 text-2xl text-strongpink">Website widgets</h2>
<div class="my-3 leading-5">
You can embed notes, long form contents and profiles in a web page
with a simple script:<br />
<span class="rounded bg-lavender px-1 dark:bg-garnet"
>&lt;script src="https://{{ .Host }}/embed/&lt;nip-19-entity&gt;"
/&gt;</span
>
<div class="mt-4 gap-8 sm:flex">
<div class="mb-4 flex-auto sm:mb-0">
<script src="/embed/npub1sn0wdenkukak0d9dfczzeacvhkrgz92ak56egt7vdgzn8pv2wfqqhrjdv9"></script>
</div>
<div class="flex-auto">
<script src="/embed/naddr1qqxnzd3cxqmrzv3exgmr2wfeqy08wumn8ghj7mn0wd68yttsw43zuam9d3kx7unyv4ezumn9wshszyrhwden5te0dehhxarj9ekk7mf0qy88wumn8ghj7mn0wvhxcmmv9uq3zamnwvaz7tmwdaehgu3wwa5kuef0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qdergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsxpqqqp65wy2vhhv"></script>
</div>
</div>
</div>
<h2 class="mt-7 text-2xl text-strongpink">Inspector tool</h2>
<p class="my-3 leading-5">
You know, we are all programmers, including our moms, so for every
<i>njump</i> page you can toggle the "Show more details" switch and
inspect the full event JSON. Without installing other tools (like
<a class="underline" href="https://github.com/fiatjaf/nak">nak</a>)
this is probably the fastest way to access that.
</p>
</div>
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -1,119 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>Stream: {{.LiveEvent.Title}} by {{ .LiveEvent.Host.Name }}</title>
{{template "opengraph" .OpenGraphPartial}}
<!---->
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div
class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<div class="w-full break-words print:w-full sm:w-3/4">
<header class="mb-4 max-w-full">
<a class="flex flex-wrap items-center" href="/{{.Npub}}">
<div
class="print:basis-1-12 imgclip mr-2 max-w-full basis-1/6 overflow-hidden sm:mr-4"
>
<img class="block h-auto w-full" src="{{.Metadata.Picture}}" />
</div>
<div class="block print:text-base sm:grow">
<div class="leading-4 sm:text-2xl">
{{.Metadata.Name}}
<!---->
{{if not (eq .Metadata.Name .Metadata.DisplayName)}}
<span class="text-stone-400 sm:text-xl"
>/ {{.Metadata.DisplayName}}</span
>
{{end}}
</div>
<div class="text-sm leading-4 text-stone-400 sm:text-base">
{{.NpubShort}}
</div>
</div>
</a>
</header>
<div class="w-full text-right text-sm text-stone-400">
{{.CreatedAt}}
</div>
<div class="w-full text-right text-sm text-stone-400">
{{ if not (eq "" .ParentLink) }} in reply to
<span class="text-strongpink">{{ .ParentLink }}</span> {{ end }}
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<article
class="prose-cite:text-sm prose mb-6 max-w-full leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-gray-100 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-zinc-800 sm:prose-a:text-justify"
>
<h1 class="text-2xl">
<span class="mr-2">{{.LiveEvent.Title}}</span>{{ if (eq "ended"
.LiveEvent.Status)}}
<span
class="whitespace-nowrap rounded bg-neutral-400 px-4 py-1 align-text-top text-base text-white dark:bg-neutral-700"
>Ended</span
>
{{ else if (eq "live" .LiveEvent.Status)}}
<span
class="whitespace-nowrap rounded bg-strongpink px-4 py-1 align-text-top text-base text-white"
>Live now!</span
>
{{ end }}
</h1>
<div class="mb-4">
{{ if not (eq "" .LiveEvent.HostNpub) }} Streaming hosted by
<a href="/{{ .LiveEvent.HostNpub }}"
>{{ .LiveEvent.Host.Name }}</a
>
{{ end }}
</div>
<!-- main content -->
<div class="mb-4">
{{ range .LiveEvent.Tags }}
<span
class="mr-2 whitespace-nowrap rounded bg-neutral-200 px-2 dark:bg-neutral-700 dark:text-white"
>{{ . }}</span
>
{{ end }}
</div>
{{ if (not (eq "" .LiveEvent.Summary))}}
<div>{{ .LiveEvent.Summary }}</div>
{{ end }} {{ if (not (eq "" .LiveEvent.Image))}}
<img
src="{{ .LiveEvent.Image }}"
alt="{{ .Alt }}"
_="on load
repeat until '{{ .LiveEvent.Status }}' == 'ended'
set @src to '{{ .LiveEvent.Image }}'
wait 5s
end
"
/>
{{ end }}
</article>
{{template "details" .DetailsPartial}}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
</div>
{{template "clients" .ClientsPartial}}
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -1,85 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>{{.TitleizedContent}}</title>
{{template "opengraph" .OpenGraphPartial}}
<!---->
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div
class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<div class="w-full break-words print:w-full sm:w-3/4">
<header class="mb-4 max-w-full">
<a class="flex flex-wrap items-center" href="/{{.Npub}}">
<div
class="print:basis-1-12 imgclip mr-2 max-w-full basis-1/6 overflow-hidden sm:mr-4"
>
<img class="block h-auto w-full" src="{{.Metadata.Picture}}" />
</div>
<div class="block print:text-base sm:grow">
<div class="leading-4 sm:text-2xl">
{{.Metadata.Name}}
<!---->
{{if not (eq .Metadata.Name .Metadata.DisplayName)}}
<span class="text-stone-400 sm:text-xl"
>/ {{.Metadata.DisplayName}}</span
>
{{end}}
</div>
<div class="text-sm leading-4 text-stone-400 sm:text-base">
{{.NpubShort}}
</div>
</div>
</a>
</header>
<div class="w-full text-right text-sm text-stone-400">
{{.CreatedAt}}
</div>
<div class="w-full text-right text-sm text-stone-400">
{{ if not (eq "" .ParentLink) }} messaging during the live event
<span class="text-strongpink">{{ .ParentLink }}</span> {{ end }}
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<article
class="prose-cite:text-sm prose mb-6 leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-gray-100 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-zinc-800 sm:prose-a:text-justify"
>
{{ if (not (eq "" .Subject))}}
<h1 class="text-2xl">{{.Subject}}</h1>
{{ else }}
<h1 class="hidden">
{{.Metadata.ShortName}} on Nostr: {{.TitleizedContent}}
</h1>
{{ end }}
<!-- main content -->
{{ .Content }}
</article>
{{template "details" .DetailsPartial}}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
</div>
{{template "clients" .ClientsPartial}}
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -1,85 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>{{.TitleizedContent}}</title>
{{template "opengraph" .OpenGraphPartial}}
<!---->
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div
class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<div class="w-full break-words print:w-full sm:w-3/4">
<header class="mb-4 max-w-full">
<a class="flex flex-wrap items-center" href="/{{.Npub}}">
<div
class="print:basis-1-12 imgclip mr-2 max-w-full basis-1/6 overflow-hidden sm:mr-4"
>
<img class="block h-auto w-full" src="{{.Metadata.Picture}}" />
</div>
<div class="block print:text-base sm:grow">
<div class="leading-4 sm:text-2xl">
{{.Metadata.Name}}
<!---->
{{if not (eq .Metadata.Name .Metadata.DisplayName)}}
<span class="text-stone-400 sm:text-xl"
>/ {{.Metadata.DisplayName}}</span
>
{{end}}
</div>
<div class="text-sm leading-4 text-stone-400 sm:text-base">
{{.NpubShort}}
</div>
</div>
</a>
</header>
<div class="w-full text-right text-sm text-stone-400">
{{.CreatedAt}}
</div>
<div class="w-full text-right text-sm text-stone-400">
{{ if not (eq "" .ParentLink) }} in reply to
<span class="text-strongpink">{{ .ParentLink }}</span> {{ end }}
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<article
class="prose-cite:text-sm prose prose mb-6 leading-5 dark:prose-invert prose-headings:font-light prose-p:m-0 prose-p:mb-2 prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:mb-2 prose-blockquote:mt-2 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-neutral-200 prose-blockquote:py-2 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-blockquote:pt-0 prose-blockquote:font-light prose-blockquote:not-italic prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-li:mb-2 dark:prose-blockquote:border-l-neutral-700 sm:prose-a:text-justify"
>
{{ if (not (eq "" .Subject))}}
<h1 class="text-2xl">{{.Subject}}</h1>
{{ else }}
<h1 class="hidden">
{{.Metadata.ShortName}} on Nostr: {{.TitleizedContent}}
</h1>
{{ end }}
<!-- main content -->
<div dir="auto">{{ .Content }}</div>
</article>
{{template "details" .DetailsPartial}}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
</div>
{{template "clients" .ClientsPartial}}
</div>
</div>
{{template "footer" .OpenGraphPartial}}
</body>
</html>

View File

@@ -1,39 +0,0 @@
{{ if not (eq "" .SingleTitle)}}
<!-- we only display this on twitter as a single title -->
<meta name="twitter:title" content="{{.SingleTitle}}" />
{{ else }}
<!-- these are not shown by twitter at all, so let's not even give them -->
<meta property="og:site_name" content="{{.Superscript}}" />
<meta property="og:title" content="{{.Subscript}}" />
{{ end }}
<!-- this is used for when we want to take over the entire screen on twitter,
mostly for the big "text-to-image" images -->
{{ if not (eq "" .BigImage) }}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@nostrprotocol" />
<meta property="og:image" content="{{.BigImage}}" />
<meta name="twitter:image" content="{{.BigImage}}" />
{{ else }}
<!-- otherwise we tell twitter to display it as a normal text-based embed.
these distinctions don't seem to make any difference in other platforms,
maybe telegram -->
<meta name="twitter:card" content="summary" />
{{ if not (eq "" .Image) }}
<meta property="og:image" content="{{.Image}}" />
<meta name="twitter:image" content="{{.ProxiedImage}}" />
{{ end }}
<!---->
{{ if not (eq "" .Video) }}
<meta property="og:video" content="{{.Video}}" />
<meta property="og:video:secure_url" content="{{.Video}}" />
<meta property="og:video:type" content="video/{{.VideoType}}" />
{{ end }}
<!-- end the BigImage /if above -->
{{ end }}
<!-- now just display the short text if we have any (which we always should) -->
{{ if not (eq "" .Text) }}
<meta property="og:description" content="{{.Text}}" />
<meta name="twitter:description" content="{{.Text}}" />
{{ end }}

View File

@@ -1,51 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>Nostr Event {{.Kind}} - {{.KindDescription }}</title>
<!---->
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div
class="mx-auto block px-4 sm:flex sm:items-center sm:justify-center sm:px-0"
>
<div
class="flex w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 print:w-full sm:w-11/12 md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<div class="w-full break-words print:w-full md:w-10/12 lg:w-9/12">
<header class="">
<div class="mb-4 text-2xl">{{.KindDescription}}</div>
</header>
{{ if not (eq "" .Alt) }}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<article
class="prose-cite:text-sm prose prose mb-6 leading-5 dark:prose-invert"
>
{{.Alt}}
</article>
{{ end }}
<!---->
{{template "details" .DetailsPartial}}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
</div>
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -1,175 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>{{.Metadata.Name}} / {{.Metadata.DisplayName}} is on nostr</title>
<meta
name="description"
content="{{.Npub}} is {{.Metadata.Name}} / {{.Metadata.DisplayName}} public key on nostr"
/>
<meta property="og:title" content="{{.Title}}" />
<meta property="og:site_name" content="{{.Npub}}" />
{{ if not (eq "" .Metadata.Picture) }}
<meta property="og:image" content="{{.Metadata.Picture}}" />
<meta property="twitter:image" content="{{.Proxy}}{{.Metadata.Picture}}" />
{{end}} {{ if not (eq "" .Metadata.About) }}
<meta property="og:description" content="{{.Metadata.About}}" />
{{end}}
<meta name="twitter:card" content="summary" />
<link rel="canonical" href="https://njump.me/{{.Npub}}" />
<link
rel="sitemap"
type="application/xml"
title="Sitemap for {{.Npub}}"
href="/{{.Npub}}.xml"
/>
<link
rel="alternate"
type="application/atom+xml"
title="RSS"
href="/{{.Npub}}.rss"
/>
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 print:text-black dark:bg-neutral-900 dark:text-neutral-50"
>
{{template "top" .}}
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div
class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-11/12 sm:px-4 md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<header
class="relative top-auto flex basis-1/4 items-center self-start sm:sticky sm:top-8 sm:mt-8 sm:block sm:items-start"
>
<div
class="hidden basis-[64%] items-center overflow-hidden text-left"
_="on load or scroll from window or resize from window get #profile_name then measure its top, height then if top is less than height / -2 or height is 0 remove .hidden otherwise add .hidden"
>
<div class="mb-3 sm:text-center">
<div class="text-2xl">{{.Metadata.Name}}</div>
{{if not (eq .Metadata.Name .Metadata.DisplayName)}}
<div class="text-base text-stone-400">
{{.Metadata.DisplayName}}
</div>
{{end}}
</div>
</div>
<div
class="imgclip max-w-[40%] basis-2/5 overflow-hidden sm:max-w-full sm:basis-auto"
>
<img class="block h-auto w-full" src="{{.Metadata.Picture}}" />
</div>
</header>
<div class="w-full break-words print:w-full sm:w-1/2">
<header class="mb-6 hidden leading-5 sm:flex sm:items-center">
<h1>
<div id="profile_name" class="text-2xl">{{.Metadata.Name}}</div>
{{if not (eq .Metadata.Name .Metadata.DisplayName)}}
<div class="text-xl text-stone-400">
{{.Metadata.DisplayName}}
</div>
{{end}}
</h1>
</header>
<div
class="-ml-4 mb-6 h-1.5 w-1/2 bg-zinc-100 sm:-ml-2.5 dark:bg-zinc-700"
></div>
{{ if not (eq "" .Metadata.Website) }}
<div class="mb-6 leading-5">
<a
class="border-b-2 border-b-gray-300 pb-0.5 hover:text-strongpink"
href="{{.NormalizedAuthorWebsiteURL}}"
>{{.Metadata.Website}}</a
>
</div>
{{ end }} {{ if not (eq "" .RenderedAuthorAboutText) }}
<div
class="prose mb-6 leading-5 dark:prose-invert prose-headings:font-light sm:prose-a:text-justify"
dir="auto"
>
{{.RenderedAuthorAboutText}}
</div>
{{ end }} {{ if or (not (eq "" .Metadata.Website)) (not (eq ""
.RenderedAuthorAboutText)) }}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 sm:-ml-2.5 dark:bg-zinc-700"
></div>
{{ end }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Public Key</div>
{{.Npub}}
</div>
<div class="mb-6 leading-5">
{{ if not (eq "" .Metadata.NIP05) }}
<div class="text-sm text-strongpink">NIP-05 Address</div>
{{.Metadata.NIP05}} {{ end }}
</div>
<div class="mb-6 leading-5">
{{ if not (eq "" .Metadata.LUD16) }}
<div class="text-sm text-strongpink">NIP-57 Address</div>
{{.Metadata.LUD16}} {{ end }}
</div>
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Profile Code</div>
{{.Nprofile}}
</div>
{{ if not (eq 0 (len .AuthorRelays)) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Publishing to</div>
{{range $index, $element := .AuthorRelays}}
<a
href="/r/{{$element}}"
class="mr-1 mt-2 inline-block max-w-full rounded-lg border border-slate-300 px-2 py-0.5 hover:border hover:border-solid hover:border-strongpink hover:bg-strongpink hover:text-white"
>{{$element}}</a
>
{{end}}
</div>
{{ end }}
<!---->
{{template "details" .DetailsPartial}}
<div
_="init fetch /profile-last-notes/{{.Npub}} then put the result into me end "
>
{{if not (eq 0 (len .LastNotes))}}
<aside>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 sm:-ml-2.5 dark:bg-zinc-700"
></div>
<nav class="mb-6 leading-5">
<h2 class="text-2xl text-strongpink">Last Notes</h2>
{{range $i, $ee := .LastNotes}}
<a
class="my-8 block no-underline hover:-ml-6 hover:border-l-05rem hover:border-solid hover:border-l-gray-100 hover:pl-4 dark:hover:border-l-zinc-700"
href="/{{$ee.Nevent}}"
>
</a>
{{end}}
</nav>
</aside>
{{end}}
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 sm:-ml-2.5 dark:bg-zinc-700"
></div>
</div>
{{template "clients" .ClientsPartial}}
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -1,157 +0,0 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>Nostr Relay {{.Hostname}} - {{.Info.Name}}</title>
<meta property="og:title" content="{{.Hostname}} - nostr relay" />
<meta name="twitter:title" content="{{.Hostname}} - nostr relay" />
<meta property="og:site_name" content="{{.Hostname}} - nostr relay" />
{{ if not (eq "" .Info.Icon) }}
<meta property="og:image" content="{{.Info.Icon}}" />
<meta name="twitter:image" content="{{.Proxy}}{{.Info.Icon}}" />
{{end}} {{ if not (eq "" .Info.Description) }}
<meta property="og:description" content="{{.Info.Description}}" />
<meta name="twitter:description" content="{{.Info.Description}}" />
{{end}}
<meta name="twitter:card" content="summary" />
<link
rel="sitemap"
type="application/xml"
title="Sitemap for {{.Hostname}}"
href="/r/{{.Hostname}}.xml"
/>
<link
rel="alternate"
type="application/atom+xml"
title="RSS"
href="/r/{{.Hostname}}.rss"
/>
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div
class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible px-4 print:w-full sm:flex md:w-10/12 lg:w-9/12 lg:gap-48vw"
>
<div
class="relative top-auto flex basis-1/4 items-center self-start sm:sticky sm:top-8 sm:mt-8 sm:items-start"
>
<div
class="flex hidden basis-2/3 items-center overflow-hidden text-left text-2xl sm:break-all"
_="on load or scroll from window or resize from window get #relay_name then measure its top, height then if top is less than height / -2 or height is 0 add .flex then remove .hidden otherwise remove .flex then add .hidden"
>
{{.Info.Name}}
</div>
<div
class="imgclip max-w-full basis-2/5 overflow-hidden sm:basis-auto"
>
<img class="block h-auto w-full" src="{{.Info.Icon}}" />
</div>
</div>
<div class="w-full break-words print:w-full sm:w-1/2">
<header class="mb-6 hidden leading-5 sm:flex sm:items-center">
<h1>
<div id="relay_name" class="text-2xl">{{.Info.Name}}</div>
</h1>
</header>
<div
class="-ml-4 mb-6 h-1.5 w-1/2 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<div class="mb-6 leading-5">
<a
class="border-b-2 border-b-gray-300 pb-0.5 hover:text-strongpink"
href="https://{{.Hostname}}"
target="_blank"
_="on mouseenter set my innerText to my.innerText.replace('wss://', 'https://')
on mouseleave set my innerText to my.innerText.replace('https://', 'wss://')"
>wss://{{.Hostname}}</a
>
</div>
<div
class="prose mb-6 leading-5 dark:prose-invert prose-headings:font-light sm:prose-a:text-justify"
dir="auto"
>
{{.Info.Description}}
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/2 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
{{ if not (eq "" .Info.PubKey) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Public Key</div>
{{.Info.PubKey}}
</div>
{{ end }}
<!---->
{{ if not (eq "" .Info.Contact) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Contact</div>
<a href="{{.Info.Contact}}">{{.Info.Contact}}</a>
</div>
{{ end }}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<aside>
<div class="mb-6 leading-5">
<h2 class="text-2xl text-strongpink">Last Notes</h2>
{{range $i, $ee := .LastNotes}}
<a
class="my-8 block no-underline hover:-ml-6 hover:border-l-05rem hover:border-solid hover:border-l-gray-100 hover:pl-4 dark:hover:border-l-zinc-700"
href="/{{$ee.Nevent}}"
>
<div
class="-ml-2.5 mb-1.5 flex flex-row border-b-4 border-solid border-b-gray-100 pb-1 pl-2.5 dark:border-b-neutral-800"
>
<div class="text-sm text-strongpink">
{{$ee.CreatedAtStr}}
</div>
<br />
{{if $ee.IsReply}}
<div class="ml-2 text-xs text-gray-300 dark:text-gray-400">
- reply
</div>
{{end}}
<div
class="ml-auto text-xs text-zinc-700 dark:text-neutral-50"
>
by
<span
class="rounded bg-lavender px-1 hover:bg-strongpink hover:text-white dark:bg-garnet dark:hover:bg-strongpink"
href="/{{$ee.Npub}}"
>{{$ee.NpubShort}}</span
>
</div>
</div>
<div
class="mt-0.5 max-h-40 basis-full overflow-hidden hover:text-strongpink"
_="on load if my scrollHeight > my offsetHeight add .gradient"
dir="auto"
>
{{$ee.Preview}}
</div>
</a>
{{end}}
</div>
</aside>
</div>
{{template "clients" .ClientsPartial}}
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -1,50 +0,0 @@
<meta charset="UTF-8" />
<!-- check https://nikstar.me/post/instant-view/ for more information on how this was set up -->
<!-- required stuff so telegram treats us like a medium.com article -->
<meta property="al:android:app_name" content="Medium" />
<meta property="article:published_time" content="{{.CreatedAt}}" />
<!-- stuff that goes in the actual telegram message preview -->
<meta property="og:site_name" content="{{.AuthorLong}}" />
{{ if not (eq "" .Description) }}
<meta property="og:description" content="{{.Description}}" />
{{ end }}
<!---->
{{ if not (eq "" .Image) }}
<meta property="og:image" content="{{.Image}}" />
{{ end }}
<!---->
{{ if not (eq "" .Video) }}
<meta property="og:video" content="{{.Video}}" />
<meta property="og:video:secure_url" content="{{.Video}}" />
<meta property="og:video:type" content="video/{{.VideoType}}" />
{{ end }}
<!-- stuff that affects the content inside the preview window -->
<meta name="author" content="{{.Metadata.ShortName}}" />
<meta name="telegram:channel" content="@nostr_protocol" />
<!-- basic content of the preview window -->
<article>
<h1>
{{ if not (eq "" .Subject) }} {{.Subject}} {{ else }}
<a href="/{{.Metadata.Npub}}">{{.Metadata.ShortName}}</a> on Nostr: {{ end
}}
</h1>
{{ if not (eq "" .ParentLink) }}
<aside>in reply to {{ .ParentLink }}</aside>
{{ end }}
<!---->
{{ if not (eq "" .Summary) }}
<aside>{{ .Summary }}</aside>
{{ end }}
<!---->
{{.Content}} {{ if not (eq "" .Subject) }}
<aside>
<a href="/{{.Metadata.Npub}}">{{.Metadata.ShortName}}</a>
</aside>
{{ end }}
</article>

View File

@@ -1,45 +0,0 @@
<header class="items-center p-4 pb-6 text-sm print:block sm:flex sm:text-base">
<a
href="https://nostr.com"
class="text-md text-right text-sm print:hidden md:basis-1/6"
target="_blank"
>What is <span class="text-strongpink">Nostr</span>?</a
>
<div
class="print:hidden; relative float-right h-4 w-4 cursor-pointer rounded-full text-gray-100 dark:text-gray-700 sm:fixed sm:right-4 sm:top-4 sm:float-none"
_="on click tell <html /> toggle between .theme--dark and .theme--default then get your @class then get it[0].split('--')[1].split(' ')[0] then set localStorage.theme to it
on load tell <html /> get localStorage.theme then if it is 'dark' add .theme--dark then remove .theme--default else if it is not 'default' then get window.matchMedia('(prefers-color-scheme: dark)').matches then if it is true add .theme--dark then remove .theme--default end"
>
<svg
aria-hidden="true"
data-prefix="fas"
class="block dark:hidden"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M283.211 512c78.962 0 151.079-35.925 198.857-94.792 7.068-8.708-.639-21.43-11.562-19.35-124.203 23.654-238.262-71.576-238.262-196.954 0-72.222 38.662-138.635 101.498-174.394 9.686-5.512 7.25-20.197-3.756-22.23A258.156 258.156 0 0 0 283.211 0c-141.309 0-256 114.511-256 256 0 141.309 114.511 256 256 256z"
></path>
</svg>
<svg
aria-hidden="true"
data-prefix="fas"
class="hidden dark:block"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M256 160c-52.9 0-96 43.1-96 96s43.1 96 96 96 96-43.1 96-96-43.1-96-96-96zm246.4 80.5l-94.7-47.3 33.5-100.4c4.5-13.6-8.4-26.5-21.9-21.9l-100.4 33.5-47.4-94.8c-6.4-12.8-24.6-12.8-31 0l-47.3 94.7L92.7 70.8c-13.6-4.5-26.5 8.4-21.9 21.9l33.5 100.4-94.7 47.4c-12.8 6.4-12.8 24.6 0 31l94.7 47.3-33.5 100.5c-4.5 13.6 8.4 26.5 21.9 21.9l100.4-33.5 47.3 94.7c6.4 12.8 24.6 12.8 31 0l47.3-94.7 100.4 33.5c13.6 4.5 26.5-8.4 21.9-21.9l-33.5-100.4 94.7-47.3c13-6.5 13-24.7.2-31.1zm-155.9 106c-49.9 49.9-131.1 49.9-181 0-49.9-49.9-49.9-131.1 0-181 49.9-49.9 131.1-49.9 181 0 49.9 49.9 49.9 131.1 0 181z"
></path>
</svg>
</div>
</header>
<script type="text/hyperscript">
on beforeprint from window tell <html /> remove .theme--dark add .theme--default
on afterprint from window tell <html /> add .theme--dark remove .theme--default
</script>

47
top.templ Normal file
View File

@@ -0,0 +1,47 @@
package main
templ topTemplate() {
<header class="items-center p-4 pb-6 text-sm print:block sm:flex sm:text-base">
<a
href="https://nostr.com"
class="text-md text-right text-sm print:hidden md:basis-1/6"
target="_blank"
>What is <span class="text-strongpink">Nostr</span>?</a>
<div
class="print:hidden; relative float-right h-4 w-4 cursor-pointer rounded-full text-gray-100 dark:text-gray-700 sm:fixed sm:right-4 sm:top-4 sm:float-none"
_="on click tell <html /> toggle between .theme--dark and .theme--default then get your @class then get it[0].split('--')[1].split(' ')[0] then set localStorage.theme to it
on load tell <html /> get localStorage.theme then if it is 'dark' add .theme--dark then remove .theme--default else if it is not 'default' then get window.matchMedia('(prefers-color-scheme: dark)').matches then if it is true add .theme--dark then remove .theme--default end"
>
<svg
aria-hidden="true"
data-prefix="fas"
class="block dark:hidden"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M283.211 512c78.962 0 151.079-35.925 198.857-94.792 7.068-8.708-.639-21.43-11.562-19.35-124.203 23.654-238.262-71.576-238.262-196.954 0-72.222 38.662-138.635 101.498-174.394 9.686-5.512 7.25-20.197-3.756-22.23A258.156 258.156 0 0 0 283.211 0c-141.309 0-256 114.511-256 256 0 141.309 114.511 256 256 256z"
></path>
</svg>
<svg
aria-hidden="true"
data-prefix="fas"
class="hidden dark:block"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M256 160c-52.9 0-96 43.1-96 96s43.1 96 96 96 96-43.1 96-96-43.1-96-96-96zm246.4 80.5l-94.7-47.3 33.5-100.4c4.5-13.6-8.4-26.5-21.9-21.9l-100.4 33.5-47.4-94.8c-6.4-12.8-24.6-12.8-31 0l-47.3 94.7L92.7 70.8c-13.6-4.5-26.5 8.4-21.9 21.9l33.5 100.4-94.7 47.4c-12.8 6.4-12.8 24.6 0 31l94.7 47.3-33.5 100.5c-4.5 13.6 8.4 26.5 21.9 21.9l100.4-33.5 47.3 94.7c6.4 12.8 24.6 12.8 31 0l47.3-94.7 100.4 33.5c13.6 4.5 26.5-8.4 21.9-21.9l-33.5-100.4 94.7-47.3c13-6.5 13-24.7.2-31.1zm-155.9 106c-49.9 49.9-131.1 49.9-181 0-49.9-49.9-49.9-131.1 0-181 49.9-49.9 131.1-49.9 181 0 49.9 49.9 49.9 131.1 0 181z"
></path>
</svg>
</div>
</header>
<script type="text/hyperscript">
on beforeprint from window tell <html /> remove .theme--dark add .theme--default
on afterprint from window tell <html /> add .theme--dark remove .theme--default
</script>
}

65
xml-pages.go Normal file
View File

@@ -0,0 +1,65 @@
package main
import (
_ "embed"
"github.com/nbd-wtf/go-nostr/nip11"
sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/tylermmorton/tmpl"
)
var (
//go:embed xml/sitemap.xml
tmplSitemap string
SitemapTemplate = tmpl.MustCompile(&SitemapPage{})
)
type SitemapPage struct {
Host string
ModifiedAt string
// for the profile sitemap
Npub string
// for the relay sitemap
RelayHostname string
Info *nip11.RelayInformationDocument
// for the profile and relay sitemaps
LastNotes []EnhancedEvent
// for the archive sitemap
PathPrefix string
Data []string
}
func (*SitemapPage) TemplateText() string { return tmplSitemap }
var (
//go:embed xml/rss.xml
tmplRSS string
RSSTemplate = tmpl.MustCompile(&RSSPage{})
)
type RSSPage struct {
Host string
ModifiedAt string
Title string
// for the profile RSS
Npub string
Metadata sdk.ProfileMetadata
// for the relay RSS
RelayHostname string
Info *nip11.RelayInformationDocument
// for the profile and relay RSSs
LastNotes []EnhancedEvent
// for the archive RSS
PathPrefix string
Data []string
}
func (*RSSPage) TemplateText() string { return tmplRSS }