Browse Source

fix theme

main
Silberengel 4 weeks ago
parent
commit
c99850e41e
  1. 178
      src/app.css
  2. 26
      src/hooks.server.ts
  3. 9
      src/lib/components/ThemeToggle.svelte
  4. 107
      src/lib/components/UserBadge.svelte
  5. 73
      src/lib/services/logger.ts
  6. 20
      src/routes/+page.svelte
  7. 6
      src/routes/docs/nip34/+page.server.ts

178
src/app.css

@ -15,31 +15,31 @@
--bg-primary: var(--snow); --bg-primary: var(--snow);
--bg-secondary: var(--lavender-blush); --bg-secondary: var(--lavender-blush);
--bg-tertiary: var(--thistle); --bg-tertiary: var(--thistle);
--text-primary: #1a1a1a; --text-primary: #0a0a0a; /* Darker for better contrast */
--text-secondary: #4a4a4a; --text-secondary: #2a2a2a; /* Darker for better contrast */
--text-muted: #6b7280; --text-muted: #4a4a4a; /* Darker for better contrast */
--border-color: var(--thistle); --border-color: var(--thistle);
--border-light: var(--lavender-blush); --border-light: var(--lavender-blush);
--accent: var(--royal-plum); --accent: var(--royal-plum);
--accent-hover: #6a1f4d; --accent-hover: #6a1f4d;
--accent-light: var(--lilac); --accent-light: var(--lilac);
--link-color: var(--royal-plum); --link-color: #5a0d4f; /* Darker plum for better contrast on light bg */
--link-hover: var(--accent-hover); --link-hover: #4a0a3f; /* Even darker for hover */
--card-bg: #ffffff; --card-bg: #ffffff;
--card-border: var(--border-color); --card-border: var(--border-color);
--button-primary: var(--royal-plum); --button-primary: var(--royal-plum);
--button-primary-hover: var(--accent-hover); --button-primary-hover: var(--accent-hover);
--button-secondary: var(--lilac); --button-secondary: #8b5a7a; /* Darker for better contrast */
--button-secondary-hover: var(--thistle); --button-secondary-hover: #7a4a6a; /* Even darker for hover */
--input-bg: #ffffff; --input-bg: #ffffff;
--input-border: var(--border-color); --input-border: var(--border-color);
--input-focus: var(--royal-plum); --input-focus: var(--royal-plum);
--error-bg: #fee2e2; --error-bg: #fee2e2;
--error-text: #991b1b; --error-text: #7a0a0a; /* Darker for better contrast */
--success-bg: #d1fae5; --success-bg: #d1fae5;
--success-text: #065f46; --success-text: #034a2e; /* Darker for better contrast */
--warning-bg: #fef3c7; --warning-bg: #fef3c7;
--warning-text: #92400e; --warning-text: #6a3000; /* Darker for better contrast */
} }
[data-theme="dark"] { [data-theme="dark"] {
@ -54,31 +54,31 @@
--bg-primary: var(--snow); --bg-primary: var(--snow);
--bg-secondary: var(--lavender-blush); --bg-secondary: var(--lavender-blush);
--bg-tertiary: var(--thistle); --bg-tertiary: var(--thistle);
--text-primary: #f5f5f5; --text-primary: #ffffff; /* Brighter for better contrast */
--text-secondary: #d1d1d1; --text-secondary: #e0e0e0; /* Brighter for better contrast */
--text-muted: #a0a0a0; --text-muted: #b0b0b0; /* Brighter for better contrast */
--border-color: var(--thistle); --border-color: var(--thistle);
--border-light: var(--lavender-blush); --border-light: var(--lavender-blush);
--accent: var(--royal-plum); --accent: var(--royal-plum);
--accent-hover: #b84a8a; --accent-hover: #b84a8a;
--accent-light: var(--lilac); --accent-light: var(--lilac);
--link-color: var(--royal-plum); --link-color: #d84ab8; /* Brighter plum for better contrast on dark bg */
--link-hover: var(--accent-hover); --link-hover: #e85ac8; /* Even brighter for hover */
--card-bg: var(--lavender-blush); --card-bg: var(--lavender-blush);
--card-border: var(--border-color); --card-border: var(--border-color);
--button-primary: var(--royal-plum); --button-primary: var(--royal-plum);
--button-primary-hover: var(--accent-hover); --button-primary-hover: var(--accent-hover);
--button-secondary: var(--lilac); --button-secondary: #7a5a6a; /* Adjusted for dark theme */
--button-secondary-hover: var(--thistle); --button-secondary-hover: #8a6a7a; /* Lighter for hover */
--input-bg: var(--lavender-blush); --input-bg: var(--lavender-blush);
--input-border: var(--border-color); --input-border: var(--border-color);
--input-focus: var(--royal-plum); --input-focus: var(--royal-plum);
--error-bg: #4a1f1f; --error-bg: #4a1f1f;
--error-text: #ff6b6b; --error-text: #ff8a8a; /* Brighter for better contrast */
--success-bg: #1a3a2a; --success-bg: #1a3a2a;
--success-text: #4ade80; --success-text: #6aff9a; /* Brighter for better contrast */
--warning-bg: #4a3a1f; --warning-bg: #4a3a1f;
--warning-text: #fbbf24; --warning-text: #ffcc44; /* Brighter for better contrast */
} }
/* Base styles */ /* Base styles */
@ -161,12 +161,59 @@ header {
margin-bottom: 2rem; margin-bottom: 2rem;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding-bottom: 1rem; padding-bottom: 1rem;
position: relative;
}
.header-logo {
display: flex;
align-items: center;
gap: 1rem;
text-decoration: none;
color: inherit;
transition: opacity 0.2s ease;
flex-shrink: 0;
}
.header-logo:hover {
opacity: 0.8;
}
.main-logo {
height: 48px;
width: auto;
object-fit: contain;
}
.header-logo h1 {
margin: 0;
font-size: 1.75rem;
font-weight: 600;
color: var(--text-primary);
} }
nav { nav {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 1rem;
flex: 1;
position: relative;
}
.nav-links {
grid-column: 2;
display: flex; display: flex;
gap: 1rem; gap: 1rem;
align-items: center; align-items: center;
justify-self: center;
}
.auth-section {
grid-column: 3;
justify-self: end;
display: flex;
align-items: center;
gap: 1rem;
} }
nav a { nav a {
@ -207,7 +254,7 @@ button:disabled, .button:disabled {
.btn-secondary, .logout-button { .btn-secondary, .logout-button {
background: var(--button-secondary); background: var(--button-secondary);
color: white; color: #ffffff; /* Ensure white text for contrast */
} }
.btn-secondary:hover, .logout-button:hover { .btn-secondary:hover, .logout-button:hover {
@ -430,7 +477,7 @@ input:disabled, textarea:disabled, select:disabled {
.pr-status.open, .issue-status.open { .pr-status.open, .issue-status.open {
background: var(--accent-light); background: var(--accent-light);
color: var(--accent); color: var(--text-primary); /* Better contrast */
} }
.pr-status.closed, .issue-status.closed { .pr-status.closed, .issue-status.closed {
@ -446,14 +493,14 @@ input:disabled, textarea:disabled, select:disabled {
.fork-badge { .fork-badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
background: var(--accent-light); background: var(--accent-light);
color: var(--accent); color: var(--text-primary); /* Better contrast */
border-radius: 4px; border-radius: 4px;
font-size: 0.85rem; font-size: 0.85rem;
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.fork-badge a { .fork-badge a {
color: var(--accent); color: var(--link-color); /* Use link color for better visibility */
text-decoration: none; text-decoration: none;
} }
@ -479,7 +526,7 @@ input:disabled, textarea:disabled, select:disabled {
color: var(--error-text); color: var(--error-text);
} }
/* Code blocks */ /* Code blocks - consistent dark-gray background in both themes */
code { code {
background: var(--bg-secondary); background: var(--bg-secondary);
padding: 0.125rem 0.25rem; padding: 0.125rem 0.25rem;
@ -490,13 +537,13 @@ code {
} }
pre { pre {
background: var(--bg-tertiary); background: #1e1e1e; /* Consistent dark-gray background */
color: var(--text-primary); color: #d4d4d4; /* Light gray text for good contrast */
padding: 1rem; padding: 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
overflow-x: auto; overflow-x: auto;
margin: 1rem 0; margin: 1rem 0;
border: 1px solid var(--border-color); border: 1px solid #3a3a3a;
} }
pre code { pre code {
@ -767,89 +814,93 @@ pre code {
padding: 2rem; padding: 2rem;
} }
/* Highlight.js Syntax Highlighting - Custom Theme using CSS Variables */ /* Highlight.js Syntax Highlighting - Consistent dark-gray background */
.hljs { .hljs {
background: var(--bg-tertiary); background: #1e1e1e !important; /* Consistent dark-gray background in both themes */
color: var(--text-primary); color: #d4d4d4 !important; /* Light gray text for good contrast */
border: 1px solid var(--border-color); border: 1px solid #3a3a3a;
border-radius: 4px; border-radius: 4px;
padding: 1rem; padding: 1rem;
overflow-x: auto; overflow-x: auto;
} }
/* Syntax highlighting colors - adapted to royal plum theme */ /* Syntax highlighting colors - high contrast for dark-gray background */
.hljs-comment, .hljs-comment,
.hljs-quote { .hljs-quote {
color: var(--text-muted); color: #6a9955; /* Green comments - good contrast */
font-style: italic; font-style: italic;
} }
.hljs-keyword, .hljs-keyword,
.hljs-selector-tag, .hljs-selector-tag,
.hljs-subst { .hljs-subst {
color: var(--accent); color: #c586c0; /* Purple/magenta keywords - good contrast */
font-weight: 500; font-weight: 500;
} }
.hljs-number, .hljs-number,
.hljs-literal, .hljs-literal {
color: #b5cea8; /* Light green numbers - good contrast */
font-weight: 500;
}
.hljs-variable, .hljs-variable,
.hljs-template-variable, .hljs-template-variable,
.hljs-tag .hljs-attr { .hljs-tag .hljs-attr {
color: var(--accent-light); color: #9cdcfe; /* Light blue variables - good contrast */
} }
.hljs-string, .hljs-string,
.hljs-doctag { .hljs-doctag {
color: var(--success-text); color: #ce9178; /* Orange strings - good contrast */
} }
.hljs-title, .hljs-title,
.hljs-section, .hljs-section,
.hljs-selector-id { .hljs-selector-id {
color: var(--accent); color: #dcdcaa; /* Yellow titles - good contrast */
font-weight: 600; font-weight: 600;
} }
.hljs-type, .hljs-type,
.hljs-class .hljs-title { .hljs-class .hljs-title {
color: var(--accent); color: #4ec9b0; /* Cyan types - good contrast */
font-weight: 500; font-weight: 500;
} }
.hljs-tag, .hljs-tag,
.hljs-name, .hljs-name,
.hljs-attribute { .hljs-attribute {
color: var(--accent); color: #569cd6; /* Blue tags - good contrast */
} }
.hljs-regexp, .hljs-regexp,
.hljs-link { .hljs-link {
color: var(--accent-light); color: #d16969; /* Red regexp - good contrast */
} }
.hljs-symbol, .hljs-symbol,
.hljs-bullet { .hljs-bullet {
color: var(--warning-text); color: #dcdcaa; /* Yellow symbols - good contrast */
} }
.hljs-built_in, .hljs-built_in,
.hljs-builtin-name { .hljs-builtin-name {
color: var(--accent-light); color: #4ec9b0; /* Cyan built-ins - good contrast */
} }
.hljs-meta { .hljs-meta {
color: var(--text-muted); color: #808080; /* Gray meta - good contrast */
} }
.hljs-deletion { .hljs-deletion {
background: var(--error-bg); background: #4a1f1f; /* Dark red background */
color: var(--error-text); color: #ff8a8a; /* Light red text */
} }
.hljs-addition { .hljs-addition {
background: var(--success-bg); background: #1a3a2a; /* Dark green background */
color: var(--success-text); color: #6aff9a; /* Light green text */
} }
.hljs-emphasis { .hljs-emphasis {
@ -860,30 +911,5 @@ pre code {
font-weight: 600; font-weight: 600;
} }
/* Dark theme adjustments for better contrast */ /* Code blocks use consistent dark-gray background in both themes */
[data-theme="dark"] .hljs { /* All syntax highlighting colors are optimized for #1e1e1e background */
background: var(--bg-tertiary);
}
[data-theme="dark"] .hljs-string,
[data-theme="dark"] .hljs-doctag {
color: var(--success-text);
}
[data-theme="dark"] .hljs-keyword,
[data-theme="dark"] .hljs-selector-tag,
[data-theme="dark"] .hljs-subst {
color: var(--accent);
}
/* Light theme adjustments */
:root:not([data-theme="dark"]) .hljs-string,
:root:not([data-theme="dark"]) .hljs-doctag {
color: #065f46; /* Darker green for light mode */
}
:root:not([data-theme="dark"]) .hljs-keyword,
:root:not([data-theme="dark"]) .hljs-selector-tag,
:root:not([data-theme="dark"]) .hljs-subst {
color: #6a1f4d; /* Darker plum for light mode */
}

26
src/hooks.server.ts

@ -24,10 +24,26 @@ if (typeof process !== 'undefined') {
} }
export const handle: Handle = async ({ event, resolve }) => { export const handle: Handle = async ({ event, resolve }) => {
// Rate limiting // Get client IP, with fallback for dev/internal requests
const clientIp = event.getClientAddress(); let clientIp: string;
try {
clientIp = event.getClientAddress();
} catch {
// Fallback for internal Vite dev server requests or when client address can't be determined
clientIp = event.request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
event.request.headers.get('x-real-ip') ||
'127.0.0.1';
}
const url = event.url; const url = event.url;
// Skip rate limiting for Vite internal requests in dev mode
const isViteInternalRequest = url.pathname.startsWith('/@') ||
url.pathname.startsWith('/src/') ||
url.pathname.startsWith('/node_modules/') ||
url.pathname.includes('react-refresh') ||
url.pathname.includes('vite-plugin-pwa');
// Determine rate limit type based on path // Determine rate limit type based on path
let rateLimitType = 'api'; let rateLimitType = 'api';
if (url.pathname.startsWith('/api/git/')) { if (url.pathname.startsWith('/api/git/')) {
@ -38,8 +54,10 @@ export const handle: Handle = async ({ event, resolve }) => {
rateLimitType = 'search'; rateLimitType = 'search';
} }
// Check rate limit // Check rate limit (skip for Vite internal requests)
const rateLimitResult = rateLimiter.check(rateLimitType, clientIp); const rateLimitResult = isViteInternalRequest
? { allowed: true, resetAt: Date.now() }
: rateLimiter.check(rateLimitType, clientIp);
if (!rateLimitResult.allowed) { if (!rateLimitResult.allowed) {
auditLogger.log({ auditLogger.log({
ip: clientIp, ip: clientIp,

9
src/lib/components/ThemeToggle.svelte

@ -48,7 +48,7 @@
} }
</script> </script>
<button class="theme-toggle" onclick={handleToggle} title="Toggle theme"> <button class="theme-toggle" onclick={handleToggle} title={currentTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}>
<span class="theme-toggle-icon"> <span class="theme-toggle-icon">
{#if currentTheme === 'dark'} {#if currentTheme === 'dark'}
@ -56,13 +56,12 @@
🌙 🌙
{/if} {/if}
</span> </span>
<span>{currentTheme === 'dark' ? 'Light' : 'Dark'}</span>
</button> </button>
<style> <style>
.theme-toggle { .theme-toggle {
cursor: pointer; cursor: pointer;
padding: 0.5rem 0.75rem; padding: 0.5rem;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 0.375rem; border-radius: 0.375rem;
background: var(--card-bg); background: var(--card-bg);
@ -71,7 +70,9 @@
font-size: 0.875rem; font-size: 0.875rem;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; justify-content: center;
width: 2.5rem;
height: 2.5rem;
font-family: 'IBM Plex Serif', serif; font-family: 'IBM Plex Serif', serif;
} }

107
src/lib/components/UserBadge.svelte

@ -0,0 +1,107 @@
<script lang="ts">
import { onMount } from 'svelte';
import { NostrClient } from '../services/nostr/nostr-client.js';
import { DEFAULT_NOSTR_RELAYS } from '../config.js';
import { KIND } from '../types/nostr.js';
import { nip19 } from 'nostr-tools';
interface Props {
pubkey: string;
}
let { pubkey }: Props = $props();
let userProfile = $state<{ name?: string; picture?: string } | null>(null);
let loading = $state(true);
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
onMount(async () => {
await loadUserProfile();
});
async function loadUserProfile() {
try {
// Fetch user profile (kind 0 - metadata)
const profileEvents = await nostrClient.fetchEvents([
{
kinds: [0],
authors: [pubkey],
limit: 1
}
]);
if (profileEvents.length > 0) {
try {
const profile = JSON.parse(profileEvents[0].content);
userProfile = {
name: profile.name,
picture: profile.picture
};
} catch {
// Invalid JSON, ignore
}
}
} catch (err) {
console.warn('Failed to load user profile:', err);
} finally {
loading = false;
}
}
function getShortNpub(): string {
try {
const npub = nip19.npubEncode(pubkey);
return `${npub.slice(0, 8)}...${npub.slice(-4)}`;
} catch {
return `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`;
}
}
</script>
<div class="user-badge">
{#if userProfile?.picture}
<img src={userProfile.picture} alt="Profile" class="user-badge-avatar" />
{:else}
<img src="/favicon.png" alt="Profile" class="user-badge-avatar user-badge-avatar-fallback" />
{/if}
<span class="user-badge-name">{userProfile?.name || getShortNpub()}</span>
</div>
<style>
.user-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
border-radius: 1.5rem;
background: var(--card-bg);
border: 1px solid var(--border-color);
transition: all 0.2s ease;
}
.user-badge:hover {
border-color: var(--accent);
background: var(--bg-secondary);
}
.user-badge-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.user-badge-avatar-fallback {
filter: grayscale(100%);
opacity: 0.7;
}
.user-badge-name {
font-size: 0.875rem;
color: var(--text-primary);
font-weight: 500;
white-space: nowrap;
}
</style>

73
src/lib/services/logger.ts

@ -1,22 +1,67 @@
/** /**
* Pino logger service * Pino logger service
* Provides structured logging with pino-pretty for development * Provides structured logging with pino-pretty for development
* Browser-safe: falls back to console in browser environments
*/ */
import pino from 'pino'; function createConsoleLogger() {
return {
const logger = pino({ info: (...args: any[]) => console.log('[INFO]', ...args),
level: process.env.LOG_LEVEL || 'info', error: (...args: any[]) => console.error('[ERROR]', ...args),
...(process.env.NODE_ENV === 'development' && { warn: (...args: any[]) => console.warn('[WARN]', ...args),
transport: { debug: (...args: any[]) => console.debug('[DEBUG]', ...args),
target: 'pino-pretty', trace: (...args: any[]) => console.trace('[TRACE]', ...args),
options: { fatal: (...args: any[]) => console.error('[FATAL]', ...args)
colorize: true, };
translateTime: 'HH:MM:ss Z', }
ignore: 'pid,hostname'
} // Check if we're in a Node.js environment
const isNode = typeof process !== 'undefined' && process.versions?.node;
let logger: any;
if (isNode) {
// Server-side: use pino
// Use dynamic import to avoid bundling for browser
const initPino = async () => {
try {
const pinoModule = await import('pino');
const pino = pinoModule.default;
const logLevel = (typeof process !== 'undefined' && process.env?.LOG_LEVEL) || 'info';
const isDev = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
return pino({
level: logLevel,
...(isDev && {
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname'
}
}
})
});
} catch {
return createConsoleLogger();
} }
}) };
});
// Initialize with console logger first
logger = createConsoleLogger();
// Upgrade to pino asynchronously (non-blocking)
initPino().then(pinoLogger => {
// Replace the logger object
Object.setPrototypeOf(logger, pinoLogger);
Object.assign(logger, pinoLogger);
}).catch(() => {
// Keep console logger if pino fails
});
} else {
// Browser-side: use console with similar API
logger = createConsoleLogger();
}
export default logger; export default logger;

20
src/routes/+page.svelte

@ -9,6 +9,7 @@
import { getPublicKeyWithNIP07, isNIP07Available } from '../lib/services/nostr/nip07-signer.js'; import { getPublicKeyWithNIP07, isNIP07Available } from '../lib/services/nostr/nip07-signer.js';
import { ForkCountService } from '../lib/services/nostr/fork-count-service.js'; import { ForkCountService } from '../lib/services/nostr/fork-count-service.js';
import ThemeToggle from '../lib/components/ThemeToggle.svelte'; import ThemeToggle from '../lib/components/ThemeToggle.svelte';
import UserBadge from '../lib/components/UserBadge.svelte';
let repos = $state<NostrEvent[]>([]); let repos = $state<NostrEvent[]>([]);
let loading = $state(true); let loading = $state(true);
@ -255,18 +256,21 @@
<div class="container"> <div class="container">
<header> <header>
<h1>gitrepublic</h1> <a href="/" class="header-logo">
<img src="/GR_logo.png" alt="GitRepublic Logo" class="main-logo" />
<h1>gitrepublic</h1>
</a>
<nav> <nav>
<a href="/">Repositories</a> <div class="nav-links">
<a href="/search">Search</a> <a href="/">Repositories</a>
<a href="/signup">Sign Up</a> <a href="/search">Search</a>
<a href="/docs/nip34">NIP-34 Docs</a> <a href="/signup">Sign Up</a>
<a href="/docs">Docs</a>
</div>
<div class="auth-section"> <div class="auth-section">
<ThemeToggle /> <ThemeToggle />
{#if userPubkey} {#if userPubkey}
<span class="user-info"> <UserBadge pubkey={userPubkey} />
{nip19.npubEncode(userPubkey).slice(0, 16)}...
</span>
<button onclick={logout} class="logout-button">Logout</button> <button onclick={logout} class="logout-button">Logout</button>
{:else} {:else}
<button onclick={login} class="login-button" disabled={!isNIP07Available()}> <button onclick={login} class="login-button" disabled={!isNIP07Available()}>

6
src/routes/docs/nip34/+page.server.ts

@ -9,12 +9,12 @@ import logger from '$lib/services/logger.js';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
try { try {
// Read NIP-34.md from the project root // Read NIP-34 documentation from docs/34.md
const filePath = join(process.cwd(), 'NIP-34.md'); const filePath = join(process.cwd(), 'docs', '34.md');
const content = await readFile(filePath, 'utf-8'); const content = await readFile(filePath, 'utf-8');
return { content }; return { content };
} catch (error) { } catch (error) {
logger.error({ error }, 'Error loading NIP-34.md'); logger.error({ error }, 'Error loading NIP-34 documentation');
return { content: null, error: 'Failed to load documentation' }; return { content: null, error: 'Failed to load documentation' };
} }
}; };

Loading…
Cancel
Save