From f54462a0d356937d80f38e4cc8e7e5ea61cb0e8f Mon Sep 17 00:00:00 2001 From: mroxso <24775431+mroxso@users.noreply.github.com> Date: Tue, 6 May 2025 18:42:00 +0200 Subject: [PATCH] Feature: Add Tag Page (#101) * feat: Add TagPage and TagCard components for displaying trending and followed tags * refactor: Adjust layout and styling in TagPage component for improved UI * feat: Add Tag link to BottomBar for improved navigation * refactor: Conditionally render 'My Tags' tab and adjust layout in TagPage; comment out unused pubkey check in BottomBar * refactor: Replace TagIcon with HashIcon in BottomBar component for improved icon representation * refactor: Remove document title update in TagPage component * feat: Implement loading state and pagination in TagFeed and TagQuickViewFeed components * fix: Update QuickViewKind20NoteCard to enable link to note in TagQuickViewFeed component * fix: Increase limit for global events in TagPage to improve trending tags retrieval * fix: Update dependency array in useEffect to track full trendingTags array for accurate updates --------- Co-authored-by: highperfocused --- app/tag/page.tsx | 129 ++++++++++++++++++++++++++++++++ components/BottomBar.tsx | 9 ++- components/TagCard.tsx | 30 ++++++++ components/TagFeed.tsx | 52 ++++++++++--- components/TagQuickViewFeed.tsx | 52 ++++++++++--- 5 files changed, 253 insertions(+), 19 deletions(-) create mode 100644 app/tag/page.tsx create mode 100644 components/TagCard.tsx diff --git a/app/tag/page.tsx b/app/tag/page.tsx new file mode 100644 index 0000000..db7ce47 --- /dev/null +++ b/app/tag/page.tsx @@ -0,0 +1,129 @@ +'use client'; + +import Head from "next/head"; +import { useNostrEvents } from "nostr-react"; +import { useState, useEffect } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Skeleton } from "@/components/ui/skeleton"; +import TagCard from "@/components/TagCard"; + +export default function TagPage() { + const [trendingTags, setTrendingTags] = useState([]); + + let pubkey = ''; + if (typeof window !== 'undefined') { + pubkey = window.localStorage.getItem("pubkey") ?? ''; + } + + const { events: followEvents, isLoading: isFollowLoading } = useNostrEvents({ + filter: { + kinds: [3], + limit: 1, + authors: [pubkey], + }, + }); + + const { events: globalEvents, isLoading: isGlobalLoading } = useNostrEvents({ + filter: { + kinds: [20], + limit: 100, + }, + }); + + // Extract tags from followed users + const followedTags = followEvents + .flatMap(event => event.tags) + .filter(tag => tag[0] === 't') + .map(tag => tag[1]); + + // Get unique followed tags + const uniqueFollowedTags = Array.from(new Set(followedTags)); + + // Extract tags from global feed for trending tags + useEffect(() => { + if (globalEvents.length > 0) { + const allTags = globalEvents + .flatMap(event => event.tags) + .filter(tag => tag[0] === 't') + .map(tag => tag[1]); + + // Count tag occurrences to find trending tags + const tagCounts = allTags.reduce((acc, tag) => { + acc[tag] = (acc[tag] || 0) + 1; + return acc; + }, {} as Record); + + // Sort by frequency and get top 20 + const sortedTags = Object.entries(tagCounts) + .sort((a, b) => b[1] - a[1]) + .map(([tag]) => tag) + .slice(0, 20); + + // Only update state if the tags have actually changed + if (JSON.stringify(sortedTags) !== JSON.stringify(trendingTags)) { + setTrendingTags(sortedTags); + } + } + }, [globalEvents, trendingTags]); // Depend on the full trendingTags array to capture content and order changes + + return ( + <> + + LUMINA.rocks - Tags + + + + +
+ + + Trending Tags + {/* {pubkey && My Tags} */} + + + +
+ {isGlobalLoading ? ( + Array(8).fill(0).map((_, i) => ( +
+ +
+ )) + ) : trendingTags.length > 0 ? ( + trendingTags.map(tag => ( + + )) + ) : ( +
+

No trending tags found

+
+ )} +
+
+ + {pubkey && ( + +
+ {isFollowLoading ? ( + Array(4).fill(0).map((_, i) => ( +
+ +
+ )) + ) : uniqueFollowedTags.length > 0 ? ( + uniqueFollowedTags.map(tag => ( + + )) + ) : ( +
+

No followed tags found. Follow tags to see them here.

+
+ )} +
+
+ )} +
+
+ + ); +} diff --git a/components/BottomBar.tsx b/components/BottomBar.tsx index c3cd2fd..3011d3a 100644 --- a/components/BottomBar.tsx +++ b/components/BottomBar.tsx @@ -4,7 +4,7 @@ import { BellIcon, GlobeIcon, HomeIcon, RowsIcon, UploadIcon } from "@radix-ui/r import Link from "next/link" import { FormEvent, JSX, SVGProps, useEffect, useState } from "react" import { useRouter, usePathname } from 'next/navigation' -import { SearchIcon } from "lucide-react"; +import { HashIcon, SearchIcon, TagIcon } from "lucide-react"; export default function BottomBar() { const router = useRouter(); @@ -61,6 +61,13 @@ export default function BottomBar() { Upload )} + {/* {pubkey && ( */} + + {/* */} + + Tags + + {/* )} */} Search diff --git a/components/TagCard.tsx b/components/TagCard.tsx new file mode 100644 index 0000000..81454e0 --- /dev/null +++ b/components/TagCard.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import Link from 'next/link'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Hash } from "lucide-react"; + +interface TagCardProps { + tag: string; +} + +const TagCard: React.FC = ({ tag }) => { + return ( + + + + + + {tag} + + + +

+ View content tagged with #{tag} +

+
+
+ + ); +}; + +export default TagCard; \ No newline at end of file diff --git a/components/TagFeed.tsx b/components/TagFeed.tsx index 29783e5..1be0ed6 100644 --- a/components/TagFeed.tsx +++ b/components/TagFeed.tsx @@ -1,7 +1,9 @@ -import { useRef } from "react"; +import { useRef, useState } from "react"; import { useNostrEvents } from "nostr-react"; import KIND20Card from "./KIND20Card"; import { getImageUrl } from "@/utils/utils"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; interface TagFeedProps { tag: string; @@ -9,27 +11,59 @@ interface TagFeedProps { const TagFeed: React.FC = ({ tag }) => { const now = useRef(new Date()); // Make sure current time isn't re-rendered + const [limit, setLimit] = useState(25); - const { events } = useNostrEvents({ + const { events, isLoading } = useNostrEvents({ filter: { // since: dateToUnix(now.current), // all new events from now // since: 0, - limit: 100, + limit: limit, kinds: [20], "#t": [tag], }, }); + const loadMore = () => { + setLimit(prevLimit => prevLimit + 25); + }; + return ( <>
- {events.map((event) => ( - //

{event.pubkey} posted: {event.content}

-
- -
- ))} + {events.length === 0 && isLoading ? ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + ) : ( + events.map((event) => ( +
+ +
+ )) + )}
+ {!isLoading && ( +
+ +
+ )} ); } diff --git a/components/TagQuickViewFeed.tsx b/components/TagQuickViewFeed.tsx index bccf44c..f6898c6 100644 --- a/components/TagQuickViewFeed.tsx +++ b/components/TagQuickViewFeed.tsx @@ -1,7 +1,9 @@ -import { useRef } from "react"; +import { useRef, useState } from "react"; import { useNostrEvents } from "nostr-react"; import { getImageUrl } from "@/utils/utils"; import QuickViewKind20NoteCard from "./QuickViewKind20NoteCard"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; interface TagQuickViewFeedProps { tag: string; @@ -9,27 +11,59 @@ interface TagQuickViewFeedProps { const TagQuickViewFeed: React.FC = ({ tag }) => { const now = useRef(new Date()); // Make sure current time isn't re-rendered + const [limit, setLimit] = useState(25); - const { events } = useNostrEvents({ + const { events, isLoading } = useNostrEvents({ filter: { // since: dateToUnix(now.current), // all new events from now // since: 0, - limit: 100, + limit: limit, kinds: [20], "#t": [tag], }, }); + const loadMore = () => { + setLimit(prevLimit => prevLimit + 25); + }; + return ( <>
- {events.map((event) => ( - //

{event.pubkey} posted: {event.content}

-
- -
- ))} + {events.length === 0 && isLoading ? ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + ) : ( + events.map((event) => ( +
+ +
+ )) + )}
+ {!isLoading && ( +
+ +
+ )} ); }