Refactor article card rendering in LatestArticles and LatestInHashtag components for improved code reuse and readability

This commit is contained in:
2025-10-05 17:46:25 +02:00
parent c6ef98e4a0
commit 6b5d07d72d
2 changed files with 182 additions and 136 deletions

View File

@@ -1,16 +1,104 @@
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 { 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 { useBlogPosts } from '@/hooks/useBlogPosts';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
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 (
<Link to={`/${naddr}`}>
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full flex flex-col">
{image && (
<div className="aspect-video overflow-hidden bg-muted">
<img
src={image}
alt={title}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
/>
</div>
)}
<CardHeader className="flex-1">
<h3 className="text-xl sm:text-2xl font-bold line-clamp-2 mb-2">
{title}
</h3>
{summary && (
<p className="text-muted-foreground text-sm line-clamp-3">
{summary}
</p>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-3">
<Calendar className="h-3 w-3" />
<time dateTime={date.toISOString()}>
{date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
</div>
<div className="flex items-center gap-2 mb-3">
<Avatar className="h-6 w-6">
<AvatarImage src={avatarUrl} alt={displayName} />
<AvatarFallback className="text-xs">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="text-sm font-medium truncate">{displayName}</span>
</div>
{hashtags.length > 0 && (
<div className="flex flex-wrap gap-1">
{hashtags.map((tag: string) => (
<Badge key={tag} variant="secondary" className="text-xs">
#{tag}
</Badge>
))}
</div>
)}
</CardContent>
</Card>
</Link>
);
}
export function LatestArticles() {
const [visibleCount, setVisibleCount] = useState(INITIAL_POSTS_COUNT);
const { data: posts, isLoading } = useBlogPosts();
@@ -68,74 +156,9 @@ export function LatestArticles() {
{/* Posts Grid */}
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{visiblePosts.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 identifier = post.tags.find(([name]) => name === 'd')?.[1] || '';
const hashtags = post.tags
.filter(([name]) => name === 't')
.map(([, value]) => 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,
});
return (
<Link key={post.id} to={`/${naddr}`}>
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full flex flex-col">
{image && (
<div className="aspect-video overflow-hidden bg-muted">
<img
src={image}
alt={title}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
/>
</div>
)}
<CardHeader className="flex-1">
<h3 className="text-xl sm:text-2xl font-bold line-clamp-2 mb-2">
{title}
</h3>
{summary && (
<p className="text-muted-foreground text-sm line-clamp-3">
{summary}
</p>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-3">
<Calendar className="h-3 w-3" />
<time dateTime={date.toISOString()}>
{date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
</div>
{hashtags.length > 0 && (
<div className="flex flex-wrap gap-1">
{hashtags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
#{tag}
</Badge>
))}
</div>
)}
</CardContent>
</Card>
</Link>
);
})}
{visiblePosts.map((post) => (
<ArticleCard key={post.id} post={post} />
))}
</div>
{/* Load More Button */}

View File

@@ -1,11 +1,15 @@
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 { 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 { useBlogPostsByHashtag } from '@/hooks/useBlogPostsByHashtag';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
interface LatestInHashtagProps {
hashtag: string;
@@ -14,6 +18,90 @@ 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 (
<Link to={`/${naddr}`}>
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full flex flex-col">
{image && (
<div className="aspect-video overflow-hidden bg-muted">
<img
src={image}
alt={title}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
/>
</div>
)}
<CardHeader className="flex-1">
<h3 className="text-lg font-bold line-clamp-2 mb-2">
{title}
</h3>
{summary && (
<p className="text-muted-foreground text-sm line-clamp-2">
{summary}
</p>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-2">
<Calendar className="h-3 w-3" />
<time dateTime={date.toISOString()}>
{date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</time>
</div>
<div className="flex items-center gap-2 mb-2">
<Avatar className="h-6 w-6">
<AvatarImage src={avatarUrl} alt={displayName} />
<AvatarFallback className="text-xs">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="text-sm font-medium truncate">{displayName}</span>
</div>
{hashtags.length > 0 && (
<div className="flex flex-wrap gap-1">
{hashtags.map((tag: string) => (
<Badge key={tag} variant="secondary" className="text-xs">
#{tag}
</Badge>
))}
</div>
)}
</CardContent>
</Card>
</Link>
);
}
export function LatestInHashtag({ hashtag, icon }: LatestInHashtagProps) {
const navigate = useNavigate();
const { data: posts, isLoading } = useBlogPostsByHashtag(hashtag);
@@ -82,74 +170,9 @@ export function LatestInHashtag({ hashtag, icon }: LatestInHashtagProps) {
{/* Posts Grid */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{visiblePosts.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 identifier = post.tags.find(([name]) => name === 'd')?.[1] || '';
const hashtags = post.tags
.filter(([name]) => name === 't')
.map(([, value]) => 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,
});
return (
<Link key={post.id} to={`/${naddr}`}>
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full flex flex-col">
{image && (
<div className="aspect-video overflow-hidden bg-muted">
<img
src={image}
alt={title}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
/>
</div>
)}
<CardHeader className="flex-1">
<h3 className="text-lg font-bold line-clamp-2 mb-2">
{title}
</h3>
{summary && (
<p className="text-muted-foreground text-sm line-clamp-2">
{summary}
</p>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-2">
<Calendar className="h-3 w-3" />
<time dateTime={date.toISOString()}>
{date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</time>
</div>
{hashtags.length > 0 && (
<div className="flex flex-wrap gap-1">
{hashtags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
#{tag}
</Badge>
))}
</div>
)}
</CardContent>
</Card>
</Link>
);
})}
{visiblePosts.map((post) => (
<HashtagArticleCard key={post.id} post={post} />
))}
</div>
</div>