Browse Source

bug-fixes

master
Silberengel 4 weeks ago
parent
commit
8635e0d41d
  1. 85
      internal/generator/html.go
  2. 30
      internal/nostr/profile.go
  3. 144
      static/css/main.css
  4. 23
      templates/base.html
  5. 4
      templates/blog.html
  6. 51
      templates/components.html
  7. 2
      templates/ebooks.html

85
internal/generator/html.go

@ -12,6 +12,26 @@ import (
"gitcitadel-online/internal/nostr" "gitcitadel-online/internal/nostr"
) )
// getTemplateFuncs returns the common template functions
func getTemplateFuncs() template.FuncMap {
return template.FuncMap{
"year": func() int { return time.Now().Year() },
"json": func(v interface{}) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
},
"hasPrefix": func(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
},
"shortenPubkey": func(pubkey string) string {
return nostr.ShortenPubkey(pubkey)
},
}
}
// HTMLGenerator generates HTML pages from wiki events // HTMLGenerator generates HTML pages from wiki events
type HTMLGenerator struct { type HTMLGenerator struct {
templates *template.Template templates *template.Template
@ -86,21 +106,18 @@ type EBookInfo struct {
TimeISO string // ISO time TimeISO string // ISO time
} }
// UserBadgeInfo represents user badge data for display
type UserBadgeInfo struct {
Pubkey string
Picture string
DisplayName string
Name string
ShortNpub string
}
// NewHTMLGenerator creates a new HTML generator // NewHTMLGenerator creates a new HTML generator
func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaultImage string) (*HTMLGenerator, error) { func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaultImage string) (*HTMLGenerator, error) {
tmpl := template.New("base").Funcs(template.FuncMap{ tmpl := template.New("base").Funcs(getTemplateFuncs())
"year": func() int { return time.Now().Year() },
"json": func(v interface{}) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
},
"hasPrefix": func(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
},
})
// Load all templates // Load all templates
templateFiles := []string{ templateFiles := []string{
@ -246,19 +263,7 @@ func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems
} }
// Use renderTemplate but with custom data // Use renderTemplate but with custom data
renderTmpl := template.New("blog-render").Funcs(template.FuncMap{ renderTmpl := template.New("blog-render").Funcs(getTemplateFuncs())
"year": func() int { return time.Now().Year() },
"json": func(v interface{}) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
},
"hasPrefix": func(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
},
})
files := []string{ files := []string{
filepath.Join(g.templateDir, "components.html"), filepath.Join(g.templateDir, "components.html"),
@ -392,19 +397,7 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
} }
// Parse base.html together with contact.html to ensure correct blocks are used // Parse base.html together with contact.html to ensure correct blocks are used
renderTmpl := template.New("render").Funcs(template.FuncMap{ renderTmpl := template.New("render").Funcs(getTemplateFuncs())
"year": func() int { return time.Now().Year() },
"json": func(v interface{}) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
},
"hasPrefix": func(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
},
})
// Parse base.html, components.html, and contact.html together // Parse base.html, components.html, and contact.html together
files := []string{ files := []string{
@ -515,19 +508,7 @@ func (g *HTMLGenerator) GenerateErrorPage(statusCode int, feedItems []FeedItemIn
func (g *HTMLGenerator) renderTemplate(templateName string, data PageData) (string, error) { func (g *HTMLGenerator) renderTemplate(templateName string, data PageData) (string, error) {
// Parse base.html together with the specific template to ensure correct blocks are used // Parse base.html together with the specific template to ensure correct blocks are used
// This avoids the issue where all templates are parsed together and the last "content" block wins // This avoids the issue where all templates are parsed together and the last "content" block wins
renderTmpl := template.New("render").Funcs(template.FuncMap{ renderTmpl := template.New("render").Funcs(getTemplateFuncs())
"year": func() int { return time.Now().Year() },
"json": func(v interface{}) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
},
"hasPrefix": func(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
},
})
// Parse base.html, components.html, and the specific template together // Parse base.html, components.html, and the specific template together
// This ensures the correct "content" block and reusable components are available // This ensures the correct "content" block and reusable components are available

30
internal/nostr/profile.go

@ -10,6 +10,36 @@ import (
"github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip19"
) )
// ShortenNpub shortens an npub to npub1...xyz format (first 8 + last 4 characters)
func ShortenNpub(npub string) string {
if len(npub) <= 12 {
return npub
}
return npub[:8] + "..." + npub[len(npub)-4:]
}
// PubkeyToNpub converts a hex pubkey to npub format
func PubkeyToNpub(pubkey string) (string, error) {
npub, err := nip19.EncodePublicKey(pubkey)
if err != nil {
return "", fmt.Errorf("failed to encode pubkey to npub: %w", err)
}
return npub, nil
}
// ShortenPubkey converts a pubkey to shortened npub format
func ShortenPubkey(pubkey string) string {
npub, err := PubkeyToNpub(pubkey)
if err != nil {
// Fallback: shorten hex pubkey
if len(pubkey) > 12 {
return pubkey[:8] + "..." + pubkey[len(pubkey)-4:]
}
return pubkey
}
return ShortenNpub(npub)
}
// Profile represents a parsed kind 0 profile event // Profile represents a parsed kind 0 profile event
type Profile struct { type Profile struct {
Event *nostr.Event Event *nostr.Event

144
static/css/main.css

@ -3,14 +3,16 @@
:root { :root {
--bg-primary: #2d2d2d; --bg-primary: #2d2d2d;
--bg-secondary: #1e1e1e; --bg-secondary: #1e1e1e;
--text-primary: #e8e8e8; --text-primary: #f0f0f0; /* Improved contrast from #e8e8e8 */
--text-secondary: #b8b8b8; --text-secondary: #c0c0c0; /* Improved contrast from #b8b8b8 */
--link-color: #7c9eff; --link-color: #7c9eff;
--link-hover: #9bb3ff; --link-hover: #9bb3ff;
--link-visited: #a58fff; --link-visited: #a58fff;
--accent-color: #7c9eff; --accent-color: #7c9eff;
--border-color: #404040; --border-color: #404040;
--focus-color: #9bb3ff; --focus-color: #9bb3ff;
--focus-outline: 3px solid var(--focus-color); /* Thicker focus outline */
--focus-offset: 3px; /* More visible offset */
--error-color: #ff6b6b; --error-color: #ff6b6b;
} }
@ -113,11 +115,14 @@ header {
transition: color 0.2s; transition: color 0.2s;
} }
.nav-menu a:hover, .nav-menu a:hover {
.nav-menu a:focus {
color: var(--link-hover); color: var(--link-hover);
outline: 2px solid var(--focus-color); }
outline-offset: 2px;
.nav-menu a:focus-visible {
color: var(--link-hover);
outline: var(--focus-outline);
outline-offset: var(--focus-offset);
} }
.mobile-menu-toggle { .mobile-menu-toggle {
@ -125,9 +130,16 @@ header {
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
background: transparent; background: transparent;
border: none; border: 2px solid transparent;
cursor: pointer; cursor: pointer;
padding: 8px; padding: 8px;
border-radius: 4px;
}
.mobile-menu-toggle:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-offset);
border-color: var(--focus-color);
} }
.mobile-menu-toggle span { .mobile-menu-toggle span {
@ -184,6 +196,9 @@ header {
.wiki-layout { .wiki-layout {
/* Three-column layout for wiki pages: wiki-sidebar | main-content | feed-sidebar */ /* Three-column layout for wiki pages: wiki-sidebar | main-content | feed-sidebar */
/* Layout handled by flexbox in .layout-container */
/* No additional styles needed - flexbox handles the layout */
display: flex;
} }
.wiki-sidebar { .wiki-sidebar {
@ -230,9 +245,9 @@ header {
color: var(--link-hover); color: var(--link-hover);
} }
.wiki-menu a:focus { .wiki-menu a:focus-visible {
outline: 2px solid var(--focus-color); outline: var(--focus-outline);
outline-offset: 2px; outline-offset: var(--focus-offset);
} }
.wiki-menu a.active { .wiki-menu a.active {
@ -261,12 +276,15 @@ h1, h2, h3, h4, h5, h6 {
color: var(--text-primary); color: var(--text-primary);
line-height: 1.2; line-height: 1.2;
margin-bottom: 1rem; margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
} }
h1 { font-size: 2.5rem; } h1 { font-size: 1.5rem; }
h2 { font-size: 2rem; } h2 { font-size: 1.25rem; }
h3 { font-size: 1.5rem; } h3 { font-size: 1.1rem; }
h4 { font-size: 1.25rem; } h4 { font-size: 1rem; }
p { p {
margin-bottom: 1rem; margin-bottom: 1rem;
@ -322,8 +340,17 @@ a:focus {
} }
.btn:focus { .btn:focus {
outline: 2px solid var(--focus-color); outline: var(--focus-outline);
outline-offset: 2px; outline-offset: var(--focus-offset);
}
.btn:focus:not(:focus-visible) {
outline: none;
}
.btn:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-offset);
} }
/* Articles and Pages */ /* Articles and Pages */
@ -732,19 +759,25 @@ li {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
/* Focus styles for accessibility */ /* Focus styles for accessibility - only show for keyboard navigation */
*:focus { *:focus:not(:focus-visible) {
outline: 2px solid var(--focus-color); outline: none;
outline-offset: 2px;
} }
button:focus, *:focus-visible {
a:focus, outline: var(--focus-outline);
input:focus, outline-offset: var(--focus-offset);
select:focus, border-radius: 2px;
textarea:focus { }
outline: 2px solid var(--focus-color);
outline-offset: 2px; button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-offset);
border-radius: 2px;
} }
/* Contact Form Styles */ /* Contact Form Styles */
@ -789,7 +822,13 @@ textarea:focus {
.form-group input[type="text"]:focus, .form-group input[type="text"]:focus,
.form-group textarea:focus { .form-group textarea:focus {
border-color: var(--focus-color); border-color: var(--focus-color);
outline: none; }
.form-group input[type="text"]:focus-visible,
.form-group textarea:focus-visible {
border-color: var(--focus-color);
outline: var(--focus-outline);
outline-offset: var(--focus-offset);
} }
.form-group textarea { .form-group textarea {
@ -972,6 +1011,55 @@ textarea:focus {
flex-shrink: 0; flex-shrink: 0;
} }
/* User Badge Styles */
.user-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.user-badge-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.user-badge-avatar-placeholder {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.user-badge-avatar-placeholder .icon-inline {
width: 14px;
height: 14px;
margin: 0;
color: var(--text-secondary);
}
.user-badge-name {
font-weight: 500;
color: var(--text-primary);
font-size: 0.9rem;
}
.user-badge-handle {
color: var(--text-secondary);
font-size: 0.85rem;
}
/* Ensure icons align properly in flex containers */ /* Ensure icons align properly in flex containers */
a, button, span, p, h1, h2, h3, h4, h5, h6, li { a, button, span, p, h1, h2, h3, h4, h5, h6, li {
display: inline-flex; display: inline-flex;

23
templates/base.html

@ -67,17 +67,7 @@
<div class="layout-container{{if and .WikiPages (or (eq .CanonicalURL (printf "%s/wiki" .SiteURL)) (hasPrefix .CanonicalURL (printf "%s/wiki/" .SiteURL)))}} wiki-layout{{end}}"> <div class="layout-container{{if and .WikiPages (or (eq .CanonicalURL (printf "%s/wiki" .SiteURL)) (hasPrefix .CanonicalURL (printf "%s/wiki/" .SiteURL)))}} wiki-layout{{end}}">
{{if and .WikiPages (or (eq .CanonicalURL (printf "%s/wiki" .SiteURL)) (hasPrefix .CanonicalURL (printf "%s/wiki/" .SiteURL)))}} {{if and .WikiPages (or (eq .CanonicalURL (printf "%s/wiki" .SiteURL)) (hasPrefix .CanonicalURL (printf "%s/wiki/" .SiteURL)))}}
<aside class="wiki-sidebar" aria-label="About The Project navigation"> {{template "wiki-sidebar" .}}
<nav class="wiki-nav">
<h2><i data-lucide="book-open" class="icon-inline"></i> About The Project</h2>
<ul class="wiki-menu">
<li><a href="/wiki"{{if eq .CanonicalURL (printf "%s/wiki" .SiteURL)}} class="active"{{end}}><i data-lucide="info" class="icon-inline"></i> Project Overview</a></li>
{{range .WikiPages}}
<li><a href="/wiki/{{.DTag}}"{{if eq $.CanonicalURL (printf "%s/wiki/%s" $.SiteURL .DTag)}} class="active"{{end}}><i data-lucide="file-text" class="icon-inline"></i> {{.Title}}</a></li>
{{end}}
</ul>
</nav>
</aside>
{{end}} {{end}}
<main id="main-content" class="main-content"> <main id="main-content" class="main-content">
@ -92,14 +82,17 @@
</div> </div>
<footer> <footer>
<p><i data-lucide="copyright" class="icon-inline"></i> 2026 GitCitadel All Rights Reserved | <a href="/contact"><i data-lucide="mail" class="icon-inline"></i> Contact</a></p> <p><i data-lucide="copyright" class="icon-inline"></i> 2026 GitCitadel LLC. All Rights Reserved | <a href="/contact"><i data-lucide="mail" class="icon-inline"></i> Contact</a></p>
</footer> </footer>
<script> <script>
// Initialize Lucide icons // Initialize Lucide icons
function initIcons() {
if (typeof lucide !== 'undefined') { if (typeof lucide !== 'undefined') {
lucide.createIcons(); lucide.createIcons();
} }
}
initIcons();
// Mobile menu toggle (optional - menu works without JS) // Mobile menu toggle (optional - menu works without JS)
if (document.querySelector('.mobile-menu-toggle')) { if (document.querySelector('.mobile-menu-toggle')) {
@ -110,6 +103,12 @@
menu.classList.toggle('active'); menu.classList.toggle('active');
}); });
} }
// Reinitialize icons when DOM changes (for dynamically added content)
const observer = new MutationObserver(function(mutations) {
initIcons();
});
observer.observe(document.body, { childList: true, subtree: true });
</script> </script>
</body> </body>
</html> </html>

4
templates/blog.html

@ -4,7 +4,7 @@
<aside class="blog-sidebar"> <aside class="blog-sidebar">
<div class="blog-header"> <div class="blog-header">
{{if .BlogIndexAuthor}} {{if .BlogIndexAuthor}}
<div class="blog-author-handle"><i data-lucide="user" class="icon-inline"></i> @{{.BlogIndexAuthor}}</div> <div class="blog-author-handle">{{template "user-badge-simple" .BlogIndexAuthor}}</div>
{{end}} {{end}}
{{if .BlogIndexImage}} {{if .BlogIndexImage}}
<div class="blog-image"> <div class="blog-image">
@ -34,7 +34,7 @@
<div class="article-link-meta"> <div class="article-link-meta">
<span class="article-date"><i data-lucide="clock" class="icon-inline"></i> {{$item.Time}}</span> <span class="article-date"><i data-lucide="clock" class="icon-inline"></i> {{$item.Time}}</span>
{{if $item.Author}} {{if $item.Author}}
<span class="article-author"><i data-lucide="user" class="icon-inline"></i> {{$item.Author}}</span> <span class="article-author">{{template "user-badge-simple" $item.Author}}</span>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}

51
templates/components.html

@ -6,7 +6,7 @@
{{range .FeedItems}} {{range .FeedItems}}
<article class="feed-item"> <article class="feed-item">
<header class="feed-header"> <header class="feed-header">
<span class="feed-author"><i data-lucide="user" class="icon-inline"></i> {{.Author}}</span> <span class="feed-author">{{template "user-badge-simple" .Author}}</span>
<time class="feed-time" datetime="{{.TimeISO}}"><i data-lucide="clock" class="icon-inline"></i> {{.Time}}</time> <time class="feed-time" datetime="{{.TimeISO}}"><i data-lucide="clock" class="icon-inline"></i> {{.Time}}</time>
</header> </header>
<div class="feed-content">{{.Content}}</div> <div class="feed-content">{{.Content}}</div>
@ -34,3 +34,52 @@
<strong><i data-lucide="alert-circle" class="icon-inline"></i> Error:</strong> {{.}} <strong><i data-lucide="alert-circle" class="icon-inline"></i> Error:</strong> {{.}}
</div> </div>
{{end}} {{end}}
{{/* Wiki Sidebar Component - Reusable wiki navigation */}}
{{define "wiki-sidebar"}}
<aside class="wiki-sidebar" aria-label="About The Project navigation">
<nav class="wiki-nav">
<h2><i data-lucide="book-open" class="icon-inline"></i> About The Project</h2>
<ul class="wiki-menu">
<li><a href="/wiki"{{if eq .CanonicalURL (printf "%s/wiki" .SiteURL)}} class="active"{{end}}><i data-lucide="info" class="icon-inline"></i> Project Overview</a></li>
{{range .WikiPages}}
<li><a href="/wiki/{{.DTag}}"{{if eq $.CanonicalURL (printf "%s/wiki/%s" $.SiteURL .DTag)}} class="active"{{end}}><i data-lucide="file-text" class="icon-inline"></i> {{.Title}}</a></li>
{{end}}
</ul>
</nav>
</aside>
{{end}}
{{/* User Badge Component - Displays user profile with picture, name, or shortened npub
Usage: {{template "user-badge" (dict "Pubkey" .Author "Picture" "" "DisplayName" "" "Name" "")}}
Or simpler: {{template "user-badge-simple" .Author}} for just pubkey
*/}}
{{define "user-badge"}}
<span class="user-badge" title="{{.Pubkey}}">
{{if .Picture}}
<img src="{{.Picture}}" alt="{{if .DisplayName}}{{.DisplayName}}{{else if .Name}}{{.Name}}{{else}}User{{end}}" class="user-badge-avatar" loading="lazy">
{{else}}
<span class="user-badge-avatar-placeholder"><i data-lucide="user" class="icon-inline"></i></span>
{{end}}
<span class="user-badge-name">
{{if .DisplayName}}
{{.DisplayName}}
{{else if .Name}}
{{.Name}}
{{else}}
{{shortenPubkey .Pubkey}}
{{end}}
</span>
{{if and .Name (not .DisplayName)}}
<span class="user-badge-handle">@{{.Name}}</span>
{{end}}
</span>
{{end}}
{{/* Simple user badge - just takes a pubkey string */}}
{{define "user-badge-simple"}}
<span class="user-badge" title="{{.}}">
<span class="user-badge-avatar-placeholder"><i data-lucide="user" class="icon-inline"></i></span>
<span class="user-badge-name">{{shortenPubkey .}}</span>
</span>
{{end}}

2
templates/ebooks.html

@ -23,7 +23,7 @@
<strong>{{.Title}}</strong> <strong>{{.Title}}</strong>
{{if .Summary}}<br><small class="text-muted">{{.Summary}}</small>{{end}} {{if .Summary}}<br><small class="text-muted">{{.Summary}}</small>{{end}}
</td> </td>
<td><code class="pubkey">{{.Author}}</code></td> <td>{{template "user-badge-simple" .Author}}</td>
<td>{{if .Type}}{{.Type}}{{else}}—{{end}}</td> <td>{{if .Type}}{{.Type}}{{else}}—{{end}}</td>
<td> <td>
<time datetime="{{.TimeISO}}">{{.Time}}</time> <time datetime="{{.TimeISO}}">{{.Time}}</time>

Loading…
Cancel
Save