dark mode! fixes #1

This commit is contained in:
Evan Jarrett
2025-10-28 22:05:37 -05:00
parent 6ef2aaf709
commit 9d773d484a
7 changed files with 232 additions and 81 deletions
+14 -2
View File
@@ -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
}
+28
View File
@@ -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
+2 -13
View File
@@ -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)
}
+148 -55
View File
@@ -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;
}
+24
View File
@@ -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(() => {
@@ -11,6 +11,7 @@
</div>
<div class="nav-links">
<button id="theme-toggle" onclick="toggleTheme()" class="btn-link theme-toggle-btn" aria-label="Toggle theme"></button>
{{ if .User }}
<div class="user-dropdown">
<button class="user-menu-btn" id="user-menu-btn" aria-expanded="false" aria-haspopup="true">
+15 -11
View File
@@ -30,17 +30,18 @@
</div>
</div>
<!-- Star Button -->
<div class="repo-actions">
<button class="star-btn{{ if .IsStarred }} starred{{ end }}" id="star-btn" onclick="toggleStar('{{ .Owner.Handle }}', '{{ .Repository.Name }}')">
<span class="star-icon" id="star-icon">{{ if .IsStarred }}★{{ else }}☆{{ end }}</span>
<span class="star-count" id="star-count">{{ .StarCount }}</span>
</button>
</div>
<!-- Star Button and Metadata Row -->
<div class="repo-info-row">
<div class="repo-actions">
<button class="star-btn{{ if .IsStarred }} starred{{ end }}" id="star-btn" onclick="toggleStar('{{ .Owner.Handle }}', '{{ .Repository.Name }}')">
<span class="star-icon" id="star-icon">{{ if .IsStarred }}★{{ else }}☆{{ end }}</span>
<span class="star-count" id="star-count">{{ .StarCount }}</span>
</button>
</div>
<!-- Metadata Section -->
{{ if or .Repository.Licenses .Repository.SourceURL .Repository.DocumentationURL .Repository.Version }}
<div class="repo-metadata">
<!-- Metadata Section -->
{{ if or .Repository.Licenses .Repository.SourceURL .Repository.DocumentationURL .Repository.Version }}
<div class="repo-metadata">
{{ if .Repository.Version }}
<span class="metadata-badge version-badge" title="Version">
{{ .Repository.Version }}
@@ -69,8 +70,11 @@
Documentation
</a>
{{ end }}
</div>
{{ else }}
<div class="repo-metadata"></div>
{{ end }}
</div>
{{ end }}
<!-- Pull Command -->
<div class="pull-command-section">