From d99250b92a6c1bd624d4d50df2e9344739611c36 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 16 Feb 2026 11:48:28 +0100 Subject: [PATCH] bug-fixes --- cmd/server/main.go | 8 +- internal/nostr/ebooks.go | 9 + internal/nostr/events.go | 9 + internal/nostr/feed.go | 20 ++ internal/server/handlers.go | 23 ++ internal/server/server.go | 4 +- static/css/main.css | 62 +++-- static/css/responsive.css | 469 +++++++++++++++++++++++++++++++++++- templates/base.html | 33 ++- templates/landing.html | 48 ++-- templates/page.html | 8 - 11 files changed, 620 insertions(+), 73 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index da7dea9..d645093 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -44,6 +44,12 @@ func main() { pageCache := cache.NewCache() feedCache := cache.NewFeedCache() + // Initialize media cache + mediaCache, err := cache.NewMediaCache("cache/media") + if err != nil { + logger.Fatalf("Failed to initialize media cache: %v", err) + } + // Initialize Nostr client nostrClient := nostr.NewClient(cfg.Relays.Primary, cfg.Relays.Fallback, cfg.Relays.AdditionalFallback) ctx := context.Background() @@ -110,7 +116,7 @@ func main() { rewarmer.Start(ctx) // Initialize HTTP server - httpServer := server.NewServer(cfg.Server.Port, pageCache, feedCache, issueService, cfg.RepoAnnouncement, htmlGenerator, nostrClient) + httpServer := server.NewServer(cfg.Server.Port, pageCache, feedCache, mediaCache, issueService, cfg.RepoAnnouncement, htmlGenerator, nostrClient) // Start server in goroutine go func() { diff --git a/internal/nostr/ebooks.go b/internal/nostr/ebooks.go index c5748cc..48aa36b 100644 --- a/internal/nostr/ebooks.go +++ b/internal/nostr/ebooks.go @@ -35,6 +35,7 @@ type EBookInfo struct { Type string CreatedAt int64 Naddr string + Image string } // FetchTopLevelIndexEvents fetches all top-level 30040 events from the specified relay @@ -146,6 +147,14 @@ func (es *EBooksService) FetchTopLevelIndexEvents(ctx context.Context) ([]EBookI } } + // Extract image + for _, tag := range event.Tags { + if len(tag) > 0 && tag[0] == "image" && len(tag) > 1 { + ebook.Image = tag[1] + break + } + } + // Build naddr - create proper bech32-encoded naddr // Extract relay hints from event tags if available var relays []string diff --git a/internal/nostr/events.go b/internal/nostr/events.go index 4c5ac5e..67ffe68 100644 --- a/internal/nostr/events.go +++ b/internal/nostr/events.go @@ -136,6 +136,7 @@ type WikiEvent struct { Title string Summary string Content string + Image string } // ParseWikiEvent parses a wiki event according to NIP-54 @@ -176,6 +177,14 @@ func ParseWikiEvent(event *nostr.Event, expectedKind int) (*WikiEvent, error) { } } + // Extract image tag (optional) + for _, tag := range event.Tags { + if len(tag) > 0 && tag[0] == "image" && len(tag) > 1 { + wiki.Image = tag[1] + break + } + } + return wiki, nil } diff --git a/internal/nostr/feed.go b/internal/nostr/feed.go index 45b8ec3..b5c2e44 100644 --- a/internal/nostr/feed.go +++ b/internal/nostr/feed.go @@ -55,11 +55,27 @@ func (fs *FeedService) FetchFeedItems(ctx context.Context, feedRelay string, max for incomingEvent := range eventChan { if incomingEvent.Event != nil { item := FeedItem{ + EventID: incomingEvent.Event.ID, Author: incomingEvent.Event.PubKey, Content: incomingEvent.Event.Content, Time: time.Unix(int64(incomingEvent.Event.CreatedAt), 0), Link: fmt.Sprintf("https://alexandria.gitcitadel.eu/events?id=nevent1%s", incomingEvent.Event.ID), } + + // Extract title, summary, and image tags + for _, tag := range incomingEvent.Event.Tags { + if len(tag) > 0 && len(tag) > 1 { + switch tag[0] { + case "title": + item.Title = tag[1] + case "summary": + item.Summary = tag[1] + case "image": + item.Image = tag[1] + } + } + } + items = append(items, item) logger.WithFields(map[string]interface{}{ "relay": incomingEvent.Relay.URL, @@ -77,8 +93,12 @@ func (fs *FeedService) FetchFeedItems(ctx context.Context, feedRelay string, max // FeedItem represents a feed item type FeedItem struct { + EventID string Author string Content string Time time.Time Link string + Title string + Summary string + Image string } diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 00e494b..4f5df86 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "path/filepath" "strings" "time" @@ -21,6 +22,9 @@ func (s *Server) setupRoutes(mux *http.ServeMux) { // Static files mux.HandleFunc("/static/", s.handleStatic) + // Media cache + mux.HandleFunc("/cache/media/", s.handleMediaCache) + // Favicon mux.HandleFunc("/favicon.ico", s.handleFavicon) @@ -316,6 +320,25 @@ func (s *Server) handleFavicon(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/GitCitadel_Icon_Black.svg") } +// handleMediaCache serves cached media files +func (s *Server) handleMediaCache(w http.ResponseWriter, r *http.Request) { + if s.mediaCache == nil { + http.Error(w, "Media cache not available", http.StatusServiceUnavailable) + return + } + + // Extract filename from path + filename := r.URL.Path[len("/cache/media/"):] + if filename == "" { + http.Error(w, "Invalid path", http.StatusBadRequest) + return + } + + // Serve file from cache directory + cachePath := filepath.Join(s.mediaCache.GetCacheDir(), filename) + http.ServeFile(w, r, cachePath) +} + // handleHealth handles health check requests func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { if s.cache.Size() == 0 { diff --git a/internal/server/server.go b/internal/server/server.go index 7b1873a..0002947 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -22,6 +22,7 @@ type Server struct { httpServer *http.Server cache *cache.Cache feedCache *cache.FeedCache + mediaCache *cache.MediaCache port int issueService IssueServiceInterface repoAnnouncement string @@ -43,10 +44,11 @@ type HTMLGeneratorInterface interface { } // NewServer creates a new HTTP server -func NewServer(port int, pageCache *cache.Cache, feedCache *cache.FeedCache, issueService IssueServiceInterface, repoAnnouncement string, htmlGenerator HTMLGeneratorInterface, nostrClient *nostr.Client) *Server { +func NewServer(port int, pageCache *cache.Cache, feedCache *cache.FeedCache, mediaCache *cache.MediaCache, issueService IssueServiceInterface, repoAnnouncement string, htmlGenerator HTMLGeneratorInterface, nostrClient *nostr.Client) *Server { s := &Server{ cache: pageCache, feedCache: feedCache, + mediaCache: mediaCache, port: port, issueService: issueService, repoAnnouncement: repoAnnouncement, diff --git a/static/css/main.css b/static/css/main.css index a4b902f..4f624fc 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -128,19 +128,6 @@ header { align-items: center; } -.nav-separator { - color: var(--text-secondary); - padding: 0 0.5rem; - user-select: none; -} - -.nav-section-label { - color: var(--text-secondary); - font-weight: 600; - font-size: 0.9em; - padding: 0 0.25rem; - user-select: none; -} .nav-menu a { color: var(--text-primary); @@ -192,6 +179,22 @@ header { position: relative; } +.dropdown-toggle { + cursor: pointer; +} + +.dropdown-arrow { + margin-left: 0.25rem; + font-size: 0.7em; + transition: transform 0.2s; + display: inline-block; +} + +.dropdown:hover .dropdown-toggle .dropdown-arrow, +.dropdown:focus-within .dropdown-toggle .dropdown-arrow { + transform: rotate(180deg); +} + .dropdown-menu { position: absolute; top: 100%; @@ -202,7 +205,7 @@ header { list-style: none; min-width: 200px; padding: 0.5rem 0; - margin: 0; + margin: 0.5rem 0 0 0; opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; @@ -223,6 +226,14 @@ header { .dropdown-menu a { display: block; padding: 0.5rem 1rem; + color: var(--text-primary); + text-decoration: none; + transition: background 0.2s, color 0.2s; +} + +.dropdown-menu a:hover { + background: var(--bg-primary); + color: var(--link-hover); } /* Layout */ @@ -602,6 +613,7 @@ a:focus { line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; + line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } @@ -1257,13 +1269,29 @@ textarea:focus-visible { } .btn-secondary { - background: var(--bg-secondary); - color: var(--text-primary); + background: var(--bg-primary); + color: var(--text-primary) !important; border: 1px solid var(--border-color); } +.btn-secondary *, +.btn-secondary span, +.btn-secondary .icon-inline, +.btn-secondary svg { + color: var(--text-primary) !important; +} + .btn-secondary:hover { - background: #2a2a2a; + background: #3a3a3a; + border-color: var(--accent-color); + color: var(--text-primary) !important; +} + +.btn-secondary:hover *, +.btn-secondary:hover span, +.btn-secondary:hover .icon-inline, +.btn-secondary:hover svg { + color: var(--text-primary) !important; } /* Alert Styles */ diff --git a/static/css/responsive.css b/static/css/responsive.css index f1245ea..b60aed9 100644 --- a/static/css/responsive.css +++ b/static/css/responsive.css @@ -2,6 +2,7 @@ /* Mobile styles (default, < 768px) */ @media (max-width: 767px) { + /* Layout */ .feed-sidebar { display: none; } @@ -27,6 +28,7 @@ word-wrap: break-word; } + /* Navigation */ .mobile-menu-toggle { display: flex; } @@ -45,6 +47,7 @@ border-top: 1px solid var(--border-color); z-index: 999; box-shadow: 2px 0 8px rgba(0, 0, 0, 0.2); + overflow-y: auto; } .nav-menu.active { @@ -64,25 +67,272 @@ box-shadow: none; margin-left: 1rem; margin-top: 0.5rem; + margin-bottom: 0.5rem; } + .dropdown-toggle .dropdown-arrow { + transform: none !important; + } + + /* Typography */ h1 { - font-size: 2rem; + font-size: 1.75rem; + line-height: 1.2; } h2 { - font-size: 1.75rem; + font-size: 1.5rem; + line-height: 1.3; + } + + h3 { + font-size: 1.25rem; } + h4 { + font-size: 1.1rem; + } + + /* Spacing */ article { - padding: 1.5rem; + padding: 1rem; + margin-bottom: 1rem; } .nav-container { padding: 0.5rem 1rem; } + /* Landing Page */ + .landing-page { + padding: 1rem; + } + + .landing-page .hero { + padding: 1.5rem 0; + margin-bottom: 2rem; + } + + .landing-page .hero h1 { + font-size: 1.75rem; + margin-bottom: 0.75rem; + } + + .landing-page .hero .lead { + font-size: 1rem; + padding: 0 1rem; + } + + .landing-page .features { + margin-top: 2rem; + } + + .landing-page .features h2 { + font-size: 1.5rem; + margin-bottom: 1.5rem; + } + + .feature-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + margin-top: 1.5rem; + } + + .feature-card { + padding: 1.5rem; + } + + .feature-card h3 { + font-size: 1.1rem; + margin-bottom: 0.75rem; + } + + .feature-image-title { + font-size: 1rem; + } + + .feature-image-summary { + font-size: 0.85rem; + } + + .feature-image-overlay { + padding: 1rem 0.75rem 0.75rem; + } + + .feature-card .wiki-links { + gap: 0.5rem; + margin-bottom: 1rem; + } + + .feature-card .wiki-link { + padding: 0.4rem 0.75rem; + font-size: 0.85rem; + } + + /* Blog Layout */ + .blog-layout { + flex-direction: column; + padding: 1rem; + gap: 1.5rem; + } + + .blog-sidebar { + width: 100%; + position: static; + max-height: none; + overflow-y: visible; + padding: 1.5rem; + order: 2; + } + + .blog-content { + order: 1; + padding: 1.5rem; + } + + .blog-header { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + } + + .blog-title { + font-size: 1.5rem; + } + + .blog-image img { + max-width: 150px; + } + + .article-title { + font-size: 1.75rem; + line-height: 1.3; + } + + .article-summary { + font-size: 1rem; + padding: 0.75rem; + margin: 1rem 0; + } + + .article-link { + padding: 0.75rem; + } + + .article-link-title { + font-size: 0.95rem; + } + + .article-link-meta { + font-size: 0.8rem; + } + + /* Contact Page */ + .contact-page { + padding: 1rem; + } + + .contact-links { + padding: 1rem; + margin-bottom: 1.5rem; + } + + .contact-links h2 { + font-size: 1.25rem; + margin-bottom: 0.75rem; + } + + .nostr-profile { + padding: 1rem; + margin-bottom: 1.5rem; + } + + .nostr-profile-content { + flex-direction: column; + align-items: center; + text-align: center; + } + + .nostr-profile-picture { + width: 100px; + height: 100px; + margin-bottom: 1rem; + } + + .nostr-profile-info { + width: 100%; + } + + .contact-form { + margin-top: 1.5rem; + } + + .form-group { + margin-bottom: 1.25rem; + } + + .form-group input[type="text"], + .form-group textarea { + padding: 0.625rem; + font-size: 16px; /* Prevent zoom on iOS */ + } + + .form-group textarea { + min-height: 120px; + } + + .form-actions { + flex-direction: column; + gap: 0.75rem; + } + + /* Buttons */ + .btn { + padding: 0.625rem 1.25rem; + font-size: 0.95rem; + min-width: 44px; /* Touch target */ + } + + /* Full-width buttons in forms and feature cards */ + .form-actions .btn, + .feature-card .btn { + width: 100%; + justify-content: center; + } + + /* Tables */ + table { + font-size: 0.875rem; + display: block; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + thead, tbody, tr { + display: table; + width: 100%; + table-layout: fixed; + } + + th, td { + padding: 0.5rem; + word-break: break-word; + } + /* E-books table: show only avatar on mobile, narrow author column */ + .ebooks-table { + display: block; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .ebooks-table thead, + .ebooks-table tbody, + .ebooks-table tr { + display: table; + width: 100%; + table-layout: fixed; + } + .ebooks-table th[data-sort="author"], .ebooks-table td:nth-child(2) { width: 60px; @@ -92,6 +342,11 @@ text-align: center; } + .ebooks-table th:not([data-sort="author"]), + .ebooks-table td:not(:nth-child(2)) { + padding: 0.5rem; + } + .ebooks-table td .user-badge { display: flex; justify-content: center; @@ -118,6 +373,156 @@ width: 20px; height: 20px; } + + /* Wiki Pages */ + .breadcrumbs { + margin-bottom: 0.75rem; + font-size: 0.875rem; + } + + .breadcrumbs ol { + flex-wrap: wrap; + gap: 0.25rem; + } + + .page-header { + margin-bottom: 1.5rem; + padding-bottom: 1rem; + } + + .page-summary { + font-size: 1rem; + } + + .page-content { + line-height: 1.7; + font-size: 0.95rem; + } + + /* Feed */ + .feed-container { + padding: 1rem; + } + + .feed-item { + padding: 0.75rem 0; + } + + .feed-header { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .feed-content { + font-size: 0.85rem; + } + + .feed-time { + font-size: 0.8rem; + } + + /* Error Pages */ + .error-page { + padding: 2rem 1rem; + } + + .error-page h1 { + font-size: 3rem; + } + + .error-page p { + font-size: 1.1rem; + } + + /* Footer */ + footer { + padding: 1.5rem 1rem; + font-size: 0.875rem; + } + + /* Code blocks */ + pre { + padding: 0.75rem; + font-size: 0.85rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + code { + font-size: 0.85rem; + padding: 0.15rem 0.3rem; + } + + /* Lists */ + ul, ol { + margin-left: 1.5rem; + margin-bottom: 0.75rem; + } + + /* Logo */ + .logo { + font-size: 1.1rem; + margin-right: 1rem; + } + + .logo-icon { + width: 28px; + height: 28px; + } + + /* Touch-friendly targets */ + a, button { + min-height: 44px; /* iOS recommended touch target */ + display: inline-flex; + align-items: center; + } + + .nav-menu a { + min-height: 48px; + padding: 0.75rem 0; + } + + .wiki-menu a { + min-height: 44px; + } + + /* Images */ + img { + max-width: 100%; + height: auto; + } + + /* Prevent horizontal scroll */ + body { + overflow-x: hidden; + } + + html { + overflow-x: hidden; + } + + /* Alerts */ + .alert { + padding: 0.875rem; + font-size: 0.9rem; + } + + /* User badges */ + .user-badge { + padding: 0.375rem 0.5rem; + } + + .user-badge-avatar, + .user-badge-avatar-placeholder { + width: 20px; + height: 20px; + } + + /* Feed sidebar hidden on mobile but show on landing if needed */ + .landing-page .feed-section { + margin: 1.5rem 0; + } } /* Tablet styles (768px - 1024px) */ @@ -132,10 +537,66 @@ .layout-container { padding: 1.5rem; + gap: 1.5rem; } h1 { - font-size: 2.25rem; + font-size: 2rem; + } + + h2 { + font-size: 1.75rem; + } + + /* Landing Page */ + .landing-page { + padding: 1.5rem; + } + + .feature-grid { + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + } + + /* Blog Layout */ + .blog-layout { + padding: 1.5rem; + gap: 2rem; + } + + .blog-sidebar { + width: 300px; + } + + .blog-content { + padding: 2rem; + } + + /* Contact Page */ + .contact-page { + padding: 1.5rem; + } + + .nostr-profile-content { + flex-direction: row; + text-align: left; + } + + .form-actions { + flex-direction: row; + } + + .form-actions .btn { + width: auto; + } + + /* Buttons */ + .btn { + width: auto; + } + + .feature-card .btn { + width: auto; } } diff --git a/templates/base.html b/templates/base.html index 6d7f669..6416d9a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -48,16 +48,22 @@ @@ -93,6 +99,13 @@ menu.classList.toggle('active'); }); } + + // Prevent navigation on dropdown toggles + document.querySelectorAll('.dropdown-toggle').forEach(function(toggle) { + toggle.addEventListener('click', function(e) { + e.preventDefault(); + }); + }); diff --git a/templates/landing.html b/templates/landing.html index 8427e54..55840f1 100644 --- a/templates/landing.html +++ b/templates/landing.html @@ -29,9 +29,15 @@ {{if .NewestBlogItem}}
{{$item := .NewestBlogItem}} - {{if $item.Image}} + {{$profile := index $.Profiles $item.Author}} + {{$image := "/static/GitCitadel_Icon_Gradient.svg"}} + {{if and $item.Image (ne $item.Image "")}} + {{$image = $item.Image}} + {{else if and $profile $profile.Picture (ne $profile.Picture "")}} + {{$image = $profile.Picture}} + {{end}}
- {{$item.Title}} + {{$item.Title}}

{{$item.Title}}

{{if $item.Summary}} @@ -39,20 +45,6 @@ {{end}}
- {{else}} - {{$profile := index $.Profiles $item.Author}} - {{if $profile.Picture}} -
- {{$item.Title}} -
-

{{$item.Title}}

- {{if $item.Summary}} -

{{truncate $item.Summary 250}}

- {{end}} -
-
- {{end}} - {{end}}
{{end}}
@@ -66,9 +58,15 @@ {{if .NewestArticleItem}}
{{$item := .NewestArticleItem}} - {{if $item.Image}} + {{$profile := index $.Profiles $item.Author}} + {{$image := "/static/GitCitadel_Icon_Gradient.svg"}} + {{if and $item.Image (ne $item.Image "")}} + {{$image = $item.Image}} + {{else if and $profile $profile.Picture (ne $profile.Picture "")}} + {{$image = $profile.Picture}} + {{end}}
- {{$item.Title}} + {{$item.Title}}

{{$item.Title}}

{{if $item.Summary}} @@ -76,20 +74,6 @@ {{end}}
- {{else}} - {{$profile := index $.Profiles $item.Author}} - {{if $profile.Picture}} -
- {{$item.Title}} -
-

{{$item.Title}}

- {{if $item.Summary}} -

{{truncate $item.Summary 250}}

- {{end}} -
-
- {{end}} - {{end}}
{{end}}
diff --git a/templates/page.html b/templates/page.html index e4a6bbf..7b6a7df 100644 --- a/templates/page.html +++ b/templates/page.html @@ -1,13 +1,5 @@ {{define "content"}}