From 9d773d484a62b4c659400f2d66684a3ad4591cf3 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Tue, 28 Oct 2025 22:05:37 -0500 Subject: [PATCH] dark mode! fixes #1 --- pkg/appview/db/queries.go | 16 +- pkg/appview/jetstream/processor.go | 28 +++ pkg/appview/jetstream/worker.go | 15 +- pkg/appview/static/css/style.css | 203 ++++++++++++++------ pkg/appview/static/js/app.js | 24 +++ pkg/appview/templates/components/nav.html | 1 + pkg/appview/templates/pages/repository.html | 26 +-- 7 files changed, 232 insertions(+), 81 deletions(-) diff --git a/pkg/appview/db/queries.go b/pkg/appview/db/queries.go index 961d0f5..f2571f7 100644 --- a/pkg/appview/db/queries.go +++ b/pkg/appview/db/queries.go @@ -302,11 +302,12 @@ func GetRepositoryMetadata(db *sql.DB, did string, repository string) (map[strin // GetUserByDID retrieves a user by DID func GetUserByDID(db *sql.DB, did string) (*User, error) { var user User + var avatar sql.NullString err := db.QueryRow(` SELECT did, handle, pds_endpoint, avatar, last_seen FROM users WHERE did = ? - `, did).Scan(&user.DID, &user.Handle, &user.PDSEndpoint, &user.Avatar, &user.LastSeen) + `, did).Scan(&user.DID, &user.Handle, &user.PDSEndpoint, &avatar, &user.LastSeen) if err == sql.ErrNoRows { return nil, nil @@ -315,17 +316,23 @@ func GetUserByDID(db *sql.DB, did string) (*User, error) { return nil, err } + // Handle NULL avatar + if avatar.Valid { + user.Avatar = avatar.String + } + return &user, nil } // GetUserByHandle retrieves a user by handle func GetUserByHandle(db *sql.DB, handle string) (*User, error) { var user User + var avatar sql.NullString err := db.QueryRow(` SELECT did, handle, pds_endpoint, avatar, last_seen FROM users WHERE handle = ? - `, handle).Scan(&user.DID, &user.Handle, &user.PDSEndpoint, &user.Avatar, &user.LastSeen) + `, handle).Scan(&user.DID, &user.Handle, &user.PDSEndpoint, &avatar, &user.LastSeen) if err == sql.ErrNoRows { return nil, nil @@ -334,6 +341,11 @@ func GetUserByHandle(db *sql.DB, handle string) (*User, error) { return nil, err } + // Handle NULL avatar + if avatar.Valid { + user.Avatar = avatar.String + } + return &user, nil } diff --git a/pkg/appview/jetstream/processor.go b/pkg/appview/jetstream/processor.go index 09ec095..45c5830 100644 --- a/pkg/appview/jetstream/processor.go +++ b/pkg/appview/jetstream/processor.go @@ -285,7 +285,20 @@ func (p *Processor) ProcessSailorProfile(ctx context.Context, did string, record // This is called when Jetstream receives an identity event indicating a handle change. // The identity cache is invalidated to ensure the next lookup uses the new handle, // and the database is updated to reflect the change in the UI. +// +// Only processes events for users who already exist in our database (have ATCR activity). func (p *Processor) ProcessIdentity(ctx context.Context, did string, newHandle string) error { + // Check if user exists in our database - only update if they're an ATCR user + user, err := db.GetUserByDID(p.db, did) + if err != nil { + return fmt.Errorf("failed to check user existence: %w", err) + } + + // Skip if user doesn't exist - they don't have any ATCR activity (manifests, profiles, etc.) + if user == nil { + return nil + } + // Update handle in database if err := db.UpdateUserHandle(p.db, did, newHandle); err != nil { slog.Warn("Failed to update user handle in database", @@ -308,6 +321,7 @@ func (p *Processor) ProcessIdentity(ctx context.Context, did string, newHandle s slog.Info("Processed identity change event", "component", "processor", "did", did, + "old_handle", user.Handle, "new_handle", newHandle) return nil @@ -326,12 +340,25 @@ func (p *Processor) ProcessIdentity(ctx context.Context, did string, newHandle s // - If truly deactivated: Resolution fails and user won't appear in new queries // // This approach prevents data loss from PDS migrations while still handling deactivations. +// +// Only processes events for users who already exist in our database (have ATCR activity). func (p *Processor) ProcessAccount(ctx context.Context, did string, active bool, status string) error { // Only process deactivation events if active || status != "deactivated" { return nil } + // Check if user exists in our database - only update if they're an ATCR user + user, err := db.GetUserByDID(p.db, did) + if err != nil { + return fmt.Errorf("failed to check user existence: %w", err) + } + + // Skip if user doesn't exist - they don't have any ATCR activity + if user == nil { + return nil + } + // Invalidate cached identity data to force re-resolution on next lookup // This will discover if the account was migrated (new PDS) or truly deactivated (resolution fails) if err := atproto.InvalidateIdentity(ctx, did); err != nil { @@ -345,6 +372,7 @@ func (p *Processor) ProcessAccount(ctx context.Context, did string, active bool, slog.Info("Processed account deactivation event - cache invalidated", "component", "processor", "did", did, + "handle", user.Handle, "status", status) return nil diff --git a/pkg/appview/jetstream/worker.go b/pkg/appview/jetstream/worker.go index c116e4e..2d4a93d 100644 --- a/pkg/appview/jetstream/worker.go +++ b/pkg/appview/jetstream/worker.go @@ -443,12 +443,7 @@ func (w *Worker) processIdentity(event *JetstreamEvent) error { } identity := event.Identity - slog.Info("Jetstream processing identity event", - "did", identity.DID, - "handle", identity.Handle, - "seq", identity.Seq) - - // Process via shared processor + // Process via shared processor (only ATCR users will be logged at Info level) return w.processor.ProcessIdentity(context.Background(), identity.DID, identity.Handle) } @@ -459,13 +454,7 @@ func (w *Worker) processAccount(event *JetstreamEvent) error { } account := event.Account - slog.Info("Jetstream processing account event", - "did", account.DID, - "active", account.Active, - "status", account.Status, - "seq", account.Seq) - - // Process via shared processor + // Process via shared processor (only ATCR users will be logged at Info level) return w.processor.ProcessAccount(context.Background(), account.DID, account.Active, account.Status) } diff --git a/pkg/appview/static/css/style.css b/pkg/appview/static/css/style.css index 2179c3e..174f1d1 100644 --- a/pkg/appview/static/css/style.css +++ b/pkg/appview/static/css/style.css @@ -1,5 +1,6 @@ :root { --primary: #0066cc; + --button-primary: #0066cc; --primary-dark: #0052a3; --secondary: #6c757d; --success: #28a745; @@ -16,6 +17,30 @@ --hover-bg: #f9f9f9; --star: #fbbf24; + /* Navbar colors - stay consistent in dark mode */ + --navbar-bg: #1a1a1a; + --navbar-fg: #ffffff; + + /* Button text color */ + --btn-text: #ffffff; + + /* Theme toggle icon */ + --theme-icon: '🌙'; + + /* Shadows */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); + --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15); + + /* Metadata badge */ + --metadata-badge-bg: #f0f0f0; + --metadata-badge-text: var(--fg); + + /* Version badge */ + --version-badge-bg: #f3e5f5; + --version-badge-text: #7b1fa2; + --version-badge-border: #ba68c8; + /* Hero section colors */ --hero-bg-start: #f8f9fa; --hero-bg-end: #e9ecef; @@ -28,6 +53,61 @@ --terminal-comment: #6a9955; } +[data-theme="dark"] { + --primary: #60a5fa; + --button-primary: #1d4ed8; + --primary-dark: #1e40af; + --secondary: #9ca3af; + --success: #34d399; + --success-bg: #064e3b; + --warning: #fbbf24; + --warning-bg: #78350f; + --danger: #dc3545; + --danger-bg: #7f1d1d; + --bg: #2a2a2a; + --fg: #e0e0e0; + --border-dark: #9ca3af; + --border: #404040; + --code-bg: #1e1e1e; + --hover-bg: #333333; + --star: #fbbf24; + + /* Navbar colors - stay consistent (always black) */ + --navbar-bg: #1a1a1a; + --navbar-fg: #ffffff; + + /* Button text color */ + --btn-text: #ffffff; + + /* Theme toggle icon */ + --theme-icon: '☀️'; + + /* Shadows - lighter for dark backgrounds */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.5); + + /* Metadata badge - darker in dark mode */ + --metadata-badge-bg: #1e1e1e; + --metadata-badge-text: #ffffff; + + /* Version badge - swapped colors with softer purple background */ + --version-badge-bg: #9b59b6; + --version-badge-text: #ffffff; + --version-badge-border: #ba68c8; + + /* Hero section colors */ + --hero-bg-start: #2d2d2d; + --hero-bg-end: #1a1a1a; + + /* Terminal colors - keep similar since already dark */ + --terminal-bg: #0d0d0d; + --terminal-header-bg: #1a1a1a; + --terminal-text: #d0d0d0; + --terminal-prompt: #4ec9b0; + --terminal-comment: #6a9955; +} + * { margin: 0; padding: 0; @@ -42,24 +122,24 @@ body { } .container { - max-width: 1200px; + max-width: 1920px; margin: 0 auto; padding: 20px; } /* Navigation */ .navbar { - background: var(--fg); - color:var(--bg); + background: var(--navbar-bg); + color: var(--navbar-fg); padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + box-shadow: var(--shadow-md); } .nav-brand a { - color:var(--bg); + color: var(--navbar-fg); text-decoration: none; font-size: 1.5rem; font-weight: bold; @@ -90,7 +170,7 @@ body { } .nav-links a { - color:var(--fg); + color: var(--navbar-fg); text-decoration: none; padding: 0.5rem 1rem; } @@ -110,7 +190,7 @@ body { align-items: center; gap: 0.5rem; background: transparent; - color:var(--bg); + color: var(--navbar-fg); border: none; padding: 0.5rem; cursor: pointer; @@ -133,7 +213,7 @@ body { width: 32px; height: 32px; border-radius: 50%; - background: var(--primary); + background: var(--button-primary); display: flex; align-items: center; justify-content: center; @@ -153,14 +233,14 @@ body { width: 80px; height: 80px; border-radius: 50%; - background: var(--primary); + background: var(--button-primary); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 2rem; text-transform: uppercase; - color: var(--bg); + color: var(--btn-text); } .user-profile { @@ -176,7 +256,7 @@ body { } .user-handle { - color: var(--bg); + color: var(--navbar-fg); font-size: 0.95rem; } @@ -195,7 +275,7 @@ body { background:var(--bg); border: 1px solid var(--border); border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + box-shadow: var(--shadow-lg); min-width: 200px; overflow: hidden; z-index: 1000; @@ -237,8 +317,8 @@ body { /* Buttons */ button, .btn, .btn-primary, .btn-secondary { padding: 0.5rem 1rem; - background: var(--primary); - color:var(--bg); + background: var(--button-primary); + color: var(--btn-text); border: none; border-radius: 4px; cursor: pointer; @@ -254,7 +334,7 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover { /* Override nav-links color for primary button */ .nav-links .btn-primary { - color: var(--bg); + color: var(--btn-text); } .btn-secondary { @@ -263,8 +343,14 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover { .btn-link { background: transparent; - color:var(--bg); - text-decoration: underline; + color: var(--navbar-fg); + text-decoration: none; +} + +.theme-toggle-btn::before { + content: var(--theme-icon); + font-size: 1.2rem; + cursor: pointer; } .delete-btn { @@ -275,7 +361,8 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover { .copy-btn { padding: 0.25rem 0.75rem; - background: var(--primary); + background: var(--button-primary); + color: var(--btn-text); font-size: 0.85rem; } @@ -286,7 +373,7 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover { padding: 1rem; margin-bottom: 1rem; background:var(--bg); - box-shadow: 0 1px 3px rgba(0,0,0,0.05); + box-shadow: var(--shadow-sm); } .push-header { @@ -396,14 +483,14 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover { width: 48px; height: 48px; border-radius: 8px; - background: var(--primary); + background: var(--button-primary); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.5rem; text-transform: uppercase; - color: var(--bg); + color: var(--btn-text); flex-shrink: 0; } @@ -538,17 +625,17 @@ a.license-badge { } a.license-badge:hover { - background: var(--primary); - color: var(--bg); - border-color: var(--primary); + background: var(--button-primary); + color: var(--btn-text); + border-color: var(--button-primary); transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + box-shadow: var(--shadow-md); } .version-badge { - background: #f3e5f5; - color: #7b1fa2; - border: 1px solid #ba68c8; + background: var(--version-badge-bg); + color: var(--version-badge-text); + border: 1px solid var(--version-badge-border); } .repo-description { @@ -633,7 +720,7 @@ a.license-badge:hover { border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem; - box-shadow: 0 1px 3px rgba(0,0,0,0.05); + box-shadow: var(--shadow-sm); } .settings-section h2 { @@ -702,7 +789,7 @@ a.license-badge:hover { max-height: 80vh; overflow-y: auto; position: relative; - box-shadow: 0 4px 6px rgba(0,0,0,0.1); + box-shadow: var(--shadow-lg); } .modal-close { @@ -817,7 +904,7 @@ a.license-badge:hover { padding: 2rem; border-radius: 8px; border: 1px solid var(--border); - box-shadow: 0 2px 4px rgba(0,0,0,0.05); + box-shadow: var(--shadow-sm); } .login-form .form-group { @@ -877,7 +964,7 @@ a.license-badge:hover { border-radius: 8px; padding: 2rem; margin-bottom: 2rem; - box-shadow: 0 1px 3px rgba(0,0,0,0.05); + box-shadow: var(--shadow-sm); } .repo-hero { @@ -899,14 +986,14 @@ a.license-badge:hover { width: 80px; height: 80px; border-radius: 12px; - background: var(--primary); + background: var(--button-primary); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 2.5rem; text-transform: uppercase; - color: var(--bg); + color: var(--btn-text); flex-shrink: 0; } @@ -944,10 +1031,17 @@ a.license-badge:hover { margin: 0.5rem 0 0 0; } -.repo-actions { +.repo-info-row { + display: flex; + gap: 2rem; + align-items: center; margin-top: 1.5rem; } +.repo-actions { + flex: 0 0 auto; +} + .star-btn { display: inline-flex; align-items: center; @@ -1002,9 +1096,8 @@ a.license-badge:hover { gap: 1rem; align-items: center; flex-wrap: wrap; - margin-bottom: 1.5rem; - padding-top: 1rem; - border-top: 1px solid var(--border); + flex: 1; + justify-content: flex-end; } .metadata-badge { @@ -1044,7 +1137,7 @@ a.license-badge:hover { border-radius: 8px; padding: 1.5rem; margin-bottom: 2rem; - box-shadow: 0 1px 3px rgba(0,0,0,0.05); + box-shadow: var(--shadow-sm); } .repo-section h2 { @@ -1077,7 +1170,7 @@ a.license-badge:hover { .tag-name-large { font-size: 1.2rem; font-weight: 600; - color: var(--primary); + color: var(--fg); } .tag-timestamp { @@ -1166,8 +1259,8 @@ a.license-badge:hover { font-size: 0.75rem; font-weight: 600; border-radius: 12px; - background: var(--primary); - color: var(--bg); + background: var(--button-primary); + color: var(--btn-text); white-space: nowrap; margin-left: 0.5rem; } @@ -1235,7 +1328,7 @@ a.license-badge:hover { border-radius: 8px; padding: 1.5rem; background: var(--bg); - box-shadow: 0 1px 3px rgba(0,0,0,0.05); + box-shadow: var(--shadow-sm); transition: all 0.2s ease; text-decoration: none; color: var(--fg); @@ -1246,7 +1339,7 @@ a.license-badge:hover { } .featured-card:hover { - box-shadow: 0 4px 8px rgba(0,0,0,0.1); + box-shadow: var(--shadow-md); border-color: var(--primary); transform: translateY(-2px); } @@ -1270,14 +1363,14 @@ a.license-badge:hover { width: 48px; height: 48px; border-radius: 8px; - background: var(--primary); + background: var(--button-primary); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.5rem; text-transform: uppercase; - color:var(--bg); + color: var(--btn-text); flex-shrink: 0; } @@ -1383,7 +1476,7 @@ a.license-badge:hover { margin: 0 auto 2.5rem; background: var(--terminal-bg); border-radius: 8px; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); + box-shadow: var(--shadow-lg); overflow: hidden; } @@ -1453,9 +1546,9 @@ a.license-badge:hover { } .btn-hero-primary { - background: var(--primary); - color: var(--bg); - border: 2px solid var(--primary); + background: var(--button-primary); + color: var(--btn-text); + border: 2px solid var(--button-primary); } .btn-hero-primary:hover { @@ -1468,12 +1561,12 @@ a.license-badge:hover { .btn-hero-secondary { background: transparent; color: var(--primary); - border: 2px solid var(--primary); + border: 2px solid var(--button-primary); } .btn-hero-secondary:hover { - background: var(--primary); - color: var(--bg); + background: var(--button-primary); + color: var(--btn-text); transform: translateY(-2px); } @@ -1496,7 +1589,7 @@ a.license-badge:hover { .benefit-card:hover { border-color: var(--primary); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + box-shadow: var(--shadow-md); transform: translateY(-4px); } @@ -1697,7 +1790,7 @@ a.license-badge:hover { /* README and Repository Layout */ .repo-content-layout { display: grid; - grid-template-columns: 1fr 450px; + grid-template-columns: 7fr 3fr; gap: 2rem; margin-top: 2rem; } diff --git a/pkg/appview/static/js/app.js b/pkg/appview/static/js/app.js index c1be9f9..daa4fb9 100644 --- a/pkg/appview/static/js/app.js +++ b/pkg/appview/static/js/app.js @@ -1,3 +1,27 @@ +// Theme management +// Load theme immediately to avoid flash +(function() { + const theme = localStorage.getItem('theme') || 'light'; + document.documentElement.setAttribute('data-theme', theme); +})(); + +function toggleTheme() { + const html = document.documentElement; + const currentTheme = html.getAttribute('data-theme') || 'light'; + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + html.setAttribute('data-theme', newTheme); + localStorage.setItem('theme', newTheme); + updateThemeIcon(); +} + +function updateThemeIcon() { + const themeBtn = document.getElementById('theme-toggle'); + if (!themeBtn) return; + + const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; + themeBtn.setAttribute('aria-label', currentTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'); +} + // Copy to clipboard function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { diff --git a/pkg/appview/templates/components/nav.html b/pkg/appview/templates/components/nav.html index 76994fe..52992d6 100644 --- a/pkg/appview/templates/components/nav.html +++ b/pkg/appview/templates/components/nav.html @@ -11,6 +11,7 @@ - -
- -
+ +
+
+ +
- - {{ if or .Repository.Licenses .Repository.SourceURL .Repository.DocumentationURL .Repository.Version }} - - {{ end }}