mirror of
https://github.com/mroxso/zelo-news.git
synced 2026-06-04 17:41:10 +02:00
Refactor article rendering by introducing ArticlePreview component across various pages and removing redundant code
This commit is contained in:
104
src/components/ArticlePreview.tsx
Normal file
104
src/components/ArticlePreview.tsx
Normal file
@@ -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 (
|
||||
<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={`${titleSize} font-bold line-clamp-2 mb-2`}>
|
||||
{title}
|
||||
</h3>
|
||||
{summary && (
|
||||
<p className={`text-muted-foreground text-sm ${summaryLines}`}>
|
||||
{summary}
|
||||
</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className={`flex items-center gap-2 text-xs text-muted-foreground ${showAuthor || hashtags.length > 0 ? 'mb-3' : ''}`}>
|
||||
<Calendar className="h-3 w-3" />
|
||||
<time dateTime={date.toISOString()}>
|
||||
{date.toLocaleDateString('en-US', dateFormat as Intl.DateTimeFormatOptions)}
|
||||
</time>
|
||||
</div>
|
||||
{showAuthor && (
|
||||
<div className={`flex items-center gap-2 ${hashtags.length > 0 ? '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>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<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();
|
||||
@@ -157,7 +67,7 @@ export function LatestArticles() {
|
||||
{/* Posts Grid */}
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{visiblePosts.map((post) => (
|
||||
<ArticleCard key={post.id} post={post} />
|
||||
<ArticlePreview key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<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);
|
||||
@@ -171,7 +82,7 @@ export function LatestInHashtag({ hashtag, icon }: LatestInHashtagProps) {
|
||||
{/* Posts Grid */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{visiblePosts.map((post) => (
|
||||
<HashtagArticleCard key={post.id} post={post} />
|
||||
<ArticlePreview key={post.id} post={post} variant="compact" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
) : posts && posts.length > 0 ? (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{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 (
|
||||
<Link key={post.id} to={`/${naddr}`}>
|
||||
<Card className="h-full hover:shadow-lg transition-shadow cursor-pointer group overflow-hidden">
|
||||
{image && (
|
||||
<div className="relative h-48 overflow-hidden">
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<CardHeader className="space-y-3">
|
||||
<h3 className="text-lg font-semibold line-clamp-2 group-hover:text-primary transition-colors">
|
||||
{title}
|
||||
</h3>
|
||||
{summary && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-3">
|
||||
{summary}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<time dateTime={date.toISOString()}>
|
||||
{date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
+{tags.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{posts.map((post) => (
|
||||
<ArticlePreview key={post.id} post={post} showAuthor={false} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="border-dashed">
|
||||
|
||||
@@ -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})
|
||||
</h2>
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{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 (
|
||||
<Link key={result.event.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 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>
|
||||
);
|
||||
})}
|
||||
{articles.map((result) => (
|
||||
<ArticlePreview key={result.event.id} post={result.event} showAuthor={true} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user