diff --git a/src/components/ArticlePreview.tsx b/src/components/ArticlePreview.tsx
new file mode 100644
index 0000000..6e5cd30
--- /dev/null
+++ b/src/components/ArticlePreview.tsx
@@ -0,0 +1,104 @@
+import { Link } from 'react-router-dom';
+import { nip19 } from 'nostr-tools';
+import type { NostrEvent } from '@nostrify/nostrify';
+import { Card, CardContent, CardHeader } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Calendar } from 'lucide-react';
+import { useAuthor } from '@/hooks/useAuthor';
+import { genUserName } from '@/lib/genUserName';
+
+interface ArticlePreviewProps {
+ post: NostrEvent;
+ variant?: 'default' | 'compact';
+ showAuthor?: boolean;
+}
+
+export function ArticlePreview({ post, variant = 'default', showAuthor = true }: ArticlePreviewProps) {
+ const { data: author } = useAuthor(post.pubkey);
+ const metadata = author?.metadata;
+
+ const title = post.tags.find(([name]: [string]) => name === 'title')?.[1] || 'Untitled';
+ const summary = post.tags.find(([name]: [string]) => name === 'summary')?.[1];
+ const image = post.tags.find(([name]: [string]) => name === 'image')?.[1];
+ const publishedAt = post.tags.find(([name]: [string]) => name === 'published_at')?.[1];
+ const identifier = post.tags.find(([name]: [string]) => name === 'd')?.[1] || '';
+ const hashtags = post.tags
+ .filter(([name]: [string]) => name === 't')
+ .map(([, value]: [string, string]) => value)
+ .slice(0, 3);
+
+ const date = publishedAt
+ ? new Date(parseInt(publishedAt) * 1000)
+ : new Date(post.created_at * 1000);
+
+ const naddr = nip19.naddrEncode({
+ kind: post.kind,
+ pubkey: post.pubkey,
+ identifier,
+ });
+
+ const displayName = metadata?.name || metadata?.display_name || genUserName(post.pubkey);
+ const avatarUrl = metadata?.picture;
+
+ const isCompact = variant === 'compact';
+ const titleSize = isCompact ? 'text-lg' : 'text-xl sm:text-2xl';
+ const summaryLines = isCompact ? 'line-clamp-2' : 'line-clamp-3';
+ const dateFormat = isCompact
+ ? { month: 'short', day: 'numeric', year: 'numeric' }
+ : { year: 'numeric', month: 'long', day: 'numeric' };
+
+ return (
+
+
+ {image && (
+
+

+
+ )}
+
+
+ {title}
+
+ {summary && (
+
+ {summary}
+
+ )}
+
+
+ 0 ? 'mb-3' : ''}`}>
+
+
+
+ {showAuthor && (
+ 0 ? 'mb-3' : ''}`}>
+
+
+
+ {displayName.slice(0, 2).toUpperCase()}
+
+
+
{displayName}
+
+ )}
+ {hashtags.length > 0 && (
+
+ {hashtags.map((tag: string) => (
+
+ #{tag}
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/LatestArticles.tsx b/src/components/LatestArticles.tsx
index 82cce6b..a5fc451 100644
--- a/src/components/LatestArticles.tsx
+++ b/src/components/LatestArticles.tsx
@@ -1,104 +1,14 @@
import { useState } from 'react';
-import { Link } from 'react-router-dom';
-import { nip19 } from 'nostr-tools';
-import type { NostrEvent } from '@nostrify/nostrify';
-import { Card, CardContent, CardHeader } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
+import { Card, CardHeader } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
-import { Calendar, Newspaper, ChevronDown } from 'lucide-react';
+import { Newspaper, ChevronDown } from 'lucide-react';
import { useBlogPosts } from '@/hooks/useBlogPosts';
-import { useAuthor } from '@/hooks/useAuthor';
-import { genUserName } from '@/lib/genUserName';
+import { ArticlePreview } from '@/components/ArticlePreview';
const INITIAL_POSTS_COUNT = 3;
const LOAD_MORE_COUNT = 6;
-function ArticleCard({ post }: { post: NostrEvent }) {
- const { data: author } = useAuthor(post.pubkey);
- const metadata = author?.metadata;
-
- const title = post.tags.find(([name]: [string]) => name === 'title')?.[1] || 'Untitled';
- const summary = post.tags.find(([name]: [string]) => name === 'summary')?.[1];
- const image = post.tags.find(([name]: [string]) => name === 'image')?.[1];
- const publishedAt = post.tags.find(([name]: [string]) => name === 'published_at')?.[1];
- const identifier = post.tags.find(([name]: [string]) => name === 'd')?.[1] || '';
- const hashtags = post.tags
- .filter(([name]: [string]) => name === 't')
- .map(([, value]: [string, string]) => value)
- .slice(0, 3);
-
- const date = publishedAt
- ? new Date(parseInt(publishedAt) * 1000)
- : new Date(post.created_at * 1000);
-
- const naddr = nip19.naddrEncode({
- kind: 30023,
- pubkey: post.pubkey,
- identifier,
- });
-
- const displayName = metadata?.name || metadata?.display_name || genUserName(post.pubkey);
- const avatarUrl = metadata?.picture;
-
- return (
-
-
- {image && (
-
-

-
- )}
-
-
- {title}
-
- {summary && (
-
- {summary}
-
- )}
-
-
-
-
-
-
-
-
-
-
- {displayName.slice(0, 2).toUpperCase()}
-
-
-
{displayName}
-
- {hashtags.length > 0 && (
-
- {hashtags.map((tag: string) => (
-
- #{tag}
-
- ))}
-
- )}
-
-
-
- );
-}
-
export function LatestArticles() {
const [visibleCount, setVisibleCount] = useState(INITIAL_POSTS_COUNT);
const { data: posts, isLoading } = useBlogPosts();
@@ -157,7 +67,7 @@ export function LatestArticles() {
{/* Posts Grid */}
{visiblePosts.map((post) => (
-
+
))}
diff --git a/src/components/LatestInHashtag.tsx b/src/components/LatestInHashtag.tsx
index 223ecff..26dc180 100644
--- a/src/components/LatestInHashtag.tsx
+++ b/src/components/LatestInHashtag.tsx
@@ -1,15 +1,10 @@
-import { Link, useNavigate } from 'react-router-dom';
-import { nip19 } from 'nostr-tools';
-import type { NostrEvent } from '@nostrify/nostrify';
-import { Card, CardContent, CardHeader } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
+import { useNavigate } from 'react-router-dom';
+import { Card, CardHeader } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
-import { Calendar, Hash, ChevronRight } from 'lucide-react';
+import { Hash, ChevronRight } from 'lucide-react';
import { useBlogPostsByHashtag } from '@/hooks/useBlogPostsByHashtag';
-import { useAuthor } from '@/hooks/useAuthor';
-import { genUserName } from '@/lib/genUserName';
+import { ArticlePreview } from '@/components/ArticlePreview';
interface LatestInHashtagProps {
hashtag: string;
@@ -18,90 +13,6 @@ interface LatestInHashtagProps {
const INITIAL_POSTS_COUNT = 3;
-function HashtagArticleCard({ post }: { post: NostrEvent }) {
- const { data: author } = useAuthor(post.pubkey);
- const metadata = author?.metadata;
-
- const title = post.tags.find(([name]: [string]) => name === 'title')?.[1] || 'Untitled';
- const summary = post.tags.find(([name]: [string]) => name === 'summary')?.[1];
- const image = post.tags.find(([name]: [string]) => name === 'image')?.[1];
- const publishedAt = post.tags.find(([name]: [string]) => name === 'published_at')?.[1];
- const identifier = post.tags.find(([name]: [string]) => name === 'd')?.[1] || '';
- const hashtags = post.tags
- .filter(([name]: [string]) => name === 't')
- .map(([, value]: [string, string]) => value)
- .slice(0, 3);
-
- const date = publishedAt
- ? new Date(parseInt(publishedAt) * 1000)
- : new Date(post.created_at * 1000);
-
- const naddr = nip19.naddrEncode({
- kind: 30023,
- pubkey: post.pubkey,
- identifier,
- });
-
- const displayName = metadata?.name || metadata?.display_name || genUserName(post.pubkey);
- const avatarUrl = metadata?.picture;
-
- return (
-
-
- {image && (
-
-

-
- )}
-
-
- {title}
-
- {summary && (
-
- {summary}
-
- )}
-
-
-
-
-
-
-
-
-
-
- {displayName.slice(0, 2).toUpperCase()}
-
-
-
{displayName}
-
- {hashtags.length > 0 && (
-
- {hashtags.map((tag: string) => (
-
- #{tag}
-
- ))}
-
- )}
-
-
-
- );
-}
-
export function LatestInHashtag({ hashtag, icon }: LatestInHashtagProps) {
const navigate = useNavigate();
const { data: posts, isLoading } = useBlogPostsByHashtag(hashtag);
@@ -171,7 +82,7 @@ export function LatestInHashtag({ hashtag, icon }: LatestInHashtagProps) {
{/* Posts Grid */}
{visiblePosts.map((post) => (
-
+
))}
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index 808f33a..370b3e9 100644
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -1,4 +1,4 @@
-import { useParams, Link } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { useAuthor } from '@/hooks/useAuthor';
import { useAuthorBlogPosts } from '@/hooks/useAuthorBlogPosts';
@@ -7,9 +7,10 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
-import { Calendar, Link2, Mail, Copy, Check } from 'lucide-react';
+import { Link2, Mail, Copy, Check } from 'lucide-react';
import { genUserName } from '@/lib/genUserName';
import { RelaySelector } from '@/components/RelaySelector';
+import { ArticlePreview } from '@/components/ArticlePreview';
import { useToast } from '@/hooks/useToast';
import NotFound from '@/pages/NotFound';
import { useState } from 'react';
@@ -234,74 +235,9 @@ export default function ProfilePage() {
) : posts && posts.length > 0 ? (
- {posts.map((post) => {
- const title = post.tags.find(([name]) => name === 'title')?.[1] || 'Untitled';
- const summary = post.tags.find(([name]) => name === 'summary')?.[1];
- const image = post.tags.find(([name]) => name === 'image')?.[1];
- const publishedAt = post.tags.find(([name]) => name === 'published_at')?.[1];
- const tags = post.tags.filter(([name]) => name === 't').map(([, value]) => value);
- const identifier = post.tags.find(([name]) => name === 'd')?.[1];
-
- const naddr = nip19.naddrEncode({
- kind: post.kind,
- pubkey: post.pubkey,
- identifier: identifier || '',
- });
-
- const date = publishedAt
- ? new Date(parseInt(publishedAt) * 1000)
- : new Date(post.created_at * 1000);
-
- return (
-
-
- {image && (
-
-

-
- )}
-
-
- {title}
-
- {summary && (
-
- {summary}
-
- )}
-
-
-
-
- {tags.length > 0 && (
-
- {tags.slice(0, 3).map((tag) => (
-
- {tag}
-
- ))}
- {tags.length > 3 && (
-
- +{tags.length - 3}
-
- )}
-
- )}
-
-
-
- );
- })}
+ {posts.map((post) => (
+
+ ))}
) : (
diff --git a/src/pages/SearchResultsPage.tsx b/src/pages/SearchResultsPage.tsx
index 7b14cbb..ea3acfb 100644
--- a/src/pages/SearchResultsPage.tsx
+++ b/src/pages/SearchResultsPage.tsx
@@ -5,9 +5,10 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
-import { User, FileText, ArrowLeft, Calendar } from 'lucide-react';
+import { User, FileText, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { SearchBar } from '@/components/SearchBar';
+import { ArticlePreview } from '@/components/ArticlePreview';
import type { NostrMetadata } from '@nostrify/nostrify';
export default function SearchResultsPage() {
@@ -161,74 +162,9 @@ export default function SearchResultsPage() {
Articles ({articles.length})
- {articles.map((result) => {
- const title = result.event.tags.find(([name]) => name === 'title')?.[1] || 'Untitled';
- const summary = result.event.tags.find(([name]) => name === 'summary')?.[1];
- const image = result.event.tags.find(([name]) => name === 'image')?.[1];
- const publishedAt = result.event.tags.find(([name]) => name === 'published_at')?.[1];
- const identifier = result.event.tags.find(([name]) => name === 'd')?.[1] || '';
- const hashtags = result.event.tags
- .filter(([name]) => name === 't')
- .map(([, value]) => value)
- .slice(0, 3);
-
- const date = publishedAt
- ? new Date(parseInt(publishedAt) * 1000)
- : new Date(result.event.created_at * 1000);
-
- const naddr = nip19.naddrEncode({
- kind: 30023,
- pubkey: result.event.pubkey,
- identifier,
- });
-
- return (
-
-
- {image && (
-
-

-
- )}
-
-
- {title}
-
- {summary && (
-
- {summary}
-
- )}
-
-
-
-
-
-
- {hashtags.length > 0 && (
-
- {hashtags.map((tag) => (
-
- #{tag}
-
- ))}
-
- )}
-
-
-
- );
- })}
+ {articles.map((result) => (
+
+ ))}
)}