+
+
+
+ Explore
+
+
+ Discover blog posts from your friends on Nostr
+
+
+
+ {blogPosts.map((post) => (
+ t[0] === 'd')?.[1]}`}
+ post={post}
+ onClick={() => handlePostClick(post)}
+ />
+ ))}
+
+
+ )
+}
+
+export default Explore
+
diff --git a/src/index.css b/src/index.css
index 93ec06ca..656380e4 100644
--- a/src/index.css
+++ b/src/index.css
@@ -2555,3 +2555,163 @@ body {
.three-pane.sidebar-collapsed .relay-status-indicator {
left: calc(var(--sidebar-collapsed-width) + 1.5rem);
}
+
+/* Explore Page Styles */
+.explore-container {
+ padding: 2rem;
+ max-width: 1400px;
+ margin: 0 auto;
+ min-height: 100vh;
+}
+
+.explore-header {
+ text-align: center;
+ margin-bottom: 3rem;
+}
+
+.explore-header h1 {
+ font-size: 2.5rem;
+ margin: 0 0 1rem 0;
+ color: #646cff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+}
+
+.explore-subtitle {
+ font-size: 1.125rem;
+ color: rgba(255, 255, 255, 0.7);
+ margin: 0;
+}
+
+.explore-loading,
+.explore-error {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+ min-height: 50vh;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.explore-error {
+ color: #ff6b6b;
+}
+
+.explore-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 2rem;
+ margin-top: 2rem;
+}
+
+.blog-post-card {
+ background: #1a1a1a;
+ border: 1px solid #333;
+ border-radius: 12px;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.blog-post-card:hover {
+ border-color: #646cff;
+ transform: translateY(-4px);
+ box-shadow: 0 8px 24px rgba(100, 108, 255, 0.15);
+}
+
+.blog-post-card-image {
+ width: 100%;
+ height: 200px;
+ overflow: hidden;
+ background: #0f0f0f;
+}
+
+.blog-post-card-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.3s ease;
+}
+
+.blog-post-card:hover .blog-post-card-image img {
+ transform: scale(1.05);
+}
+
+.blog-post-card-content {
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ flex: 1;
+}
+
+.blog-post-card-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: 0;
+ color: rgba(255, 255, 255, 0.95);
+ line-height: 1.4;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.blog-post-card-summary {
+ font-size: 0.875rem;
+ color: rgba(255, 255, 255, 0.6);
+ margin: 0;
+ line-height: 1.6;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ flex: 1;
+}
+
+.blog-post-card-meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding-top: 0.75rem;
+ border-top: 1px solid #333;
+ font-size: 0.75rem;
+ color: rgba(255, 255, 255, 0.5);
+ flex-wrap: wrap;
+}
+
+.blog-post-card-author,
+.blog-post-card-date {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.blog-post-card-author svg,
+.blog-post-card-date svg {
+ opacity: 0.7;
+}
+
+@media (max-width: 768px) {
+ .explore-container {
+ padding: 1rem;
+ }
+
+ .explore-header h1 {
+ font-size: 2rem;
+ }
+
+ .explore-grid {
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+ }
+}
diff --git a/src/services/exploreService.ts b/src/services/exploreService.ts
new file mode 100644
index 00000000..38b7d0ce
--- /dev/null
+++ b/src/services/exploreService.ts
@@ -0,0 +1,87 @@
+import { RelayPool, completeOnEose } from 'applesauce-relay'
+import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs'
+import { NostrEvent } from 'nostr-tools'
+import { Helpers } from 'applesauce-core'
+
+const { getArticleTitle, getArticleImage, getArticlePublished, getArticleSummary } = Helpers
+
+export interface BlogPostPreview {
+ event: NostrEvent
+ title: string
+ summary?: string
+ image?: string
+ published?: number
+ author: string
+}
+
+/**
+ * Fetches blog posts (kind:30023) from a list of pubkeys (friends)
+ * @param relayPool - The relay pool to query
+ * @param pubkeys - Array of pubkeys to fetch posts from
+ * @param relayUrls - Array of relay URLs to query
+ * @returns Array of blog post previews
+ */
+export const fetchBlogPostsFromAuthors = async (
+ relayPool: RelayPool,
+ pubkeys: string[],
+ relayUrls: string[]
+): Promise