diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index f17d471..df69ec0 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -10,6 +10,7 @@ import Nip05ProfilePage from "./pages/Nip05ProfilePage"; import ArticleByDTagPage from "./pages/ArticleByDTagPage"; import { NIP19Page } from "./pages/NIP19Page"; import SettingsPage from "./pages/SettingsPage"; +import TagPage from "./pages/TagPage"; import NotFound from "./pages/NotFound"; import HomePage from "./pages/HomePage"; @@ -30,6 +31,8 @@ export function AppRouter() { } /> {/* Article by d-tag route (e.g., /article/my-article-slug) */} } /> + {/* Tag browsing route (e.g., /tag/bitcoin) */} + } /> {/* NIP-19 route for all Nostr identifiers (npub, nprofile, naddr, note, nevent) */} } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} diff --git a/src/components/ArticleView.tsx b/src/components/ArticleView.tsx index d96dd60..9c263e1 100644 --- a/src/components/ArticleView.tsx +++ b/src/components/ArticleView.tsx @@ -182,7 +182,7 @@ export function ArticleView({ post }: ArticleViewProps) { {hashtags.map((tag) => ( #{tag} diff --git a/src/components/LatestInHashtag.tsx b/src/components/LatestInHashtag.tsx index eb133b5..fa8f056 100644 --- a/src/components/LatestInHashtag.tsx +++ b/src/components/LatestInHashtag.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card, CardHeader } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -5,6 +6,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { Hash, ChevronRight } from 'lucide-react'; import { useBlogPostsByHashtag } from '@/hooks/useBlogPostsByHashtag'; import { ArticlePreview } from '@/components/ArticlePreview'; +import { deduplicateEvents } from '@/lib/deduplicateEvents'; interface LatestInHashtagProps { hashtag: string; @@ -16,7 +18,12 @@ const INITIAL_POSTS_COUNT = 3; export function LatestInHashtag({ hashtag, icon, title }: LatestInHashtagProps) { const navigate = useNavigate(); - const { data: posts, isLoading } = useBlogPostsByHashtag(hashtag, 4); + const { data, isLoading } = useBlogPostsByHashtag(hashtag, 4); + + // Remove duplicate events by ID + const posts = useMemo(() => { + return deduplicateEvents(data?.pages.flat() || []); + }, [data?.pages]); // Loading state if (isLoading) { @@ -67,7 +74,7 @@ export function LatestInHashtag({ hashtag, icon, title }: LatestInHashtagProps) {hasMore && ( +
+ +
+ +
+
+ + {/* Loading skeletons */} +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( + + + + + + + + ))} +
+ + + ); + } + + return ( +
+
+ {/* Header with back button */} +
+ + + {/* Tag heading */} +
+ +
+

+ {hashtag} +

+
+
+
+ + {/* No results */} + {!isLoading && posts.length === 0 && ( + + +
+

+ No articles found with tag #{hashtag}. Check your relay connections or wait for content to load. +

+
+
+
+ )} + + {/* Posts Grid */} + {posts && posts.length > 0 && ( + <> +
+ {posts.map((post) => ( + + ))} +
+ + {/* Infinite scroll sentinel */} + {hasNextPage && ( +
+ {isFetchingNextPage && ( +
+ {[1, 2, 3].map((i) => ( + + + + + + + + ))} +
+ )} +
+ )} + + )} +
+
+ ); +}