Browse Source

make login/logout page session persistent

fix /cache styling
fix preference panel modality
simplify feed page and speed it up
master
Silberengel 1 month ago
parent
commit
166689eafd
  1. 4
      public/healthz.json
  2. 240
      src/app.css
  3. 16
      src/lib/components/layout/Header.svelte
  4. 351
      src/lib/modules/feed/FeedPage.svelte
  5. 43
      src/lib/services/auth/session-manager.ts
  6. 21
      src/routes/+layout.svelte
  7. 170
      src/routes/cache/+page.svelte
  8. 46
      src/routes/discussions/+page.svelte
  9. 47
      src/routes/feed/+page.svelte
  10. 4
      src/routes/feed/relay/[relay]/+page.svelte
  11. 40
      src/routes/find/+page.svelte
  12. 22
      src/routes/login/+page.svelte
  13. 4
      src/routes/relay/+page.svelte
  14. 4
      src/routes/replaceable/[d_tag]/+page.svelte
  15. 4
      src/routes/repos/+page.svelte
  16. 36
      src/routes/repos/[naddr]/+page.svelte
  17. 40
      src/routes/rss/+page.svelte
  18. 4
      src/routes/topics/+page.svelte
  19. 4
      src/routes/topics/[name]/+page.svelte
  20. 5
      src/routes/write/+page.svelte

4
public/healthz.json

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
"status": "ok",
"service": "aitherboard",
"version": "0.1.1",
"buildTime": "2026-02-05T09:41:28.201Z",
"buildTime": "2026-02-05T11:28:23.225Z",
"gitCommit": "unknown",
"timestamp": 1770284488201
"timestamp": 1770290903225
}

240
src/app.css

@ -12,15 +12,15 @@ @@ -12,15 +12,15 @@
}
[data-text-size='small'] {
--text-size: 14px;
--text-size: 12px;
}
[data-text-size='medium'] {
--text-size: 16px;
--text-size: 14px;
}
[data-text-size='large'] {
--text-size: 18px;
--text-size: 16px;
}
[data-line-spacing='tight'] {
@ -103,24 +103,42 @@ p { @@ -103,24 +103,42 @@ p {
/* Consistent heading sizes relative to base font size */
h1 {
font-size: clamp(1.5rem, 4vw, 2rem);
font-size: clamp(1.25rem, 3vw, 1.5rem);
line-height: 1.2;
margin-bottom: 1rem;
margin-bottom: 1.5rem;
margin-top: 0;
font-weight: 700;
color: var(--fog-text, #1f2937);
}
:global(.dark) h1 {
color: var(--fog-dark-text, #f9fafb);
}
h2 {
font-size: clamp(1.25rem, 3vw, 1.5rem);
font-size: clamp(1.125rem, 2.5vw, 1.25rem);
line-height: 1.3;
margin-bottom: 0.875rem;
margin-top: 0;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) h2 {
color: var(--fog-dark-text, #f9fafb);
}
h3 {
font-size: clamp(1.125rem, 2.5vw, 1.25rem);
font-size: clamp(1rem, 2vw, 1.125rem);
line-height: 1.4;
margin-bottom: 0.75rem;
margin-top: 0;
margin-bottom: 0.625rem;
margin-top: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) h3 {
color: var(--fog-dark-text, #f9fafb);
}
h4, h5, h6 {
@ -130,6 +148,210 @@ h4, h5, h6 { @@ -130,6 +148,210 @@ h4, h5, h6 {
margin-top: 0;
}
/* Common main container */
main {
max-width: var(--content-width);
margin: 0 auto;
}
/* Common loading and empty states */
.loading-state,
.empty-state {
padding: 2rem;
text-align: center;
color: var(--fog-text, #1f2937);
}
:global(.dark) .loading-state,
:global(.dark) .empty-state {
color: var(--fog-dark-text, #f9fafb);
}
/* Common button styles */
.write-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
text-decoration: none;
transition: all 0.2s;
min-width: 2.5rem;
min-height: 2.5rem;
flex-shrink: 0;
cursor: pointer;
}
:global(.dark) .write-button {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
}
.write-button:hover {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .write-button:hover {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-accent, #94a3b8);
}
/* Action buttons (accent background) */
.find-button,
.create-rss-button,
.edit-rss-button,
.bulk-action-button,
.load-more-button,
.clear-kind-button {
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875em;
font-weight: 500;
text-decoration: none;
display: inline-block;
transition: opacity 0.2s;
}
:global(.dark) .find-button,
:global(.dark) .create-rss-button,
:global(.dark) .edit-rss-button,
:global(.dark) .bulk-action-button,
:global(.dark) .load-more-button,
:global(.dark) .clear-kind-button {
background: var(--fog-dark-accent, #94a3b8);
}
.find-button:hover:not(:disabled),
.create-rss-button:hover,
.edit-rss-button:hover,
.bulk-action-button:hover,
.load-more-button:hover:not(:disabled),
.clear-kind-button:hover {
opacity: 0.9;
}
.find-button:disabled,
.load-more-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Tab buttons */
.tab-button {
padding: 0.75rem 1.5rem;
border: none;
background: transparent;
color: var(--fog-text-light, #6b7280);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
font-size: 1rem;
}
:global(.dark) .tab-button {
color: var(--fog-dark-text-light, #9ca3af);
}
.tab-button:hover {
color: var(--fog-text, #1f2937);
}
:global(.dark) .tab-button:hover {
color: var(--fog-dark-text, #f9fafb);
}
.tab-button.active {
color: var(--fog-text, #1f2937);
border-bottom-color: var(--fog-accent, #64748b);
}
:global(.dark) .tab-button.active {
color: var(--fog-dark-text, #f9fafb);
border-bottom-color: var(--fog-dark-accent, #94a3b8);
}
/* Common emoji styles */
.emoji {
font-size: 1.25rem;
line-height: 1;
}
.emoji-grayscale {
filter: grayscale(100%);
opacity: 0.7;
}
/* Common button styles */
.action-button {
padding: 0.5rem 1rem;
background: var(--fog-highlight, #f3f4f6);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
color: var(--fog-text, #1f2937);
cursor: pointer;
font-size: 0.875em;
text-decoration: none;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
:global(.dark) .action-button {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-border, #475569);
color: var(--fog-dark-text, #f9fafb);
}
.action-button:hover {
background: var(--fog-accent, #64748b);
color: white;
border-color: var(--fog-accent, #64748b);
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Common form elements */
.filter-select,
.filter-input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
font-size: 0.875em;
}
:global(.dark) .filter-select,
:global(.dark) .filter-input {
background: var(--fog-dark-post, #334155);
border-color: var(--fog-dark-border, #475569);
color: var(--fog-dark-text, #f9fafb);
}
.filter-label {
font-size: 0.875em;
font-weight: 500;
color: var(--fog-text, #1f2937);
}
:global(.dark) .filter-label {
color: var(--fog-dark-text, #f9fafb);
}
/* Apply monospace font to all elements globally */
* {
font-family: inherit;

16
src/lib/components/layout/Header.svelte

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
import { sessionManager, type UserSession } from '../../services/auth/session-manager.js';
import UserPreferences from '../preferences/UserPreferences.svelte';
import ProfileBadge from '../layout/ProfileBadge.svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
let currentSession = $state<UserSession | null>(sessionManager.session.value);
let isLoggedIn = $derived(currentSession !== null);
@ -15,6 +17,18 @@ @@ -15,6 +17,18 @@
return unsubscribe;
});
function handleLogout() {
// Store current route before logging out
const currentRoute = $page.url.pathname + $page.url.search;
sessionManager.clearSession(currentRoute);
// Redirect to stored login redirect if available, otherwise stay on current page
const loginRedirect = sessionManager.getLoginRedirect();
if (loginRedirect) {
goto(loginRedirect);
}
}
</script>
<header class="relative border-b border-fog-border dark:border-fog-dark-border">
@ -56,7 +70,7 @@ @@ -56,7 +70,7 @@
<UserPreferences />
<ProfileBadge pubkey={currentPubkey} />
<button
onclick={() => sessionManager.clearSession()}
onclick={handleLogout}
class="px-3 py-1 rounded border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight transition-colors"
title="Logout"
aria-label="Logout"

351
src/lib/modules/feed/FeedPage.svelte

@ -45,11 +45,8 @@ @@ -45,11 +45,8 @@
let { singleRelay }: Props = $props();
let posts = $state<NostrEvent[]>([]);
let allPosts = $state<NostrEvent[]>([]); // Store all posts before filtering
let highlights = $state<NostrEvent[]>([]); // Store highlight events (kind 9802)
let allHighlights = $state<NostrEvent[]>([]); // Store all highlights before filtering
let otherFeedEvents = $state<NostrEvent[]>([]); // Store other feed kinds (not kind 1 or 9802)
let allOtherFeedEvents = $state<NostrEvent[]>([]); // Store all other feed events before filtering
let loading = $state(true);
let loadingMore = $state(false);
let hasMore = $state(true);
@ -65,11 +62,6 @@ @@ -65,11 +62,6 @@
const allEvents = $derived.by(() => [...posts, ...highlights, ...otherFeedEvents].sort((a, b) => b.created_at - a.created_at));
const visibleEvents = $derived.by(() => allEvents.slice(0, visibleItemCount));
// List filter state
let availableLists = $state<Array<{ kind: number; name: string; event: NostrEvent }>>([]);
let selectedListId = $state<string | null>(null); // Format: "kind:eventId"
let listFilterIds = $state<Set<string>>(new Set()); // Event IDs or pubkeys to filter by
// Batch-loaded parent events: eventId -> parentEvent
let parentEventsMap = $state<Map<string, NostrEvent>>(new Map());
@ -109,8 +101,6 @@ @@ -109,8 +101,6 @@
isMounted = true;
await nostrClient.initialize();
if (!isMounted) return; // Check if unmounted during init
await loadUserLists();
if (!isMounted) return;
await loadFeed();
if (!isMounted) return;
// Set up persistent subscription for new events (only once)
@ -121,144 +111,8 @@ @@ -121,144 +111,8 @@
}
});
// Load user lists for filtering
async function loadUserLists() {
// Don't load user lists for single relay mode
if (singleRelay) {
return;
}
const session = sessionManager.getSession();
if (!session) return;
const listKinds = [
KIND.CONTACTS,
KIND.FAVORITE_RELAYS,
KIND.RELAY_LIST,
KIND.LOCAL_RELAYS,
KIND.PIN_LIST,
KIND.BOOKMARKS,
KIND.INTEREST_LIST,
KIND.FOLOW_SET
];
try {
const relays = relayManager.getProfileReadRelays();
const lists: Array<{ kind: number; name: string; event: NostrEvent }> = [];
// Fetch all list types
for (const kind of listKinds) {
const limit = kind === KIND.FOLOW_SET ? 50 : 1; // Multiple follow sets allowed
const events = await nostrClient.fetchEvents(
[{ kinds: [kind], authors: [session.pubkey], limit }],
relays,
{ useCache: true, cacheResults: true }
);
const kindName = getKindName(kind);
for (const event of events) {
lists.push({
kind,
name: `${kindName}${kind === KIND.FOLOW_SET ? ` (${new Date(event.created_at * 1000).toLocaleDateString()})` : ''}`,
event
});
}
}
availableLists = lists;
} catch (error) {
console.error('Error loading user lists:', error);
}
}
function getKindName(kind: number): string {
const names: Record<number, string> = {
[KIND.CONTACTS]: 'Contacts',
[KIND.FAVORITE_RELAYS]: 'Favorite Relays',
[KIND.RELAY_LIST]: 'Relay List',
[KIND.LOCAL_RELAYS]: 'Local Relays',
[KIND.PIN_LIST]: 'Pin List',
[KIND.BOOKMARKS]: 'Bookmarks',
[KIND.INTEREST_LIST]: 'Interest List',
[KIND.FOLOW_SET]: 'Follow Set'
};
return names[kind] || `Kind ${kind}`;
}
function handleListFilterChange(listId: string | null) {
selectedListId = listId;
if (!listId) {
// No filter selected - show all posts, highlights, and other feed events
listFilterIds = new Set();
posts = [...allPosts];
highlights = [...allHighlights];
otherFeedEvents = [...allOtherFeedEvents];
return;
}
// Find the selected list
const [kindStr, eventId] = listId.split(':');
const kind = parseInt(kindStr, 10);
const list = availableLists.find(l => l.kind === kind && l.event.id === eventId);
if (!list) {
listFilterIds = new Set();
posts = [...allPosts];
highlights = [...allHighlights];
otherFeedEvents = [...allOtherFeedEvents];
return;
}
// Extract IDs from the list
const ids = new Set<string>();
// For contacts and follow sets, extract pubkeys from 'p' tags
if (kind === KIND.CONTACTS || kind === KIND.FOLOW_SET) {
for (const tag of list.event.tags) {
if (tag[0] === 'p' && tag[1]) {
ids.add(tag[1]);
}
}
} else {
// For other lists, extract event IDs from 'e' and 'a' tags
for (const tag of list.event.tags) {
if (tag[0] === 'e' && tag[1]) {
ids.add(tag[1]);
} else if (tag[0] === 'a' && tag[1]) {
// For 'a' tags, we'd need to resolve them to event IDs
// For now, we'll just use the 'a' tag value as-is
// This is a simplified approach - full implementation would resolve 'a' tags
ids.add(tag[1]);
}
}
}
listFilterIds = ids;
// Filter posts, highlights, and other feed events
if (kind === KIND.CONTACTS || kind === KIND.FOLOW_SET) {
// Filter by author pubkey
posts = allPosts.filter(post => ids.has(post.pubkey));
highlights = allHighlights.filter(highlight => ids.has(highlight.pubkey));
otherFeedEvents = allOtherFeedEvents.filter(event => ids.has(event.pubkey));
} else {
// Filter by event ID
posts = allPosts.filter(post => ids.has(post.id));
highlights = allHighlights.filter(highlight => ids.has(highlight.id));
otherFeedEvents = allOtherFeedEvents.filter((event: NostrEvent) => ids.has(event.id));
}
}
// Apply filter when allPosts, allHighlights, or allOtherFeedEvents changes
// Reset visible count when data changes (new feed loaded)
$effect(() => {
if (selectedListId) {
handleListFilterChange(selectedListId);
} else {
posts = [...allPosts];
highlights = [...allHighlights];
otherFeedEvents = [...allOtherFeedEvents];
}
// Reset visible count when data changes (new feed loaded)
visibleItemCount = INITIAL_RENDER_LIMIT;
});
@ -554,16 +408,14 @@ @@ -554,16 +408,14 @@
getKindInfo(e.kind).showInFeed === true
);
// Sort by created_at descending and deduplicate
// Deduplicate (relays already return events in reverse chronological order)
const uniquePostsMap = new Map<string, NostrEvent>();
for (const event of postsList) {
if (!uniquePostsMap.has(event.id)) {
uniquePostsMap.set(event.id, event);
}
}
const uniquePosts = Array.from(uniquePostsMap.values());
const sortedPosts = uniquePosts.sort((a, b) => b.created_at - a.created_at);
allPosts = sortedPosts;
posts = Array.from(uniquePostsMap.values());
const uniqueHighlightsMap = new Map<string, NostrEvent>();
for (const event of highlightsList) {
@ -571,9 +423,7 @@ @@ -571,9 +423,7 @@
uniqueHighlightsMap.set(event.id, event);
}
}
const uniqueHighlights = Array.from(uniqueHighlightsMap.values());
const sortedHighlights = uniqueHighlights.sort((a, b) => b.created_at - a.created_at);
allHighlights = sortedHighlights;
highlights = Array.from(uniqueHighlightsMap.values());
// Store other feed events
const uniqueOtherMap = new Map<string, NostrEvent>();
@ -582,42 +432,29 @@ @@ -582,42 +432,29 @@
uniqueOtherMap.set(event.id, event);
}
}
const uniqueOther = Array.from(uniqueOtherMap.values());
const sortedOther = uniqueOther.sort((a, b) => b.created_at - a.created_at);
allOtherFeedEvents = sortedOther;
// Always set posts, highlights, and other feed events immediately, even if empty
// This ensures cached data shows up right away
// Apply filter if one is selected
if (selectedListId) {
handleListFilterChange(selectedListId);
} else {
posts = [...allPosts];
highlights = [...allHighlights];
otherFeedEvents = [...allOtherFeedEvents];
}
otherFeedEvents = Array.from(uniqueOtherMap.values());
// Set loading to false immediately after showing cached data
// This allows the UI to render while fresh data loads in background
loading = false;
console.log(`[FeedPage] Loaded ${sortedPosts.length} posts and ${sortedHighlights.length} highlights`);
console.log(`[FeedPage] Loaded ${posts.length} posts and ${highlights.length} highlights`);
if (sortedPosts.length > 0 || sortedHighlights.length > 0) {
const allTimestamps = [...sortedPosts.map(e => e.created_at), ...sortedHighlights.map(e => e.created_at)];
if (posts.length > 0 || highlights.length > 0) {
const allTimestamps = [...posts.map(e => e.created_at), ...highlights.map(e => e.created_at)];
oldestTimestamp = Math.min(...allTimestamps);
// Load secondary data (reactions, profiles, etc.) AFTER posts are displayed
// Collect all post IDs and pubkeys first, then batch fetch everything
const allPostIds = [
...sortedPosts.map(p => p.id),
...sortedHighlights.map(p => p.id),
...sortedOther.map(p => p.id)
...posts.map(p => p.id),
...highlights.map(p => p.id),
...otherFeedEvents.map(p => p.id)
];
const allPubkeys = new Set<string>();
sortedPosts.forEach(p => allPubkeys.add(p.pubkey));
sortedHighlights.forEach(p => allPubkeys.add(p.pubkey));
sortedOther.forEach(p => allPubkeys.add(p.pubkey));
posts.forEach(p => allPubkeys.add(p.pubkey));
highlights.forEach(p => allPubkeys.add(p.pubkey));
otherFeedEvents.forEach(p => allPubkeys.add(p.pubkey));
// Use requestIdleCallback or setTimeout to defer loading so posts render first
const deferSecondaryData = () => {
@ -626,9 +463,9 @@ @@ -626,9 +463,9 @@
// Batch load all secondary data in parallel using collected IDs/pubkeys
// Note: Reactions are handled by FeedPost component itself
const promise = Promise.all([
loadParentAndQuotedEvents(sortedPosts),
loadZapCountsForPosts(sortedPosts),
loadProfilesForPosts(sortedPosts)
loadParentAndQuotedEvents(posts),
loadZapCountsForPosts(posts),
loadProfilesForPosts(posts)
]).catch(error => {
if (isMounted) {
console.error('[FeedPage] Error loading secondary data:', error);
@ -716,17 +553,17 @@ @@ -716,17 +553,17 @@
);
// Filter out duplicates
const existingPostIds = new Set(allPosts.map(p => p.id));
const existingHighlightIds = new Set(allHighlights.map(h => h.id));
const existingOtherIds = new Set(allOtherFeedEvents.map((e: NostrEvent) => e.id));
const existingPostIds = new Set(posts.map(p => p.id));
const existingHighlightIds = new Set(highlights.map(h => h.id));
const existingOtherIds = new Set(otherFeedEvents.map((e: NostrEvent) => e.id));
const uniqueNewPosts = newPosts.filter(e => !existingPostIds.has(e.id));
const uniqueNewHighlights = newHighlights.filter(e => !existingHighlightIds.has(e.id));
const uniqueNewOther = newOtherFeedEvents.filter((e: NostrEvent) => !existingOtherIds.has(e.id));
if (uniqueNewPosts.length > 0 || uniqueNewHighlights.length > 0) {
if (uniqueNewPosts.length > 0) {
const sorted = uniqueNewPosts.sort((a, b) => b.created_at - a.created_at);
allPosts = [...allPosts, ...sorted];
// Append new posts (they're already in reverse chronological order from relays)
posts = [...posts, ...uniqueNewPosts];
// Load secondary data with low priority after posts are displayed
const secondaryDataPromise = new Promise<void>((resolve) => {
setTimeout(() => {
@ -736,9 +573,9 @@ @@ -736,9 +573,9 @@
}
// Note: Reactions are handled by FeedPost component itself
const promise = Promise.all([
loadParentAndQuotedEvents(sorted),
loadZapCountsForPosts(sorted),
loadProfilesForPosts(sorted)
loadParentAndQuotedEvents(uniqueNewPosts),
loadZapCountsForPosts(uniqueNewPosts),
loadProfilesForPosts(uniqueNewPosts)
]).catch(error => {
if (isMounted) { // Only log if still mounted
console.error('[FeedPage] Error loading secondary data for new posts:', error);
@ -754,23 +591,13 @@ @@ -754,23 +591,13 @@
}
if (uniqueNewHighlights.length > 0) {
const sorted = uniqueNewHighlights.sort((a, b) => b.created_at - a.created_at);
allHighlights = [...allHighlights, ...sorted];
// Append new highlights (they're already in reverse chronological order from relays)
highlights = [...highlights, ...uniqueNewHighlights];
}
if (uniqueNewOther.length > 0) {
const sorted = uniqueNewOther.sort((a, b) => b.created_at - a.created_at);
allOtherFeedEvents = [...allOtherFeedEvents, ...sorted];
otherFeedEvents = [...allOtherFeedEvents];
}
// Apply filter if one is selected
if (selectedListId) {
handleListFilterChange(selectedListId);
} else {
posts = [...allPosts];
highlights = [...allHighlights];
otherFeedEvents = [...allOtherFeedEvents];
// Append new other events (they're already in reverse chronological order from relays)
otherFeedEvents = [...otherFeedEvents, ...uniqueNewOther];
}
const allNewTimestamps = [...uniqueNewPosts.map(e => e.created_at), ...uniqueNewHighlights.map(e => e.created_at)];
@ -808,9 +635,9 @@ @@ -808,9 +635,9 @@
// Deduplicate incoming updates before adding to pending
// Check against all feed event types
const existingIds = new Set([
...allPosts.map(p => p.id),
...allHighlights.map(h => h.id),
...allOtherFeedEvents.map(e => e.id)
...posts.map((p: NostrEvent) => p.id),
...highlights.map((h: NostrEvent) => h.id),
...otherFeedEvents.map((e: NostrEvent) => e.id)
]);
const newUpdates = updated.filter(e => e && e.id && !existingIds.has(e.id));
@ -841,9 +668,9 @@ @@ -841,9 +668,9 @@
// Final deduplication check against all feed event types (may have changed)
const currentIds = new Set([
...allPosts.map(p => p.id),
...allHighlights.map(h => h.id),
...allOtherFeedEvents.map(e => e.id)
...posts.map(p => p.id),
...highlights.map(h => h.id),
...otherFeedEvents.map(e => e.id)
]);
const newEvents = pendingUpdates.filter(e => e && e.id && !currentIds.has(e.id));
@ -852,7 +679,7 @@ @@ -852,7 +679,7 @@
return;
}
console.log(`[FeedPage] Processing ${newEvents.length} new events, existing: ${allPosts.length}`);
console.log(`[FeedPage] Processing ${newEvents.length} new events, existing: ${posts.length}`);
// Separate events by kind
const newPosts = newEvents.filter(e => e.kind === KIND.SHORT_TEXT_NOTE);
@ -863,9 +690,9 @@ @@ -863,9 +690,9 @@
getKindInfo(e.kind).showInFeed === true
);
// Merge and sort posts, then deduplicate by ID
// Merge and deduplicate posts (relays already return in reverse chronological order)
if (newPosts.length > 0) {
const mergedPosts = [...allPosts, ...newPosts];
const mergedPosts = [...posts, ...newPosts];
const uniquePostsMap = new Map<string, NostrEvent>();
for (const event of mergedPosts) {
if (event && event.id && !uniquePostsMap.has(event.id)) {
@ -873,17 +700,16 @@ @@ -873,17 +700,16 @@
}
}
const uniquePosts = Array.from(uniquePostsMap.values());
const sortedPosts = uniquePosts.sort((a, b) => b.created_at - a.created_at);
// Only update if we actually have new events to prevent loops
if (sortedPosts.length > allPosts.length || sortedPosts.some((e, i) => e.id !== allPosts[i]?.id)) {
allPosts = sortedPosts;
if (uniquePosts.length > posts.length || uniquePosts.some((e, i) => e.id !== posts[i]?.id)) {
posts = uniquePosts;
}
}
// Merge and sort highlights, then deduplicate by ID
// Merge and deduplicate highlights (relays already return in reverse chronological order)
if (newHighlights.length > 0) {
const mergedHighlights = [...allHighlights, ...newHighlights];
const mergedHighlights = [...highlights, ...newHighlights];
const uniqueHighlightsMap = new Map<string, NostrEvent>();
for (const event of mergedHighlights) {
if (event && event.id && !uniqueHighlightsMap.has(event.id)) {
@ -891,17 +717,16 @@ @@ -891,17 +717,16 @@
}
}
const uniqueHighlights = Array.from(uniqueHighlightsMap.values());
const sortedHighlights = uniqueHighlights.sort((a, b) => b.created_at - a.created_at);
// Only update if we actually have new events to prevent loops
if (sortedHighlights.length > allHighlights.length || sortedHighlights.some((e, i) => e.id !== allHighlights[i]?.id)) {
allHighlights = sortedHighlights;
if (uniqueHighlights.length > highlights.length || uniqueHighlights.some((e, i) => e.id !== highlights[i]?.id)) {
highlights = uniqueHighlights;
}
}
// Merge and sort other feed events, then deduplicate by ID
// Merge and deduplicate other feed events (relays already return in reverse chronological order)
if (newOtherFeedEvents.length > 0) {
const mergedOther = [...allOtherFeedEvents, ...newOtherFeedEvents];
const mergedOther = [...otherFeedEvents, ...newOtherFeedEvents];
const uniqueOtherMap = new Map<string, NostrEvent>();
for (const event of mergedOther) {
if (event && event.id && !uniqueOtherMap.has(event.id)) {
@ -909,24 +734,14 @@ @@ -909,24 +734,14 @@
}
}
const uniqueOther = Array.from(uniqueOtherMap.values());
const sortedOther = uniqueOther.sort((a, b) => b.created_at - a.created_at);
// Only update if we actually have new events to prevent loops
if (sortedOther.length > allOtherFeedEvents.length || sortedOther.some((e, i) => e.id !== allOtherFeedEvents[i]?.id)) {
allOtherFeedEvents = sortedOther;
if (uniqueOther.length > otherFeedEvents.length || uniqueOther.some((e, i) => e.id !== otherFeedEvents[i]?.id)) {
otherFeedEvents = uniqueOther;
}
}
// Apply filter if one is selected
if (selectedListId) {
handleListFilterChange(selectedListId);
} else {
posts = [...allPosts];
highlights = [...allHighlights];
otherFeedEvents = [...allOtherFeedEvents];
}
console.debug(`[FeedPage] Updated: ${allPosts.length} posts, ${allHighlights.length} highlights`);
console.debug(`[FeedPage] Updated: ${posts.length} posts, ${highlights.length} highlights`);
pendingUpdates = [];
}, 500);
@ -1127,23 +942,6 @@ @@ -1127,23 +942,6 @@
</script>
<div class="feed-page">
{#if !loading && availableLists.length > 0 && !singleRelay}
<div class="feed-filter">
<label for="list-filter" class="filter-label">Filter by list:</label>
<select
id="list-filter"
bind:value={selectedListId}
onchange={(e) => handleListFilterChange((e.target as HTMLSelectElement).value || null)}
class="filter-select"
>
<option value="">All Posts</option>
{#each availableLists as list}
<option value="{list.kind}:{list.event.id}">{list.name}</option>
{/each}
</select>
</div>
{/if}
{#if singleRelay}
<div class="relay-info">
<p class="relay-info-text">
@ -1166,11 +964,7 @@ @@ -1166,11 +964,7 @@
{:else if posts.length === 0 && highlights.length === 0 && otherFeedEvents.length === 0}
<div class="empty-state">
<p class="text-fog-text dark:text-fog-dark-text">
{#if selectedListId}
No posts found in selected list.
{:else}
No posts found. Check back later!
{/if}
No posts found. Check back later!
</p>
</div>
{:else}
@ -1225,51 +1019,6 @@ @@ -1225,51 +1019,6 @@
max-width: 100%;
}
.feed-filter {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.filter-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--fog-text, #1f2937);
}
:global(.dark) .filter-label {
color: var(--fog-dark-text, #f9fafb);
}
.filter-select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
font-size: 0.875rem;
cursor: pointer;
min-width: 200px;
}
.filter-select:focus {
outline: none;
border-color: var(--fog-accent, #64748b);
box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.1);
}
:global(.dark) .filter-select {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f9fafb);
}
:global(.dark) .filter-select:focus {
border-color: var(--fog-dark-accent, #94a3b8);
box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.1);
}
.loading-state,
.empty-state,
.error-state {

43
src/lib/services/auth/session-manager.ts

@ -99,8 +99,14 @@ class SessionManager { @@ -99,8 +99,14 @@ class SessionManager {
/**
* Clear session
* Also clears password from memory for security
* Stores current route for redirect after logout
*/
clearSession(): void {
clearSession(currentRoute?: string): void {
// Store current route for redirect after logout
if (typeof window !== 'undefined' && currentRoute) {
localStorage.setItem('aitherboard_logout_redirect', currentRoute);
}
// Clear password from memory if it exists
if (this.currentSession?.password) {
// Overwrite password in memory (though JS doesn't guarantee this)
@ -113,6 +119,41 @@ class SessionManager { @@ -113,6 +119,41 @@ class SessionManager {
}
}
/**
* Get stored redirect route after logout
*/
getLogoutRedirect(): string | null {
if (typeof window === 'undefined') return null;
const stored = localStorage.getItem('aitherboard_logout_redirect');
if (stored) {
localStorage.removeItem('aitherboard_logout_redirect');
return stored;
}
return null;
}
/**
* Store current route for redirect after login
*/
storeLoginRedirect(route: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem('aitherboard_login_redirect', route);
}
}
/**
* Get stored redirect route after login
*/
getLoginRedirect(): string | null {
if (typeof window === 'undefined') return null;
const stored = localStorage.getItem('aitherboard_login_redirect');
if (stored) {
localStorage.removeItem('aitherboard_login_redirect');
return stored;
}
return null;
}
/**
* Restore session from localStorage
* This will attempt to restore the session based on the auth method

21
src/routes/+layout.svelte

@ -3,6 +3,14 @@ @@ -3,6 +3,14 @@
import { sessionManager } from '../lib/services/auth/session-manager.js';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { page } from '$app/stores';
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
}
let { children }: Props = $props();
// Restore session immediately if in browser (before onMount)
if (browser) {
@ -31,6 +39,17 @@ @@ -31,6 +39,17 @@
console.error('Failed to restore session:', error);
}
});
// Track current route when user is logged in (for redirect after logout)
$effect(() => {
if (sessionManager.isLoggedIn() && browser) {
const currentRoute = $page.url.pathname + $page.url.search;
// Don't store /login as the route to return to
if (currentRoute !== '/login') {
sessionManager.storeLoginRedirect(currentRoute);
}
}
});
</script>
<slot />
{@render children()}

170
src/routes/cache/+page.svelte vendored

@ -425,7 +425,7 @@ @@ -425,7 +425,7 @@
<main class="container mx-auto px-4 py-8">
<div class="cache-page">
<h1 class="page-title font-mono">/Cache</h1>
<h1 class="font-mono">/Cache</h1>
{#if loading && !stats}
<div class="loading-state">
@ -434,7 +434,7 @@ @@ -434,7 +434,7 @@
{:else if stats}
<!-- Statistics -->
<div class="stats-section">
<h2 class="section-title">Statistics</h2>
<h2>Statistics</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Total Events</div>
@ -457,7 +457,7 @@ @@ -457,7 +457,7 @@
<!-- Events by Kind -->
{#if stats.eventsByKind.size > 0}
<div class="kind-stats">
<h3 class="subsection-title">Events by Kind</h3>
<h3>Events by Kind</h3>
<div class="kind-list">
{#each Array.from(stats.eventsByKind.entries()).sort((a, b) => b[1] - a[1]) as [kind, count]}
<div class="kind-item" onclick={() => handleKindClick(kind)} role="button" tabindex="0" onkeydown={(e) => e.key === 'Enter' && handleKindClick(kind)}>
@ -479,7 +479,7 @@ @@ -479,7 +479,7 @@
<!-- Filters -->
<div class="filters-section">
<h2 class="section-title">Filters</h2>
<h2>Filters</h2>
<div class="filters-grid">
<div class="filter-group">
<label for="kind-filter" class="filter-label">Kind</label>
@ -524,7 +524,7 @@ @@ -524,7 +524,7 @@
<!-- Bulk Actions -->
<div class="bulk-actions-section">
<h2 class="section-title">Bulk Actions</h2>
<h2>Bulk Actions</h2>
<div class="bulk-actions">
<button class="bulk-action-button" onclick={handleClearAll}>
Clear All Cache
@ -543,7 +543,7 @@ @@ -543,7 +543,7 @@
<!-- Events List -->
<div class="events-section">
<h2 class="section-title">Cached Events ({events.length})</h2>
<h2>Cached Events ({events.length})</h2>
{#if loading && events.length === 0}
<div class="loading-state">
@ -660,49 +660,10 @@ @@ -660,49 +660,10 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.cache-page {
max-width: 100%;
}
.page-title {
font-size: clamp(1.5rem, 4vw, 2rem);
font-weight: 700;
margin-bottom: 2rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .page-title {
color: var(--fog-dark-text, #f9fafb);
}
.section-title {
font-size: clamp(1.25rem, 3vw, 1.5rem);
font-weight: 600;
margin-bottom: 1rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .section-title {
color: var(--fog-dark-text, #f9fafb);
}
.subsection-title {
font-size: clamp(1.125rem, 2.5vw, 1.25rem);
font-weight: 600;
margin-bottom: 0.75rem;
margin-top: 1.5rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .subsection-title {
color: var(--fog-dark-text, #f9fafb);
}
.stats-section,
.filters-section,
.bulk-actions-section,
@ -752,7 +713,7 @@ @@ -752,7 +713,7 @@
}
.stat-value {
font-size: clamp(1.125rem, 2.5vw, 1.5rem);
font-size: clamp(1rem, 2vw, 1.25rem);
font-weight: 600;
color: var(--fog-text, #1f2937);
}
@ -812,19 +773,6 @@ @@ -812,19 +773,6 @@
color: var(--fog-dark-text-light, #9ca3af);
}
.clear-kind-button {
padding: 0.25rem 0.75rem;
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875em;
}
.clear-kind-button:hover {
opacity: 0.9;
}
.filters-grid {
display: grid;
@ -838,53 +786,12 @@ @@ -838,53 +786,12 @@
gap: 0.5rem;
}
.filter-label {
font-size: 0.875em;
font-weight: 500;
color: var(--fog-text, #1f2937);
}
:global(.dark) .filter-label {
color: var(--fog-dark-text, #f9fafb);
}
.filter-select,
.filter-input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
font-size: 0.875em;
}
:global(.dark) .filter-select,
:global(.dark) .filter-input {
background: var(--fog-dark-post, #334155);
border-color: var(--fog-dark-border, #475569);
color: var(--fog-dark-text, #f9fafb);
}
.bulk-actions {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.bulk-action-button {
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875em;
font-weight: 500;
}
.bulk-action-button:hover {
opacity: 0.9;
}
.events-list {
display: flex;
@ -1041,34 +948,6 @@ @@ -1041,34 +948,6 @@
color: var(--fog-dark-text-light, #9ca3af);
}
.action-button {
padding: 0.5rem 1rem;
background: var(--fog-highlight, #f3f4f6);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
color: var(--fog-text, #1f2937);
cursor: pointer;
font-size: 0.875em;
text-decoration: none;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
:global(.dark) .action-button {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-border, #475569);
color: var(--fog-dark-text, #f9fafb);
}
.action-button:hover {
background: var(--fog-accent, #64748b);
color: white;
border-color: var(--fog-accent, #64748b);
}
.action-button.delete-action:hover {
background: #ef4444;
border-color: #ef4444;
@ -1085,45 +964,10 @@ @@ -1085,45 +964,10 @@
border-color: #d97706;
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.load-more-section {
text-align: center;
margin-top: 2rem;
}
.load-more-button {
padding: 0.75rem 2rem;
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875em;
font-weight: 500;
}
.load-more-button:hover:not(:disabled) {
opacity: 0.9;
}
.load-more-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading-state,
.empty-state {
padding: 2rem;
text-align: center;
color: var(--fog-text, #1f2937);
}
:global(.dark) .loading-state,
:global(.dark) .empty-state {
color: var(--fog-dark-text, #f9fafb);
}
</style>

46
src/routes/discussions/+page.svelte

@ -31,55 +31,9 @@ @@ -31,55 +31,9 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.discussions-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.write-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
text-decoration: none;
transition: all 0.2s;
min-width: 2.5rem;
min-height: 2.5rem;
}
:global(.dark) .write-button {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
}
.write-button:hover {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .write-button:hover {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-accent, #94a3b8);
}
.emoji {
font-size: 1.25rem;
line-height: 1;
}
.emoji-grayscale {
filter: grayscale(100%);
opacity: 0.7;
}
</style>

47
src/routes/feed/+page.svelte

@ -28,11 +28,6 @@ @@ -28,11 +28,6 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.feed-header {
display: flex;
flex-direction: column;
@ -49,46 +44,4 @@ @@ -49,46 +44,4 @@
.search-section {
flex: 1;
}
.write-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
text-decoration: none;
transition: all 0.2s;
min-width: 2.5rem;
min-height: 2.5rem;
flex-shrink: 0;
}
:global(.dark) .write-button {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
}
.write-button:hover {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .write-button:hover {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-accent, #94a3b8);
}
.emoji {
font-size: 1.25rem;
line-height: 1;
}
.emoji-grayscale {
filter: grayscale(100%);
opacity: 0.7;
}
</style>

4
src/routes/feed/relay/[relay]/+page.svelte

@ -84,10 +84,6 @@ @@ -84,10 +84,6 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.error-state,
.loading-state {

40
src/routes/find/+page.svelte

@ -116,7 +116,7 @@ @@ -116,7 +116,7 @@
<!-- Find User Section -->
<section class="find-section">
<h2 class="section-title">Find User</h2>
<h2>Find User</h2>
<p class="section-description">Enter a user ID (NIP-05, hex pubkey, npub, or nprofile)</p>
<div class="input-group">
@ -150,11 +150,6 @@ @@ -150,11 +150,6 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.find-page {
max-width: 800px;
margin: 0 auto;
@ -178,18 +173,6 @@ @@ -178,18 +173,6 @@
background: var(--fog-dark-post, #1f2937);
}
.section-title {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
font-family: monospace;
}
:global(.dark) .section-title {
color: var(--fog-dark-text, #f9fafb);
}
.section-description {
margin: 0 0 1.5rem 0;
color: var(--fog-text-light, #6b7280);
@ -228,30 +211,9 @@ @@ -228,30 +211,9 @@
}
.find-button {
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
font-family: monospace;
}
:global(.dark) .find-button {
background: var(--fog-dark-accent, #94a3b8);
}
.find-button:hover:not(:disabled) {
opacity: 0.9;
}
.find-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-message {
margin-top: 1rem;
padding: 0.75rem;

22
src/routes/login/+page.svelte

@ -7,6 +7,8 @@ @@ -7,6 +7,8 @@
import { nip19 } from 'nostr-tools';
import { listNsecKeys } from '../../lib/services/cache/nsec-key-store.js';
import { listAnonymousKeys } from '../../lib/services/cache/anonymous-key-store.js';
import { sessionManager } from '../../lib/services/auth/session-manager.js';
import { page } from '$app/stores';
onMount(async () => {
await nostrClient.initialize();
@ -69,7 +71,9 @@ @@ -69,7 +71,9 @@
try {
await authenticateWithNIP07();
goto('/');
// Redirect to stored route or default to home
const redirectRoute = sessionManager.getLogoutRedirect() || '/';
goto(redirectRoute);
} catch (err) {
error = err instanceof Error ? err.message : 'Authentication failed';
} finally {
@ -98,7 +102,9 @@ @@ -98,7 +102,9 @@
nsecPassword = '';
selectedNsecKey = null;
goto('/');
// Redirect to stored route or default to home
const redirectRoute = sessionManager.getLogoutRedirect() || '/';
goto(redirectRoute);
} catch (err) {
error = err instanceof Error ? err.message : 'Authentication failed';
// Clear sensitive data on error
@ -189,7 +195,9 @@ @@ -189,7 +195,9 @@
// Reload stored keys
await loadStoredKeys();
goto('/');
// Redirect to stored route or default to home
const redirectRoute = sessionManager.getLogoutRedirect() || '/';
goto(redirectRoute);
} catch (err) {
error = err instanceof Error ? err.message : 'Authentication failed';
// Clear sensitive data on error
@ -222,7 +230,9 @@ @@ -222,7 +230,9 @@
anonymousPassword = '';
selectedAnonymousKey = null;
goto('/');
// Redirect to stored route or default to home
const redirectRoute = sessionManager.getLogoutRedirect() || '/';
goto(redirectRoute);
} catch (err) {
error = err instanceof Error ? err.message : 'Authentication failed';
// Clear sensitive data on error
@ -260,7 +270,9 @@ @@ -260,7 +270,9 @@
// Reload stored keys
await loadStoredKeys();
goto('/');
// Redirect to stored route or default to home
const redirectRoute = sessionManager.getLogoutRedirect() || '/';
goto(redirectRoute);
} catch (err) {
error = err instanceof Error ? err.message : 'Authentication failed';
// Clear sensitive data on error

4
src/routes/relay/+page.svelte

@ -160,10 +160,6 @@ @@ -160,10 +160,6 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.relay-page {
max-width: 1000px;

4
src/routes/replaceable/[d_tag]/+page.svelte

@ -138,10 +138,6 @@ @@ -138,10 +138,6 @@
{/if}
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.replaceable-header {
border-bottom: 1px solid var(--fog-border, #e5e7eb);

4
src/routes/repos/+page.svelte

@ -202,10 +202,6 @@ @@ -202,10 +202,6 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.repos-header {
border-bottom: 1px solid var(--fog-border, #e5e7eb);

36
src/routes/repos/[naddr]/+page.svelte

@ -1068,10 +1068,6 @@ @@ -1068,10 +1068,6 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.loading-state,
.empty-state {
@ -1100,38 +1096,6 @@ @@ -1100,38 +1096,6 @@
border-bottom-color: var(--fog-dark-border, #374151);
}
.tab-button {
padding: 0.75rem 1.5rem;
border: none;
background: transparent;
color: var(--fog-text-light, #6b7280);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
font-size: 1rem;
}
:global(.dark) .tab-button {
color: var(--fog-dark-text-light, #9ca3af);
}
.tab-button:hover {
color: var(--fog-text, #1f2937);
}
:global(.dark) .tab-button:hover {
color: var(--fog-dark-text, #f9fafb);
}
.tab-button.active {
color: var(--fog-text, #1f2937);
border-bottom-color: var(--fog-accent, #64748b);
}
:global(.dark) .tab-button.active {
color: var(--fog-dark-text, #f9fafb);
border-bottom-color: var(--fog-dark-accent, #94a3b8);
}
.tab-content {
margin-top: 2rem;

40
src/routes/rss/+page.svelte

@ -138,25 +138,6 @@ @@ -138,25 +138,6 @@
border-color: var(--fog-dark-border, #374151);
}
.create-rss-button {
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: opacity 0.2s;
}
:global(.dark) .create-rss-button {
background: var(--fog-dark-accent, #94a3b8);
}
.create-rss-button:hover {
opacity: 0.9;
}
.rss-info {
padding: 2rem;
@ -204,25 +185,4 @@ @@ -204,25 +185,4 @@
text-decoration: underline;
}
.edit-rss-button {
display: inline-block;
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: opacity 0.2s;
}
:global(.dark) .edit-rss-button {
background: var(--fog-dark-accent, #94a3b8);
}
.edit-rss-button:hover {
opacity: 0.9;
}
</style>

4
src/routes/topics/+page.svelte

@ -143,10 +143,6 @@ @@ -143,10 +143,6 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.topics-page {
max-width: 1000px;

4
src/routes/topics/[name]/+page.svelte

@ -107,10 +107,6 @@ @@ -107,10 +107,6 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.topic-header {
border-bottom: 1px solid var(--fog-border, #e5e7eb);

5
src/routes/write/+page.svelte

@ -86,11 +86,6 @@ @@ -86,11 +86,6 @@
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.write-page {
max-width: 800px;
margin: 0 auto;

Loading…
Cancel
Save