13 changed files with 440 additions and 82 deletions
@ -0,0 +1,44 @@ |
|||||||
|
package nostr |
||||||
|
|
||||||
|
// Standard Nostr event kinds
|
||||||
|
// These are protocol-level constants and should not be configurable
|
||||||
|
const ( |
||||||
|
// KindProfile is kind 0 - user profile/metadata
|
||||||
|
KindProfile = 0 |
||||||
|
|
||||||
|
// KindNote is kind 1 - regular notes/text posts
|
||||||
|
KindNote = 1 |
||||||
|
|
||||||
|
// KindWiki is kind 30818 - wiki pages (NIP-54)
|
||||||
|
KindWiki = 30818 |
||||||
|
|
||||||
|
// KindBlog is kind 30041 - blog articles
|
||||||
|
KindBlog = 30041 |
||||||
|
|
||||||
|
// KindLongform is kind 30023 - longform markdown articles
|
||||||
|
KindLongform = 30023 |
||||||
|
|
||||||
|
// KindIndex is kind 30040 - publication index events (NKBIP-01)
|
||||||
|
KindIndex = 30040 |
||||||
|
|
||||||
|
// KindIssue is kind 1621 - issue events
|
||||||
|
KindIssue = 1621 |
||||||
|
|
||||||
|
// KindRepoAnnouncement is kind 30617 - repository announcement events
|
||||||
|
KindRepoAnnouncement = 30617 |
||||||
|
) |
||||||
|
|
||||||
|
// SupportedWikiKinds returns the list of supported wiki kinds
|
||||||
|
func SupportedWikiKinds() []int { |
||||||
|
return []int{KindWiki} |
||||||
|
} |
||||||
|
|
||||||
|
// SupportedBlogKinds returns the list of supported blog kinds
|
||||||
|
func SupportedBlogKinds() []int { |
||||||
|
return []int{KindBlog} |
||||||
|
} |
||||||
|
|
||||||
|
// SupportedArticleKinds returns all supported article kinds (wiki + blog)
|
||||||
|
func SupportedArticleKinds() []int { |
||||||
|
return []int{KindWiki, KindBlog} |
||||||
|
} |
||||||
@ -1,8 +1,6 @@ |
|||||||
{ |
{ |
||||||
"dependencies": { |
"dependencies": { |
||||||
"@asciidoctor/core": "^3.0.4" |
"@asciidoctor/core": "^3.0.4", |
||||||
}, |
"marked": "^12.0.0" |
||||||
"devDependencies": { |
|
||||||
"lucide": "^0.564.0" |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,130 @@ |
|||||||
|
{{define "content"}} |
||||||
|
<div class="blog-layout"> |
||||||
|
<!-- Left Sidebar --> |
||||||
|
<aside class="blog-sidebar"> |
||||||
|
<div class="blog-header"> |
||||||
|
<h1 class="blog-title">Articles</h1> |
||||||
|
<p class="blog-description">Longform markdown articles</p> |
||||||
|
<div class="blog-tags"> |
||||||
|
<span class="tag">#articles</span> |
||||||
|
<span class="tag">#longform</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<nav class="blog-nav" aria-label="Articles"> |
||||||
|
<ul class="article-menu"> |
||||||
|
{{range $index, $item := .ArticleItems}} |
||||||
|
<li> |
||||||
|
<a href="#" class="article-link" data-dtag="{{$item.DTag}}" data-index="{{$index}}"{{if eq $index 0}} data-active="true"{{end}}> |
||||||
|
<div class="article-link-title"><span class="icon-inline">{{icon "file-text"}}</span> {{$item.Title}}</div> |
||||||
|
{{if $item.Time}} |
||||||
|
<div class="article-link-meta"> |
||||||
|
<span class="article-date"><span class="icon-inline">{{icon "clock"}}</span> {{$item.Time}}</span> |
||||||
|
{{if $item.Author}} |
||||||
|
<span class="article-author">{{template "user-badge-simple" (dict "Pubkey" $item.Author "Profiles" $.Profiles)}}</span> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{{end}} |
||||||
|
</ul> |
||||||
|
</nav> |
||||||
|
</aside> |
||||||
|
|
||||||
|
<!-- Right Content Pane --> |
||||||
|
<main class="blog-content"> |
||||||
|
{{range $index, $item := .ArticleItems}} |
||||||
|
<article class="blog-article{{if eq $index 0}} active{{end}}" data-dtag="{{$item.DTag}}" id="article-{{$item.DTag}}"> |
||||||
|
<header class="article-header"> |
||||||
|
<h1 class="article-title">{{$item.Title}}</h1> |
||||||
|
<p class="article-subtitle">Longform article</p> |
||||||
|
</header> |
||||||
|
{{if $item.Summary}}<p class="article-summary">{{$item.Summary}}</p>{{end}} |
||||||
|
<div class="article-content"> |
||||||
|
{{$item.Content}} |
||||||
|
</div> |
||||||
|
</article> |
||||||
|
{{else}} |
||||||
|
<article class="blog-article active"> |
||||||
|
<header class="article-header"> |
||||||
|
<h1 class="article-title"><span class="icon-inline">{{icon "file-x"}}</span> No Articles</h1> |
||||||
|
</header> |
||||||
|
<div class="article-content"> |
||||||
|
<p><span class="icon-inline">{{icon "inbox"}}</span> No articles available yet.</p> |
||||||
|
</div> |
||||||
|
</article> |
||||||
|
{{end}} |
||||||
|
</main> |
||||||
|
</div> |
||||||
|
|
||||||
|
<script> |
||||||
|
(function() { |
||||||
|
const articleLinks = document.querySelectorAll('.article-link'); |
||||||
|
const articles = document.querySelectorAll('.blog-article'); |
||||||
|
|
||||||
|
function showArticle(dtag) { |
||||||
|
// Hide all articles |
||||||
|
articles.forEach(article => { |
||||||
|
article.classList.remove('active'); |
||||||
|
}); |
||||||
|
|
||||||
|
// Show selected article |
||||||
|
const targetArticle = document.querySelector(`.blog-article[data-dtag="${dtag}"]`); |
||||||
|
if (targetArticle) { |
||||||
|
targetArticle.classList.add('active'); |
||||||
|
} |
||||||
|
|
||||||
|
// Update active link |
||||||
|
articleLinks.forEach(link => { |
||||||
|
if (link.dataset.dtag === dtag) { |
||||||
|
link.setAttribute('data-active', 'true'); |
||||||
|
} else { |
||||||
|
link.removeAttribute('data-active'); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Update URL hash without scrolling |
||||||
|
if (history.pushState) { |
||||||
|
history.pushState(null, null, `#${dtag}`); |
||||||
|
} else { |
||||||
|
window.location.hash = dtag; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Handle link clicks |
||||||
|
articleLinks.forEach(link => { |
||||||
|
link.addEventListener('click', function(e) { |
||||||
|
e.preventDefault(); |
||||||
|
const dtag = this.dataset.dtag; |
||||||
|
showArticle(dtag); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
// Handle initial hash on page load |
||||||
|
if (window.location.hash) { |
||||||
|
const hash = window.location.hash.substring(1); |
||||||
|
const targetLink = document.querySelector(`.article-link[data-dtag="${hash}"]`); |
||||||
|
if (targetLink) { |
||||||
|
showArticle(hash); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Handle browser back/forward |
||||||
|
window.addEventListener('popstate', function() { |
||||||
|
const hash = window.location.hash.substring(1); |
||||||
|
if (hash) { |
||||||
|
showArticle(hash); |
||||||
|
} else { |
||||||
|
// Show first article if no hash |
||||||
|
const firstLink = document.querySelector('.article-link'); |
||||||
|
if (firstLink) { |
||||||
|
showArticle(firstLink.dataset.dtag); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
})(); |
||||||
|
</script> |
||||||
|
{{end}} |
||||||
|
|
||||||
|
{{/* Feed is defined in components.html */}} |
||||||
Loading…
Reference in new issue