Browse Source

add favorite relays to the relay page and a field for entering a relay manually

master
Silberengel 1 month ago
parent
commit
77c45b3a6c
  1. 12
      src/app.css
  2. 3
      src/lib/components/content/MediaAttachments.svelte
  3. 76
      src/lib/components/preferences/UserPreferences.svelte
  4. 70
      src/lib/components/write/CreateEventForm.svelte
  5. 32
      src/lib/components/write/FindEventForm.svelte
  6. 4
      src/lib/modules/discussions/DiscussionCard.svelte
  7. 831
      src/lib/modules/feed/FeedPage.svelte
  8. 257
      src/lib/modules/feed/FeedPost.svelte
  9. 7
      src/lib/services/nostr/nostr-client.ts
  10. 6
      src/routes/cache/+page.svelte
  11. 14
      src/routes/discussions/+page.svelte
  12. 8
      src/routes/feed/+page.svelte
  13. 10
      src/routes/feed/relay/[relay]/+page.svelte
  14. 18
      src/routes/find/+page.svelte
  15. 300
      src/routes/relay/+page.svelte
  16. 7
      src/routes/replaceable/[d_tag]/+page.svelte
  17. 7
      src/routes/repos/+page.svelte
  18. 6
      src/routes/rss/+page.svelte
  19. 3
      src/routes/topics/+page.svelte
  20. 7
      src/routes/topics/[name]/+page.svelte
  21. 3
      src/routes/write/+page.svelte

12
src/app.css

@ -108,11 +108,11 @@ h1 { @@ -108,11 +108,11 @@ h1 {
margin-bottom: 1.5rem;
margin-top: 0;
font-weight: 700;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
}
:global(.dark) h1 {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
h2 {
@ -121,11 +121,11 @@ h2 { @@ -121,11 +121,11 @@ h2 {
margin-bottom: 0.875rem;
margin-top: 0;
font-weight: 600;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
}
:global(.dark) h2 {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
h3 {
@ -134,11 +134,11 @@ h3 { @@ -134,11 +134,11 @@ h3 {
margin-bottom: 0.625rem;
margin-top: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
}
:global(.dark) h3 {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
h4, h5, h6 {

3
src/lib/components/content/MediaAttachments.svelte

@ -384,6 +384,7 @@ @@ -384,6 +384,7 @@
.cover-image img {
border: 1px solid var(--fog-border, #e5e7eb);
max-width: 600px;
}
:global(.dark) .cover-image img {
@ -412,7 +413,7 @@ @@ -412,7 +413,7 @@
.media-item img,
.media-item video {
max-width: 100%;
max-width: 600px;
height: auto;
}

76
src/lib/components/preferences/UserPreferences.svelte

@ -126,13 +126,21 @@ @@ -126,13 +126,21 @@
$effect(() => {
if (showPreferences) {
// Prevent body scroll when modal is open
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
showPreferences = false;
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
return () => {
document.body.style.overflow = originalOverflow;
document.removeEventListener('keydown', handleEscape);
};
}
});
</script>
@ -304,7 +312,7 @@ @@ -304,7 +312,7 @@
border-radius: 0.25rem;
border: 1px solid var(--fog-border, #e5e7eb);
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
cursor: pointer;
transition: all 0.2s;
}
@ -312,7 +320,7 @@ @@ -312,7 +320,7 @@
:global(.dark) .preferences-button {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.preferences-button:hover {
@ -331,31 +339,37 @@ @@ -331,31 +339,37 @@
}
.preferences-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
background: rgba(0, 0, 0, 0.5) !important;
backdrop-filter: blur(4px);
z-index: 999;
-webkit-backdrop-filter: blur(4px);
z-index: 99999 !important;
animation: fadeIn 0.2s ease-out;
pointer-events: auto !important;
isolation: isolate;
}
.preferences-panel {
position: fixed;
top: 0;
left: 0;
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 320px;
height: 100vh;
background: var(--fog-post, #ffffff);
border-right: 1px solid var(--fog-border, #e5e7eb);
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
z-index: 100000 !important;
display: flex;
flex-direction: column;
animation: slideIn 0.3s ease-out;
overflow: hidden;
isolation: isolate;
}
:global(.dark) .preferences-panel {
@ -399,11 +413,11 @@ @@ -399,11 +413,11 @@
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
}
:global(.dark) .preferences-header h2 {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.close-button {
@ -417,13 +431,13 @@ @@ -417,13 +431,13 @@
display: flex;
align-items: center;
justify-content: center;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
border-radius: 0.25rem;
transition: background 0.2s;
}
:global(.dark) .close-button {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.close-button:hover {
@ -450,13 +464,13 @@ @@ -450,13 +464,13 @@
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
padding: 0;
font-size: 0.875rem;
}
:global(.dark) .preference-label {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.preference-options {
@ -470,7 +484,7 @@ @@ -470,7 +484,7 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
@ -485,7 +499,7 @@ @@ -485,7 +499,7 @@
:global(.dark) .preference-option {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.preference-option:hover {
@ -499,13 +513,13 @@ @@ -499,13 +513,13 @@
.preference-option.active {
background: var(--fog-accent, #64748b);
color: white;
color: var(--fog-text, #f1f5f9);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .preference-option.active {
background: var(--fog-dark-accent, #94a3b8);
color: var(--fog-dark-text, #1f2937);
color: var(--fog-dark-text, #f1f5f9);
border-color: var(--fog-dark-accent, #94a3b8);
}
@ -514,12 +528,12 @@ @@ -514,12 +528,12 @@
align-items: center;
gap: 0.5rem;
cursor: pointer;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
}
:global(.dark) .checkbox-label {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.checkbox-input {
@ -545,14 +559,14 @@ @@ -545,14 +559,14 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
}
:global(.dark) .text-input {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.manage-cache-button {
@ -561,7 +575,7 @@ @@ -561,7 +575,7 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-accent, #64748b);
color: white;
color: var(--fog-text, #f1f5f9);
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
@ -595,11 +609,11 @@ @@ -595,11 +609,11 @@
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
}
:global(.dark) .about-title {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.about-text {

70
src/lib/components/write/CreateEventForm.svelte

@ -1135,7 +1135,7 @@ @@ -1135,7 +1135,7 @@
.create-form-container {
display: flex;
gap: 2rem;
max-width: 1200px;
max-width: var(--content-width);
width: 100%;
}
@ -1175,14 +1175,14 @@ @@ -1175,14 +1175,14 @@
.form-title {
margin: 0;
font-size: 1.5rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
flex-shrink: 0;
}
:global(.dark) .form-title {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.help-text-panel {
@ -1215,13 +1215,13 @@ @@ -1215,13 +1215,13 @@
.help-description {
margin: 0 0 0.75rem 0;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
line-height: 1.5;
flex: 1;
}
:global(.dark) .help-description {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.example-button-wrapper {
@ -1234,7 +1234,7 @@ @@ -1234,7 +1234,7 @@
border-radius: 50%;
border: 1px solid var(--fog-border, #e5e7eb);
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
@ -1248,12 +1248,12 @@ @@ -1248,12 +1248,12 @@
:global(.dark) .example-button {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.example-button:hover {
background: var(--fog-accent, #64748b);
color: white;
color: var(--fog-text, #f1f5f9);
border-color: var(--fog-accent, #64748b);
}
@ -1283,14 +1283,14 @@ @@ -1283,14 +1283,14 @@
margin: 0;
font-size: 0.75rem;
font-family: 'Courier New', Courier, monospace;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.4;
}
:global(.dark) .example-json {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.suggested-tags {
@ -1300,12 +1300,12 @@ @@ -1300,12 +1300,12 @@
.suggested-tags strong {
display: block;
margin-bottom: 0.5rem;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.8125rem;
}
:global(.dark) .suggested-tags strong {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.suggested-tags ul {
@ -1331,12 +1331,12 @@ @@ -1331,12 +1331,12 @@
.form-label {
font-weight: 500;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
}
:global(.dark) .form-label {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.kind-select {
@ -1344,14 +1344,14 @@ @@ -1344,14 +1344,14 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
}
:global(.dark) .kind-select {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.custom-kind-input {
@ -1366,14 +1366,14 @@ @@ -1366,14 +1366,14 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
}
:global(.dark) .kind-id-input {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.content-input {
@ -1384,7 +1384,7 @@ @@ -1384,7 +1384,7 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
font-family: monospace;
resize: vertical;
@ -1399,7 +1399,7 @@ @@ -1399,7 +1399,7 @@
:global(.dark) .content-input {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.tags-list {
@ -1433,14 +1433,14 @@ @@ -1433,14 +1433,14 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
}
:global(.dark) .tag-input {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.tag-add-value,
@ -1449,7 +1449,7 @@ @@ -1449,7 +1449,7 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-highlight, #f3f4f6);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
cursor: pointer;
font-size: 0.875rem;
min-width: 2rem;
@ -1459,7 +1459,7 @@ @@ -1459,7 +1459,7 @@
:global(.dark) .tag-remove {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-highlight, #374151);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.tag-add-value:hover,
@ -1481,7 +1481,7 @@ @@ -1481,7 +1481,7 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-highlight, #f3f4f6);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
cursor: pointer;
font-size: 0.875rem;
}
@ -1489,7 +1489,7 @@ @@ -1489,7 +1489,7 @@
:global(.dark) .add-tag-button {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-highlight, #374151);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.add-tag-button:hover {
@ -1520,7 +1520,7 @@ @@ -1520,7 +1520,7 @@
.publish-button {
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: white;
color: var(--fog-text, #f1f5f9);
border: none;
border-radius: 0.25rem;
cursor: pointer;
@ -1556,18 +1556,18 @@ @@ -1556,18 +1556,18 @@
.republish-text {
margin: 0 0 0.5rem 0;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
}
:global(.dark) .republish-text {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.republish-button {
padding: 0.5rem 1rem;
background: var(--fog-accent, #64748b);
color: white;
color: var(--fog-text, #f1f5f9);
border: none;
border-radius: 0.25rem;
cursor: pointer;
@ -1608,7 +1608,7 @@ @@ -1608,7 +1608,7 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
@ -1627,7 +1627,7 @@ @@ -1627,7 +1627,7 @@
:global(.dark) .toolbar-button {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
@ -1660,7 +1660,7 @@ @@ -1660,7 +1660,7 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-highlight, #f3f4f6);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
@ -1669,7 +1669,7 @@ @@ -1669,7 +1669,7 @@
:global(.dark) .content-button {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-highlight, #374151);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.content-button:hover:not(:disabled) {

32
src/lib/components/write/FindEventForm.svelte

@ -146,23 +146,23 @@ @@ -146,23 +146,23 @@
.find-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-title {
margin: 0;
font-size: 1.5rem;
margin: 0 0 1.5rem 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
}
:global(.dark) .form-title {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.form-description {
margin: 0;
margin: 0 0 1.5rem 0;
color: var(--fog-text-light, #6b7280);
font-size: 0.875rem;
}
:global(.dark) .form-description {
@ -180,14 +180,15 @@ @@ -180,14 +180,15 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
font-family: monospace;
}
:global(.dark) .event-input {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.event-input:disabled {
@ -198,12 +199,13 @@ @@ -198,12 +199,13 @@
.find-button {
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: white;
color: var(--fog-text, #f1f5f9);
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
font-family: monospace;
}
:global(.dark) .find-button {
@ -220,6 +222,7 @@ @@ -220,6 +222,7 @@
}
.error-message {
margin-top: 1rem;
padding: 0.75rem;
background: var(--fog-danger-light, #fee2e2);
color: var(--fog-danger, #dc2626);
@ -255,11 +258,11 @@ @@ -255,11 +258,11 @@
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
}
:global(.dark) .event-title {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.view-link {
@ -291,24 +294,25 @@ @@ -291,24 +294,25 @@
.event-json pre {
margin: 0;
font-size: 0.75rem;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
white-space: pre-wrap;
word-wrap: break-word;
}
:global(.dark) .event-json pre {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.edit-button {
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: white;
color: var(--fog-text, #f1f5f9);
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
font-family: monospace;
}
:global(.dark) .edit-button {

4
src/lib/modules/discussions/DiscussionCard.svelte

@ -193,7 +193,7 @@ @@ -193,7 +193,7 @@
<a href="/event/{thread.id}" class="card-link">
<div class="card-content" class:expanded bind:this={contentElement}>
<div class="flex justify-between items-start mb-2">
<h3 class="text-lg font-semibold">
<h3 class="text-lg font-semibold text-fog-text dark:text-fog-dark-text">
{getTitle()}
</h3>
<div class="flex items-center gap-2">
@ -211,7 +211,7 @@ @@ -211,7 +211,7 @@
{/if}
</div>
<p class="text-sm mb-2">{getPreview()}</p>
<p class="text-sm mb-2 text-fog-text dark:text-fog-dark-text">{getPreview()}</p>
{#if getTopics().length > 0}
<div class="flex gap-2 topic-tags">

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

File diff suppressed because it is too large Load Diff

257
src/lib/modules/feed/FeedPost.svelte

@ -45,19 +45,12 @@ @@ -45,19 +45,12 @@
onParentLoaded?: (event: NostrEvent) => void; // Callback when parent is loaded
onQuotedLoaded?: (event: NostrEvent) => void; // Callback when quoted event is loaded
onOpenEvent?: (event: NostrEvent) => void; // Callback to open event in drawer
previewMode?: boolean; // If true, show only title and first 150 chars of content
reactions?: NostrEvent[]; // Optional pre-loaded reactions (for performance)
zapCount?: number; // Optional pre-loaded zap count (for performance)
}
let { post, parentEvent: providedParentEvent, quotedEvent: providedQuotedEvent, onReply, onParentLoaded, onQuotedLoaded, onOpenEvent, previewMode = false, reactions, zapCount: providedZapCount }: Props = $props();
let { post, parentEvent: providedParentEvent, quotedEvent: providedQuotedEvent, onReply, onParentLoaded, onQuotedLoaded, onOpenEvent }: Props = $props();
let loadedParentEvent = $state<NostrEvent | null>(null);
let loadingParent = $state(false);
let expanded = $state(false);
let contentElement: HTMLElement | null = $state(null);
let needsExpansion = $state(false);
let zapCount = $state(0);
// Check if this event is bookmarked (async, so we use state)
// Only check if user is logged in
@ -74,22 +67,16 @@ @@ -74,22 +67,16 @@
}
});
// Vote counting is handled by DiscussionVoteButtons component - no calculation here
// Derive the effective parent event: prefer provided, fall back to loaded
let parentEvent = $derived(providedParentEvent || loadedParentEvent);
// Sync provided parent event changes and load if needed
$effect(() => {
if (providedParentEvent) {
// If provided parent event is available, use it
return;
}
// Only load if not provided and this is a reply (fallback for edge cases)
// In most cases, FeedPage will have pre-loaded the parent
if (!loadedParentEvent && isReply()) {
// Delay loading to give FeedPage time to batch load
setTimeout(() => {
if (!providedParentEvent && !loadedParentEvent && isReply()) {
loadParentEvent();
@ -98,15 +85,6 @@ @@ -98,15 +85,6 @@
}
});
// Sync provided zap count - initialize and update when prop changes
$effect(() => {
if (providedZapCount !== undefined) {
zapCount = providedZapCount;
} else {
zapCount = 0;
}
});
// Lazy load PollCard when post is a poll and component is visible
$effect(() => {
if (post.kind === KIND.POLL && !PollCardComponent && !pollCardLoading) {
@ -120,100 +98,9 @@ @@ -120,100 +98,9 @@
}
});
let isMounted = $state(true);
let zapCountTimeout: ReturnType<typeof setTimeout> | null = null;
onMount(() => {
isMounted = true;
// Only load zap count if not provided (fallback for edge cases)
// In most cases, FeedPage will have pre-loaded the zap count
// Wait longer to give FeedPage time to batch load all zaps at once
if (providedZapCount === undefined) {
// Delay loading significantly to give FeedPage time to batch load
// FeedPage loads secondary data after posts are displayed
zapCountTimeout = setTimeout(() => {
if (providedZapCount === undefined && isMounted) {
loadZapCount();
}
}, 3000); // Wait 3 seconds for FeedPage to batch load
}
// Votes are now calculated as derived values, no need to load separately
return () => {
isMounted = false;
if (zapCountTimeout) {
clearTimeout(zapCountTimeout);
zapCountTimeout = null;
}
};
});
async function loadZapCount() {
try {
const config = nostrClient.getConfig();
const threshold = config.zapThreshold;
const zapRelays = relayManager.getZapReceiptReadRelays();
const filters = [{
kinds: [KIND.ZAP_RECEIPT],
'#e': [post.id]
}];
const receipts = await nostrClient.fetchEvents(
filters,
zapRelays,
{
useCache: true,
cacheResults: true,
onUpdate: (updated: NostrEvent[]) => {
// Recalculate zap count when new receipts arrive
const validReceipts = updated.filter((receipt) => {
const amountTag = receipt.tags.find((t) => t[0] === 'amount');
if (amountTag && amountTag[1]) {
const amount = parseInt(amountTag[1], 10);
return !isNaN(amount) && amount >= threshold;
}
return false;
});
// Get all receipts for this post (including cached ones)
nostrClient.fetchEvents(
filters,
zapRelays,
{ useCache: true, cacheResults: false }
).then(allReceipts => {
const allValidReceipts = allReceipts.filter((receipt) => {
const amountTag = receipt.tags.find((t) => t[0] === 'amount');
if (amountTag && amountTag[1]) {
const amount = parseInt(amountTag[1], 10);
return !isNaN(amount) && amount >= threshold;
}
return false;
});
zapCount = allValidReceipts.length;
}).catch(error => {
console.error('Error recalculating zap count:', error);
// No zap count loading needed
});
}
}
);
// Filter by threshold and count
const validReceipts = receipts.filter((receipt) => {
const amountTag = receipt.tags.find((t) => t[0] === 'amount');
if (amountTag && amountTag[1]) {
const amount = parseInt(amountTag[1], 10);
return !isNaN(amount) && amount >= threshold;
}
return false;
});
zapCount = validReceipts.length;
} catch (error) {
console.error('Error loading zap count:', error);
zapCount = 0;
}
}
function getRelativeTime(): string {
const now = Math.floor(Date.now() / 1000);
@ -288,32 +175,6 @@ @@ -288,32 +175,6 @@
}
}
$effect(() => {
if (contentElement) {
checkContentHeight();
// Use ResizeObserver to detect when content changes (e.g., images loading)
const observer = new ResizeObserver(() => {
checkContentHeight();
});
observer.observe(contentElement);
return () => observer.disconnect();
}
});
function checkContentHeight() {
if (contentElement) {
// Use requestAnimationFrame to ensure DOM is fully updated
requestAnimationFrame(() => {
if (contentElement) {
needsExpansion = contentElement.scrollHeight > 500;
}
});
}
}
function toggleExpanded() {
expanded = !expanded;
}
function getTitle(): string {
const titleTag = post.tags.find((t) => t[0] === 'title');
@ -389,56 +250,7 @@ @@ -389,56 +250,7 @@
class:cursor-pointer={!!onOpenEvent}
{...(onOpenEvent ? { role: "button", tabindex: 0 } : {})}
>
{#if previewMode}
<!-- Preview mode: show only title and first 150 chars -->
<a href="/event/{post.id}" class="card-link">
<div class="card-content">
<div class="flex justify-between items-start mb-2">
<h3 class="text-lg font-semibold">
{getTitle()}
</h3>
<div class="flex items-center gap-2">
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
<div class="interactive-element">
<EventMenu event={post} showContentActions={true} />
</div>
</div>
</div>
<div class="mb-2 flex items-center gap-2 flex-wrap">
<div class="interactive-element">
<ProfileBadge pubkey={post.pubkey} />
</div>
{#if getClientName()}
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span>
{/if}
{#if post.kind === KIND.DISCUSSION_THREAD}
{@const topics = getTopics()}
{#if topics.length === 0}
<span class="topic-badge text-xs px-2 py-0.5 rounded bg-fog-border dark:bg-fog-dark-border text-fog-text-light dark:text-fog-dark-text-light">General</span>
{:else}
{#each topics.slice(0, 3) as topic}
<span class="topic-badge text-xs px-2 py-0.5 rounded bg-fog-border dark:bg-fog-dark-border text-fog-text-light dark:text-fog-dark-text-light">{topic}</span>
{/each}
{/if}
{/if}
</div>
<p class="text-sm mb-2">{getPreviewContent()}</p>
{#if post.kind === KIND.POLL}
{@const optionTags = post.tags.filter(t => t[0] === 'option')}
<div class="text-xs text-fog-text-light dark:text-fog-dark-text-light mb-2">
Poll: {optionTags.length} {optionTags.length === 1 ? 'option' : 'options'}
</div>
{/if}
<!-- Vote counts are shown in full mode, not in preview mode -->
</div>
</a>
{:else}
<!-- Full mode: show complete content -->
<div class="card-content" class:expanded bind:this={contentElement}>
{#if isReply()}
<ReplyContext
parentEvent={parentEvent || undefined}
@ -459,13 +271,13 @@ @@ -459,13 +271,13 @@
/>
{/if}
<!-- Display title prominently for kind 30040 (book index) and 30041 (chapter sections) -->
{#if post.kind === 30040 || post.kind === 30041 || post.kind === 1 || post.kind === 30817}
<!-- Display title prominently for kind 30040 (book index), 30041 (chapter sections), and kind 11 (discussion threads) -->
{#if post.kind === 30040 || post.kind === 30041 || post.kind === 1 || post.kind === 30817 || post.kind === KIND.DISCUSSION_THREAD}
{@const title = getTitle()}
{#if title && title !== 'Untitled'}
<h1 class="post-title text-2xl font-bold mb-4 text-fog-text dark:text-fog-dark-text">
<h2 class="post-title text-2xl font-bold mb-4 text-fog-text dark:text-fog-dark-text">
{title}
</h1>
</h2>
{/if}
{/if}
@ -516,29 +328,9 @@ @@ -516,29 +328,9 @@
</div>
</div>
{#if needsExpansion}
<button
onclick={toggleExpanded}
class="show-more-button text-sm text-fog-accent dark:text-fog-dark-accent hover:underline mt-2"
>
{expanded ? 'Show less' : 'Show more'}
</button>
{/if}
<!-- Post actions (reactions, etc.) - always visible, outside collapsible content -->
<!-- Post actions: emoji button and menu -->
<div class="post-actions flex flex-wrap items-center gap-2 sm:gap-4">
{#if zapCount > 0}
<span class="zap-count-display">
<span class="zap-emoji"></span>
<span class="zap-count-number">{zapCount}</span>
</span>
{/if}
{#if post.kind === KIND.DISCUSSION_THREAD}
<!-- DiscussionVoteButtons includes both vote counts and buttons -->
<DiscussionVoteButtons event={post} preloadedReactions={reactions} />
{:else}
<FeedReactionButtons event={post} preloadedReactions={reactions} />
{/if}
<FeedReactionButtons event={post} />
{#if onReply}
<button
onclick={() => onReply(post)}
@ -553,7 +345,6 @@ @@ -553,7 +345,6 @@
<span class="kind-number">{getKindInfo(post.kind).number}</span>
<span class="kind-description">{getKindInfo(post.kind).description}</span>
</div>
{/if}
</article>
<style>
@ -574,6 +365,12 @@ @@ -574,6 +365,12 @@
line-height: 1.6;
}
/* Limit all images to 600px wide */
.post-content :global(img) {
max-width: 600px;
height: auto;
}
.post-actions {
padding-top: 0.5rem;
padding-right: 6rem; /* Reserve space for kind badge */
@ -673,32 +470,6 @@ @@ -673,32 +470,6 @@
background: var(--fog-dark-highlight, #374151);
}
.card-link {
display: block;
color: inherit;
text-decoration: none;
cursor: pointer;
}
.card-link:hover {
background: var(--fog-highlight, #f3f4f6);
}
:global(.dark) .card-link:hover {
background: var(--fog-dark-highlight, #475569);
}
.card-link {
pointer-events: auto;
}
.card-link > * {
pointer-events: none;
}
.card-link .interactive-element {
pointer-events: auto;
}
.post-header {
display: flex;

7
src/lib/services/nostr/nostr-client.ts

@ -1339,7 +1339,7 @@ class NostrClient { @@ -1339,7 +1339,7 @@ class NostrClient {
if (updatedEntry && !updatedEntry.pending) {
if ((Date.now() - updatedEntry.cachedAt) < this.EMPTY_RESULT_CACHE_TTL) {
const finalAge = Math.round((Date.now() - updatedEntry.cachedAt) / 1000);
console.log(`[nostr-client] Skipping fetch [${filterDesc}] from [${relayDesc}] - empty result cached ${finalAge}s ago (waited for pending)`);
console.debug(`[nostr-client] Skipping fetch [${filterDesc}] from [${relayDesc}] - empty result cached ${finalAge}s ago (waited for pending)`);
return [];
}
break; // No longer pending, but result expired or had data
@ -1351,7 +1351,8 @@ class NostrClient { @@ -1351,7 +1351,8 @@ class NostrClient {
// If still pending after waiting, proceed (might be a slow fetch)
} else if (!emptyCacheEntry.pending && age < this.EMPTY_RESULT_CACHE_TTL) {
const ageSeconds = Math.round(age / 1000);
console.log(`[nostr-client] Skipping fetch [${filterDesc}] from [${relayDesc}] - empty result cached ${ageSeconds}s ago`);
// Use debug level to reduce console noise - this is expected behavior
console.debug(`[nostr-client] Skipping fetch [${filterDesc}] from [${relayDesc}] - empty result cached ${ageSeconds}s ago`);
return Promise.resolve([]);
}
}
@ -1364,7 +1365,7 @@ class NostrClient { @@ -1364,7 +1365,7 @@ class NostrClient {
const recheck = this.emptyResultCache.get(emptyCacheKey);
if (recheck && !recheck.pending && (Date.now() - recheck.cachedAt) < this.EMPTY_RESULT_CACHE_TTL) {
const finalAge = Math.round((Date.now() - recheck.cachedAt) / 1000);
console.log(`[nostr-client] Skipping fetch [${filterDesc}] from [${relayDesc}] - empty result cached ${finalAge}s ago (waited for concurrent)`);
console.debug(`[nostr-client] Skipping fetch [${filterDesc}] from [${relayDesc}] - empty result cached ${finalAge}s ago (waited for concurrent)`);
return [];
}
}

6
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="font-mono">/Cache</h1>
<h1 class="text-2xl font-bold mb-6 text-fog-text dark:text-fog-dark-text font-mono">/Cache</h1>
{#if loading && !stats}
<div class="loading-state">
@ -661,7 +661,9 @@ @@ -661,7 +661,9 @@
<style>
.cache-page {
max-width: 100%;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 1rem;
}
.stats-section,

14
src/routes/discussions/+page.svelte

@ -13,9 +13,10 @@ @@ -13,9 +13,10 @@
<Header />
<main class="container mx-auto px-4 py-8">
<div class="discussions-content">
<div class="discussions-header mb-4">
<div>
<h1 class="text-2xl font-bold mb-4 text-fog-text dark:text-fog-dark-text font-mono">/Discussions</h1>
<h1 class="text-2xl font-bold mb-6 text-fog-text dark:text-fog-dark-text font-mono">/Discussions</h1>
<p class="mb-4 text-fog-text dark:text-fog-dark-text">Decentralized discussion board on Nostr.</p>
</div>
<a href="/write?kind=11" class="write-button" title="Write a new thread">
@ -28,12 +29,23 @@ @@ -28,12 +29,23 @@
</div>
<DiscussionList />
</div>
</main>
<style>
.discussions-content {
max-width: var(--content-width);
margin: 0 auto;
}
.discussions-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 0 1rem;
}
.search-section {
padding: 0 1rem;
}
</style>

8
src/routes/feed/+page.svelte

@ -13,6 +13,7 @@ @@ -13,6 +13,7 @@
<Header />
<main class="container mx-auto px-4 py-8">
<div class="feed-content">
<div class="feed-header mb-6">
<h1 class="text-2xl font-bold text-fog-text dark:text-fog-dark-text font-mono">/Feed</h1>
<div class="feed-controls">
@ -25,13 +26,20 @@ @@ -25,13 +26,20 @@
</div>
</div>
<FeedPage />
</div>
</main>
<style>
.feed-content {
max-width: var(--content-width);
margin: 0 auto;
}
.feed-header {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0 1rem;
}
.feed-controls {

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

@ -65,6 +65,7 @@ @@ -65,6 +65,7 @@
<Header />
<main class="container mx-auto px-4 py-8">
<div class="relay-feed-content">
<div class="search-section mb-6">
<SearchBox />
</div>
@ -81,9 +82,18 @@ @@ -81,9 +82,18 @@
<p class="text-fog-text dark:text-fog-dark-text">Loading relay feed...</p>
</div>
{/if}
</div>
</main>
<style>
.relay-feed-content {
max-width: var(--content-width);
margin: 0 auto;
}
.search-section {
padding: 0 1rem;
}
.error-state,
.loading-state {

18
src/routes/find/+page.svelte

@ -151,8 +151,9 @@ @@ -151,8 +151,9 @@
<style>
.find-page {
max-width: 800px;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 1rem;
}
.find-sections {
@ -173,6 +174,17 @@ @@ -173,6 +174,17 @@
background: var(--fog-dark-post, #1f2937);
}
.find-section h2 {
margin: 0 0 1.5rem 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--fog-text, #475569);
}
:global(.dark) .find-section h2 {
color: var(--fog-dark-text, #cbd5e1);
}
.section-description {
margin: 0 0 1.5rem 0;
color: var(--fog-text-light, #6b7280);
@ -194,7 +206,7 @@ @@ -194,7 +206,7 @@
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-size: 0.875rem;
font-family: monospace;
}
@ -202,7 +214,7 @@ @@ -202,7 +214,7 @@
:global(.dark) .user-input {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.user-input:disabled {

300
src/routes/relay/+page.svelte

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { sessionManager } from '../../lib/services/auth/session-manager.js';
import { KIND } from '../../lib/types/kind-lookup.js';
interface RelayInfo {
url: string;
@ -14,7 +15,9 @@ @@ -14,7 +15,9 @@
}
let relays = $state<RelayInfo[]>([]);
let favoriteRelays = $state<RelayInfo[]>([]);
let loading = $state(true);
let customRelayUrl = $state('');
function categorizeRelay(url: string): string[] {
const categories: string[] = [];
@ -78,9 +81,80 @@ @@ -78,9 +81,80 @@
});
relays = relayList;
// Load favorite relays if user is logged in
await loadFavoriteRelays();
loading = false;
}
async function loadFavoriteRelays() {
const currentPubkey = sessionManager.getCurrentPubkey();
if (!currentPubkey) {
favoriteRelays = [];
return;
}
try {
// Fetch kind 10012 (FAVORITE_RELAYS) event for current user
// Fetch multiple in case there are multiple replaceable events
const favoriteEvents = await nostrClient.fetchEvents(
[{ kinds: [KIND.FAVORITE_RELAYS], authors: [currentPubkey], limit: 10 }],
relayManager.getProfileReadRelays(),
{ useCache: true, cacheResults: true, timeout: 5000 }
);
if (favoriteEvents.length === 0) {
favoriteRelays = [];
return;
}
// Get the latest event (replaceable event, so should be only one, but take latest just in case)
const latestEvent = favoriteEvents.sort((a, b) => b.created_at - a.created_at)[0];
// Extract relay URLs from 'relay' tags (kind 10012 uses 'relay' tags)
const favoriteRelayUrls = new Set<string>();
for (const tag of latestEvent.tags) {
if (tag[0] === 'relay' && tag[1]) {
// Normalize URL: trim, remove trailing slash, ensure protocol
let url = tag[1].trim();
// Remove trailing slash
url = url.replace(/\/$/, '');
// If no protocol, assume wss://
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
url = `wss://${url}`;
}
favoriteRelayUrls.add(url);
}
}
// Get connection status - normalize URLs for comparison
const connectedRelays = nostrClient.getConnectedRelays();
const normalizeUrlForComparison = (url: string): string => {
return url.replace(/\/$/, '').toLowerCase();
};
const favoriteRelayList: RelayInfo[] = Array.from(favoriteRelayUrls).map(url => {
const normalizedUrl = normalizeUrlForComparison(url);
const isConnected = connectedRelays.some(connected =>
normalizeUrlForComparison(connected) === normalizedUrl
);
return {
url,
categories: ['Favorite'],
connected: isConnected
};
});
// Sort by URL
favoriteRelayList.sort((a, b) => a.url.localeCompare(b.url));
favoriteRelays = favoriteRelayList;
} catch (err) {
console.error('Error loading favorite relays:', err);
favoriteRelays = [];
}
}
function handleRelayClick(url: string) {
// Extract hostname from relay URL (e.g., wss://thecitadel.nostr1.com -> thecitadel.nostr1.com)
// The route expects just the domain without protocol or port
@ -99,8 +173,37 @@ @@ -99,8 +173,37 @@
goto(`/feed/relay/${relayPath}`);
}
function handleCustomRelaySubmit() {
const url = customRelayUrl.trim();
if (!url) return;
// Validate URL format
try {
// Try to parse as URL, add protocol if missing
let fullUrl = url;
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
fullUrl = `wss://${url}`;
}
new URL(fullUrl); // Validate URL format
// Navigate to the relay
handleRelayClick(fullUrl);
customRelayUrl = ''; // Clear input after navigation
} catch {
alert('Please enter a valid relay URL (e.g., relay.example.com or wss://relay.example.com)');
}
}
onMount(async () => {
await nostrClient.initialize();
// Ensure session is restored before loading favorite relays
if (!sessionManager.isLoggedIn()) {
try {
await sessionManager.restoreSession();
} catch (error) {
console.error('Failed to restore session in relay page:', error);
}
}
await loadRelays();
});
</script>
@ -111,10 +214,69 @@ @@ -111,10 +214,69 @@
<div class="relay-page">
<h1 class="text-2xl font-bold mb-6 text-fog-text dark:text-fog-dark-text font-mono">/Relay</h1>
<!-- Custom Relay Input -->
<section class="relay-category custom-relay-section">
<h2 class="category-title">Custom Relay</h2>
<div class="custom-relay-input">
<input
type="text"
placeholder="Enter relay URL (e.g., relay.example.com or wss://relay.example.com)"
bind:value={customRelayUrl}
onkeydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleCustomRelaySubmit();
}
}}
class="custom-relay-text-input"
/>
<button
onclick={handleCustomRelaySubmit}
class="custom-relay-button"
disabled={!customRelayUrl.trim()}
>
Go
</button>
</div>
</section>
{#if loading}
<p class="text-fog-text dark:text-fog-dark-text">Loading relays...</p>
{:else}
<div class="relay-categories">
<!-- Favorite Relays Section -->
{#if favoriteRelays.length > 0}
<section class="relay-category">
<h2 class="category-title">Favorite</h2>
<div class="relay-list">
{#each favoriteRelays as relay}
<div
class="relay-item"
onclick={() => handleRelayClick(relay.url)}
role="button"
tabindex="0"
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleRelayClick(relay.url);
}
}}
>
<div class="relay-info">
<span class="relay-url">{relay.url}</span>
<div class="relay-meta">
<span class="relay-status" class:connected={relay.connected} class:disconnected={!relay.connected}>
{relay.connected ? '● Connected' : '○ Disconnected'}
</span>
</div>
</div>
<span class="relay-arrow"></span>
</div>
{/each}
</div>
</section>
{/if}
{#each ['Default', 'Profile', 'Thread Publish', 'GIF', 'Other'] as category}
{@const categoryRelays = relays.filter(r => r.categories.includes(category))}
{#if categoryRelays.length > 0}
@ -137,14 +299,17 @@ @@ -137,14 +299,17 @@
<div class="relay-info">
<span class="relay-url">{relay.url}</span>
<div class="relay-meta">
{#if relay.categories.length > 1}
<span class="relay-categories-badge">
{relay.categories.length} categories
</span>
{/if}
<span class="relay-status" class:connected={relay.connected} class:disconnected={!relay.connected}>
{relay.connected ? '● Connected' : '○ Disconnected'}
</span>
{#if relay.categories.length > 1}
{@const otherCategories = relay.categories.filter(c => c !== category)}
{#if otherCategories.length > 0}
{#each otherCategories as cat}
<span class="relay-category-badge">{cat}</span>
{/each}
{/if}
{/if}
</div>
</div>
<span class="relay-arrow"></span>
@ -162,8 +327,9 @@ @@ -162,8 +327,9 @@
<style>
.relay-page {
max-width: 1000px;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 1rem;
}
.relay-categories {
@ -188,14 +354,14 @@ @@ -188,14 +354,14 @@
margin: 0 0 1rem 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
font-family: monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
}
:global(.dark) .category-title {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.relay-list {
@ -206,8 +372,8 @@ @@ -206,8 +372,8 @@
.relay-item {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
@ -217,6 +383,10 @@ @@ -217,6 +383,10 @@
font-family: monospace;
}
.relay-item > .relay-info {
flex: 1;
}
:global(.dark) .relay-item {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-highlight, #374151);
@ -243,6 +413,7 @@ @@ -243,6 +413,7 @@
flex-direction: column;
gap: 0.25rem;
flex: 1;
min-width: 0;
}
.relay-meta {
@ -252,61 +423,126 @@ @@ -252,61 +423,126 @@
flex-wrap: wrap;
}
.relay-categories-badge {
.relay-status {
font-size: 0.75rem;
opacity: 0.7;
flex-shrink: 0;
}
.relay-status.connected {
color: #10b981;
}
.relay-status.disconnected {
color: #ef4444;
}
.relay-item:hover .relay-status {
color: white;
opacity: 1;
}
.relay-category-badge {
font-size: 0.7rem;
opacity: 0.6;
padding: 0.125rem 0.375rem;
border-radius: 0.125rem;
background: var(--fog-border, #e5e7eb);
color: var(--fog-text-light, #6b7280);
font-family: monospace;
}
:global(.dark) .relay-categories-badge {
:global(.dark) .relay-category-badge {
background: var(--fog-dark-border, #475569);
color: var(--fog-dark-text-light, #9ca3af);
}
.relay-item:hover .relay-categories-badge {
.relay-item:hover .relay-category-badge {
background: rgba(255, 255, 255, 0.2);
color: white;
opacity: 0.8;
}
.relay-url {
font-size: 0.875rem;
color: var(--fog-text, #1f2937);
color: var(--fog-text, #475569);
word-break: break-all;
}
:global(.dark) .relay-url {
color: var(--fog-dark-text, #f9fafb);
color: var(--fog-dark-text, #cbd5e1);
}
.relay-item:hover .relay-url {
color: white;
}
.relay-status {
font-size: 0.75rem;
opacity: 0.7;
.relay-arrow {
font-size: 1.25rem;
opacity: 0.5;
flex-shrink: 0;
margin-top: 0.125rem;
}
.relay-status.connected {
color: #10b981;
.relay-item:hover .relay-arrow {
opacity: 1;
}
.relay-status.disconnected {
color: #ef4444;
.custom-relay-section {
margin-bottom: 2rem;
}
.relay-item:hover .relay-status {
color: white;
opacity: 1;
.custom-relay-input {
display: flex;
gap: 0.5rem;
align-items: center;
}
.relay-arrow {
font-size: 1.25rem;
.custom-relay-text-input {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #475569);
font-family: monospace;
font-size: 0.875rem;
}
:global(.dark) .custom-relay-text-input {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #cbd5e1);
}
.custom-relay-text-input:focus {
outline: 2px solid var(--fog-accent, #64748b);
outline-offset: 2px;
}
.custom-relay-button {
padding: 0.75rem 1.5rem;
border: 1px solid var(--fog-accent, #64748b);
border-radius: 0.25rem;
background: var(--fog-accent, #64748b);
color: var(--fog-text, #f1f5f9);
font-family: monospace;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.custom-relay-button:hover:not(:disabled) {
background: var(--fog-dark-accent, #94a3b8);
border-color: var(--fog-dark-accent, #94a3b8);
}
.custom-relay-button:disabled {
opacity: 0.5;
margin-left: 1rem;
cursor: not-allowed;
}
.relay-item:hover .relay-arrow {
opacity: 1;
.custom-relay-button:focus {
outline: 2px solid var(--fog-accent, #64748b);
outline-offset: 2px;
}
</style>

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

@ -94,6 +94,7 @@ @@ -94,6 +94,7 @@
<Header />
<main class="container mx-auto px-4 py-8">
<div class="replaceable-content">
<div class="replaceable-header mb-6">
<h1 class="text-2xl font-bold text-fog-text dark:text-fog-dark-text">
Replaceable Events: {dTag}
@ -131,6 +132,7 @@ @@ -131,6 +132,7 @@
{/each}
</div>
{/if}
</div>
</main>
{#if drawerOpen && drawerEvent}
@ -138,8 +140,13 @@ @@ -138,8 +140,13 @@
{/if}
<style>
.replaceable-content {
max-width: var(--content-width);
margin: 0 auto;
}
.replaceable-header {
padding: 0 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
padding-bottom: 1rem;
}

7
src/routes/repos/+page.svelte

@ -142,6 +142,7 @@ @@ -142,6 +142,7 @@
<Header />
<main class="container mx-auto px-4 py-8">
<div class="repos-content">
<div class="repos-header mb-6">
<h1 class="text-2xl font-bold text-fog-text dark:text-fog-dark-text font-mono">/Repos</h1>
<p class="text-fog-text-light dark:text-fog-dark-text-light mt-2 mb-4">
@ -199,11 +200,17 @@ @@ -199,11 +200,17 @@
{/each}
</div>
{/if}
</div>
</main>
<style>
.repos-content {
max-width: var(--content-width);
margin: 0 auto;
}
.repos-header {
padding: 0 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
padding-bottom: 1rem;
}

6
src/routes/rss/+page.svelte

@ -63,7 +63,7 @@ @@ -63,7 +63,7 @@
<main class="container mx-auto px-4 py-8">
<div class="rss-page">
<h1 class="text-2xl font-bold mb-4 text-fog-text dark:text-fog-dark-text">RSS Feed</h1>
<h1 class="text-2xl font-bold mb-6 text-fog-text dark:text-fog-dark-text font-mono">/RSS</h1>
{#if loading}
<p class="text-fog-text dark:text-fog-dark-text">Loading...</p>
@ -123,7 +123,9 @@ @@ -123,7 +123,9 @@
<style>
.rss-page {
max-width: 600px;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 1rem;
}
.rss-setup {

3
src/routes/topics/+page.svelte

@ -145,8 +145,9 @@ @@ -145,8 +145,9 @@
<style>
.topics-page {
max-width: 1000px;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 1rem;
}
.topics-list {

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

@ -78,6 +78,7 @@ @@ -78,6 +78,7 @@
<Header />
<main class="container mx-auto px-4 py-8">
<div class="topic-content">
<div class="topic-header mb-6">
<h1 class="text-2xl font-bold text-fog-text dark:text-fog-dark-text">
Topic: #{topicName}
@ -104,11 +105,17 @@ @@ -104,11 +105,17 @@
{/each}
</div>
{/if}
</div>
</main>
<style>
.topic-content {
max-width: var(--content-width);
margin: 0 auto;
}
.topic-header {
padding: 0 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
padding-bottom: 1rem;
}

3
src/routes/write/+page.svelte

@ -87,8 +87,9 @@ @@ -87,8 +87,9 @@
<style>
.write-page {
max-width: 800px;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 1rem;
}
.form-container {

Loading…
Cancel
Save