feat: update LatestInHashtag component to support multiple hashtags and improve blog post validation

This commit is contained in:
2025-11-23 01:58:45 +01:00
parent e7035da411
commit 5ef088421e
2 changed files with 69 additions and 12 deletions

View File

@@ -4,21 +4,76 @@ import { Card, CardHeader } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Hash, ChevronRight } from 'lucide-react';
import { useBlogPostsByHashtag } from '@/hooks/useBlogPostsByHashtag';
import { useNostr } from '@nostrify/react';
import { useInfiniteQuery } from '@tanstack/react-query';
import type { NostrEvent } from '@nostrify/nostrify';
import { ArticlePreview } from '@/components/ArticlePreview';
import { deduplicateEvents } from '@/lib/deduplicateEvents';
interface LatestInHashtagProps {
hashtag: string;
hashtags: string | string[];
icon?: React.ReactNode;
title?: string;
}
interface BlogPost extends NostrEvent {
kind: 30023;
}
/**
* Validates that a Nostr event is a valid NIP-23 blog post
*/
function validateBlogPost(event: NostrEvent): event is BlogPost {
if (event.kind !== 30023) return false;
const d = event.tags.find(([name]) => name === 'd')?.[1];
const title = event.tags.find(([name]) => name === 'title')?.[1];
if (!d || !title) return false;
return true;
}
const INITIAL_POSTS_COUNT = 3;
export function LatestInHashtag({ hashtag, icon, title }: LatestInHashtagProps) {
export function LatestInHashtag({ hashtags, icon, title }: LatestInHashtagProps) {
const navigate = useNavigate();
const { data, isLoading } = useBlogPostsByHashtag(hashtag, 4);
const { nostr } = useNostr();
// Normalize hashtags to always be an array
const hashtagArray = useMemo(() => {
return Array.isArray(hashtags) ? hashtags : [hashtags];
}, [hashtags]);
const { data, isLoading } = useInfiniteQuery({
queryKey: ['blog-posts-hashtags', hashtagArray, 4],
queryFn: async ({ pageParam, signal }) => {
const filter: {
kinds: number[];
'#t': string[];
limit: number;
until?: number;
} = {
kinds: [30023],
'#t': hashtagArray.map(h => h.toLowerCase()),
limit: 4
};
if (pageParam) {
filter.until = pageParam;
}
const events = await nostr.query([filter], { signal: AbortSignal.any([signal, AbortSignal.timeout(3000)]) });
return events.filter(validateBlogPost);
},
getNextPageParam: (lastPage) => {
if (lastPage.length === 0) return undefined;
const oldestEvent = lastPage[lastPage.length - 1];
return oldestEvent.created_at;
},
initialPageParam: undefined as number | undefined,
});
// Remove duplicate events by ID
const posts = useMemo(() => {
@@ -66,15 +121,18 @@ export function LatestInHashtag({ hashtag, icon, title }: LatestInHashtagProps)
{icon || <Hash className="h-8 w-8 text-primary" />}
<div className="flex-1">
<h2 className="text-3xl font-bold tracking-tight">
{title || `Latest in #${hashtag}`}
{title || (hashtagArray.length === 1
? `Latest in #${hashtagArray[0]}`
: `Latest in ${hashtagArray.map(h => `#${h}`).join(', ')}`
)}
</h2>
{/* <p className="text-sm text-muted-foreground mt-1">
{posts.length} {posts.length === 1 ? 'article' : 'articles'} in this category
</p> */}
</div>
{hasMore && (
{hasMore && hashtagArray.length === 1 && (
<Button
onClick={() => navigate(`/tag/${encodeURIComponent(hashtag)}`)}
onClick={() => navigate(`/tag/${encodeURIComponent(hashtagArray[0])}`)}
variant="outline"
size="default"
className="gap-1"

View File

@@ -59,24 +59,23 @@ export default function HomePage() {
{/* Display interest sets or default hashtags */}
{displaySets ? (
// User's custom interest sets - show first hashtag from each set
// User's custom interest sets - show all hashtags from each set
displaySets
.filter((set) => set.hashtags.length > 0)
.map((set) => {
const primaryHashtag = set.hashtags[0];
return (
<LatestInHashtag
key={set.id}
hashtag={primaryHashtag}
hashtags={set.hashtags}
icon={<Hash className="h-6 w-6 text-primary" />}
title={set.title || `Latest in #${primaryHashtag}`}
title={set.title}
/>
);
})
) : (
// Default hashtags for logged-out users or users without interest sets
defaultHashtags.map(({ hashtag, icon }) => (
<LatestInHashtag key={hashtag} hashtag={hashtag} icon={icon} />
<LatestInHashtag key={hashtag} hashtags={hashtag} icon={icon} />
))
)}
</div>