From 67599a696fea5e1c8fdfe20bd3b4d1089a874570 Mon Sep 17 00:00:00 2001 From: Daniele Tonon Date: Mon, 11 Sep 2023 07:48:38 +0200 Subject: [PATCH] Add homepage --- main.go | 4 +- render.go | 2 +- render_homepage.go | 48 ++++++++++++++++++ render_try.go | 15 ++++++ static/styles.css | 97 +++++++++++++++++++++++++++++++++++++ static/styles.scss | 81 +++++++++++++++++++++++++++++++ templates/head_common.html | 2 +- templates/homepage.html | 99 ++++++++++++++++++++++++++++++++++++++ templates/scripts.js | 46 ++++++++++-------- 9 files changed, 370 insertions(+), 24 deletions(-) create mode 100644 render_homepage.go create mode 100644 render_try.go create mode 100644 templates/homepage.html diff --git a/main.go b/main.go index 2360d9d..0d84caf 100644 --- a/main.go +++ b/main.go @@ -61,6 +61,7 @@ func main() { // initialize templates // use a mapping to expressly link the templates and share them between more kinds/types + templateMapping["homepage"] = "homepage.html" templateMapping["profile"] = "profile.html" templateMapping["profile_sitemap"] = "sitemap.xml" templateMapping["note"] = "note.html" @@ -88,6 +89,8 @@ func main() { ) // routes + http.HandleFunc("/", render) + http.HandleFunc("/try", renderTry) http.HandleFunc("/robots.txt", renderRobots) http.HandleFunc("/njump/image/", generate) http.HandleFunc("/njump/proxy/", proxy) @@ -96,7 +99,6 @@ func main() { http.HandleFunc("/relays-archive/", renderArchive) http.HandleFunc("/npubs-archive.xml", renderArchive) http.HandleFunc("/relays-archive.xml", renderArchive) - http.HandleFunc("/", render) log.Print("listening at http://0.0.0.0:" + s.Port) if err := http.ListenAndServe("0.0.0.0:"+s.Port, nil); err != nil { diff --git a/render.go b/render.go index 18444a6..d4de9fb 100644 --- a/render.go +++ b/render.go @@ -66,7 +66,7 @@ func render(w http.ResponseWriter, r *http.Request) { } if code == "" { - fmt.Fprintf(w, "call /") + renderHomepage(w, r) return } diff --git a/render_homepage.go b/render_homepage.go new file mode 100644 index 0000000..b202a7f --- /dev/null +++ b/render_homepage.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "net/http" + "time" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" +) + +func renderHomepage(w http.ResponseWriter, r *http.Request) { + typ := "homepage" + w.Header().Set("Cache-Control", "max-age=3600") + + npubsHex := cache.GetPaginatedkeys("pa", 1, 50) + npubs := []string{} + for i := 0; i < len(npubsHex); i++ { + npub, _ := nip19.EncodePublicKey(npubsHex[i]) + npubs = append(npubs, npub) + } + + ctx, cancel := context.WithTimeout(r.Context(), time.Second*5) + defer cancel() + var lastEvents []*nostr.Event + if relay, err := pool.EnsureRelay("nostr.wine"); err == nil { + lastEvents, _ = relay.QuerySync(ctx, nostr.Filter{ + Kinds: []int{1}, + Limit: 50, + }) + } + lastNotes := []string{} + relay := []string{"wss://nostr.wine"} + for _, n := range lastEvents { + nevent, _ := nip19.EncodeEvent(n.ID, relay, n.PubKey) + lastNotes = append(lastNotes, nevent) + } + + params := map[string]any{ + "npubs": npubs, + "lastNotes": lastNotes, + } + + if err := tmpl.ExecuteTemplate(w, templateMapping[typ], params); err != nil { + log.Error().Err(err).Msg("error rendering") + return + } +} diff --git a/render_try.go b/render_try.go new file mode 100644 index 0000000..ca319c4 --- /dev/null +++ b/render_try.go @@ -0,0 +1,15 @@ +package main + +import ( + "net/http" +) + +func renderTry(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, "Failed to parse form data", http.StatusBadRequest) + return + } + nip19entity := r.FormValue("nip19entity") + http.Redirect(w, r, "/"+nip19entity, http.StatusFound) +} diff --git a/static/styles.css b/static/styles.css index 5a40c0f..ed9f0f5 100644 --- a/static/styles.css +++ b/static/styles.css @@ -250,6 +250,70 @@ iframe { width: 100%; } } +.container .try { + padding: 2rem 2rem 1.4rem 2rem; + border-radius: 8px; + font-size: 0.8rem; +} +.theme--default .container .try { + background-color: #f3f3f3; +} +.theme--dark .container .try { + background-color: #2d2d2d; +} +@media (max-width: 580px) { + .container .try { + padding: 1rem 1rem 0.8rem 1rem; + } +} +.container .try .tryForm { + display: flex; +} +.container .try .tryForm input, .container .try .tryForm button { + font-size: 1.2rem; + padding: 0.5rem; + flex-basis: 90%; + border-radius: 8px; +} +.theme--default .container .try .tryForm input { + color: #373737; + background-color: #ffffff; + border: 1px solid #c9c9c9; +} +.theme--dark .container .try .tryForm input { + color: #fafafa; + background-color: #1e1e1e; + border: 1px solid #969696; +} +.container .try .tryForm input:focus { + outline: none; /* Remove the default outline */ +} +.theme--default .container .try .tryForm ::placeholder { + color: #c9c9c9; +} +.theme--dark .container .try .tryForm ::placeholder { + color: #969696; +} +.container .try .tryForm button { + flex-basis: 10%; +} +.theme--default .container .try .tryForm button { + background-color: #e32a6d; + color: #ffffff; + border: none; + margin-left: -16px; +} +.theme--dark .container .try .tryForm button { + background-color: #e32a6d; + color: #ffffff; + border: none; + margin-left: -16px; +} +.container .try #pickRandomEntityLink { + display: block; + padding-left: 0.4rem; + margin-top: 0.2rem; +} .container .columnA { position: -webkit-sticky; position: sticky; @@ -876,6 +940,39 @@ iframe { } } +body.homepage .container_wrapper { + display: block; +} +@media (max-width: 580px) { + body.homepage .container_wrapper { + display: block; + padding: 0 1rem; + margin: 0 auto; + } +} +body.homepage .container_wrapper .container { + display: block; + width: 60%; + margin: 0 auto; +} +@media (max-width: 580px) { + body.homepage .container_wrapper .container { + display: block; + width: 100%; + } +} +body.homepage .container_wrapper .container span.exampleUrl { + color: #ffffff; + padding: 0 0.4rem; + border-radius: 4px; +} +.theme--default body.homepage .container_wrapper .container span.exampleUrl { + background-color: #e32a6d; +} +.theme--dark body.homepage .container_wrapper .container span.exampleUrl { + background-color: #e32a6d; +} + body.profile .column_content { flex-basis: 50%; max-width: 50%; diff --git a/static/styles.scss b/static/styles.scss index 2bcc817..c7a1674 100644 --- a/static/styles.scss +++ b/static/styles.scss @@ -290,6 +290,56 @@ iframe { width: 100%; } + .try { + @include themed() { + background-color: t($over-bg); + } + padding: 2rem 2rem 1.4rem 2rem; + border-radius: 8px; + font-size: 0.8rem; + @media (max-width: 580px) { + padding: 1rem 1rem 0.8rem 1rem; + } + .tryForm { + display: flex; + input, button { + font-size: 1.2rem; + padding: 0.5rem; + flex-basis: 90%; + border-radius: 8px; + } + input { + @include themed() { + color: t($base7); + background-color: t($bg-up); + border: 1px solid t($base4); + } + &:focus { + outline: none; /* Remove the default outline */ + } + } + ::placeholder { + @include themed() { + color: t($base4); + } + } + button { + flex-basis: 10%; + @include themed() { + background-color: t($accent1); + color: $color-base1; + border: none; + margin-left: -16px; + } + } + } + #pickRandomEntityLink { + display: block; + padding-left: 0.4rem; + margin-top: 0.2rem; + } + } + .columnA { position: -webkit-sticky; position: sticky; @@ -802,6 +852,37 @@ iframe { } } +body.homepage { + .container_wrapper { + display: block; + @media (max-width: 580px) { + display: block; + padding: 0 1rem; + margin: 0 auto; + } + + .container { + display: block; + width: 60%; + margin: 0 auto; + @media (max-width: 580px) { + display: block; + width: 100%; + } + + span.exampleUrl { + @include themed() { + background-color: t($accent1); + } + color: $color-base1; + padding: 0 0.4rem; + border-radius: 4px; + } + + } + } +} + body.profile { .column_content { flex-basis: 50%; diff --git a/templates/head_common.html b/templates/head_common.html index 43acf04..70fb37a 100644 --- a/templates/head_common.html +++ b/templates/head_common.html @@ -1,2 +1,2 @@ - + diff --git a/templates/homepage.html b/templates/homepage.html new file mode 100644 index 0000000..d97b795 --- /dev/null +++ b/templates/homepage.html @@ -0,0 +1,99 @@ + + + + + Njump - The Nostr static gateway + + + {{template "head_common.html" }} + + + + {{template "top.html" .}} + +
+
+
+

What is Njump?

+ +

Njump is a static Nostr gateway that allows you to browse profiles, notes and relays; it is an easy way to preview a resource and then open it with the preferred client.

+

Njump currently lives under njump.me, you can reach it appending a nostr (NIP-19) entity (npub, nevent, naddr, etc) after the domain: njump.me/p/<nip-19-entity>
+ For example a user profile, a note or a long blog post

+ +

The typical use of njump is to share a resource outside the nostr world (clients), where the nostr: schema is not (yet) active. There are several reasons to prefer njump to share a nostr resource, versus other web clients, let's see them.

+ +

Try it now, jump to a nostr content

+ +
+
+
+ +
+ +
+ +
+ +

Clean, fast and solid

+ +

Pages by njump are extremely light 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 unecessary details. Furthermore they are cached, so 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!

+ +

Good preview

+ + Njump previews notes in a simple but effective way, including links (to other nostr resources and web), images, video, quotes, code. It is compatible with long form content so it also renders markdown. It shows the note parent, allowing to follow it up. It has custom css for printing or exporting to PDF, so it is a nice option to read long form contents offline. + +

Cooperative (jump-out)

+ +

Njump is not interested into "capture" users at all, on the contrary it invites them to "jump" to the nostr resource with one of the proposed clients. It even remembers the most used one and put it on the top for fast click/tap.

+ +

Search engine friendly (jump-in)

+ +

This is crucial: njump pages are static so search engines can index them, these means that njump can help others to discover great content on nostr, jump in and join us! Njump is the only nostr resource that has this explicit goal, if you care that a good note could be found online use njump to share it, this way you also help nostr flourish.

+ +

Bonus: NIP-5 profiles

+ + Now you can share your own profile with an pretty NIP-05 inspired permalink: njump.me/p/<nip-5>, example: https://njump.me/p/fiatjaf.com + A profile shows the basic metadata infos, the used "outbox" relays (Gossip model) and the last notes. + Of course profiles are also static, fast and indexable, so start to promote your nostr presence this way! + +

Bonus 2: relays

+ + You can have a view of the last content posted to a relay using njump.me/r/<relay-host>, example: https://njump.me/r/nostr.wine + Some basic infos (NIP-11) are available; I 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. + +

Bonus 3: Inspector tool

+ + You know, we are all devs including our moms, so for every njump resource you can toggle the "Show more details" switch and inspect the full event's json; without installing other tools, like nak, this is probably the fastest way to obtain it. +
+
+
+ + {{template "footer.html"}} + + + + diff --git a/templates/scripts.js b/templates/scripts.js index bcff247..253cfa7 100644 --- a/templates/scripts.js +++ b/templates/scripts.js @@ -43,15 +43,17 @@ for (let i = 0; i < jsons.length; i++) { } const shareButton = document.querySelector('.open-list') -const clients_list = document.querySelector('.column_clients') -shareButton.addEventListener('click', function () { - clients_list.classList.toggle('up') - if (clients_list.classList.contains('up')) { - document.body.classList.add('lock') - } else { - document.body.classList.remove('lock') - } -}) +if (shareButton) { + const clients_list = document.querySelector('.column_clients') + shareButton.addEventListener('click', function () { + clients_list.classList.toggle('up') + if (clients_list.classList.contains('up')) { + document.body.classList.add('lock') + } else { + document.body.classList.remove('lock') + } + }) +} function updateAdvanceSwitch() { advanced_list.forEach(element => { @@ -125,18 +127,20 @@ document.addEventListener('DOMContentLoaded', function () { }) const desktop_name = document.querySelector('.column_content .name'); -window.addEventListener('scroll', function() { - desktop_profile = document.querySelector('.column_content .info-wrapper'); - if (window.getComputedStyle(desktop_profile).display === 'none') { - return - } - columnA = document.querySelector('.columnA') - if (columnA != null && isElementInViewport(desktop_name)) { - columnA.querySelector('.info-wrapper').style.display = 'none'; - } else { - document.querySelector('.info-wrapper').style.display = 'block'; - } -}); +if (desktop_name) { + window.addEventListener('scroll', function() { + desktop_profile = document.querySelector('.column_content .info-wrapper'); + if (window.getComputedStyle(desktop_profile).display === 'none') { + return + } + columnA = document.querySelector('.columnA') + if (columnA != null && isElementInViewport(desktop_name)) { + columnA.querySelector('.info-wrapper').style.display = 'none'; + } else { + document.querySelector('.info-wrapper').style.display = 'block'; + } + }); +} // Needed to apply proper print styles if (