From e4070648663a63879d8e1818c19b621d2bf32ec4 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sun, 21 Jun 2026 14:41:20 +0200 Subject: [PATCH] feat(explore): add trending tab with hot posts and hashtags - explore.getTrending: top-12 photo posts by reaction count (48h window) and top-10 hashtags by post count (7d window) - Explore page opens on Trending tab by default; species/breed tabs preserved with breed filter hidden when not applicable - Trending UI: hashtag chips linking to /hashtag/[tag] + 3-col post grid with paw-count overlay and PostCard detail dialog feat(follows): auto-trigger follower milestone posts - After each follow, count followers for followeePetId and check against all thresholds (500 / 1K / 2.5K / 5K / 10K / 25K / 50K / 100K / 250K / 500K / 1M) - Guard against duplicate awards via existing Milestone lookup - Fire-and-forget: milestone post created + fanned out to follower feeds without blocking the follow response --- src/app/(app)/explore/page.tsx | 284 +++++++++++++++++++++++++++------ src/trpc/routers/explore.ts | 37 +++++ src/trpc/routers/follows.ts | 46 +++++- 3 files changed, 313 insertions(+), 54 deletions(-) diff --git a/src/app/(app)/explore/page.tsx b/src/app/(app)/explore/page.tsx index c445894..145c1eb 100644 --- a/src/app/(app)/explore/page.tsx +++ b/src/app/(app)/explore/page.tsx @@ -1,12 +1,18 @@ "use client"; import { useState } from "react"; +import Link from "next/link"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { Loader2 } from "lucide-react"; +import { Loader2, TrendingUp } from "lucide-react"; import { useTRPC } from "@/trpc/client"; import { useActivePet } from "@/context/ActivePetContext"; import { ExploreCard } from "@/components/explore/ExploreCard"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { PostCard } from "@/components/feed/PostCard"; +import { getMediaUrl } from "@/lib/media-url"; +import type { PostCardPost } from "@/components/feed/PostCard"; import { Select, SelectContent, @@ -20,8 +26,13 @@ export default function ExplorePage() { const trpc = useTRPC(); const { activePetId } = useActivePet(); - const [activeSpeciesId, setActiveSpeciesId] = useState(null); + const [activeTab, setActiveTab] = useState("trending"); const [activeBreedId, setActiveBreedId] = useState(null); + const [selectedPost, setSelectedPost] = useState(null); + + const isTrending = activeTab === "trending"; + const activeSpeciesId = + activeTab === "trending" || activeTab === "all" ? null : activeTab; const { data: speciesList, isLoading: loadingSpecies } = useQuery( trpc.pets.listSpecies.queryOptions() @@ -35,12 +46,16 @@ export default function ExplorePage() { ); const breeds = breedData ?? []; + const { data: trendingData, isLoading: loadingTrending } = useQuery( + trpc.explore.getTrending.queryOptions(undefined, { enabled: isTrending }) + ); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, - isLoading, + isLoading: loadingPets, } = useInfiniteQuery( trpc.explore.listPets.infiniteQueryOptions( { @@ -51,15 +66,16 @@ export default function ExplorePage() { { getNextPageParam: (lastPage) => lastPage.nextCursor, initialCursor: undefined, + enabled: !isTrending, } ) ); const pets = data?.pages.flatMap((p) => p.pets) ?? []; - function handleSpeciesChange(speciesId: string | null) { - setActiveSpeciesId(speciesId); - setActiveBreedId(null); // reset breed when switching species + function handleTabChange(tab: string) { + setActiveTab(tab); + setActiveBreedId(null); } if (loadingSpecies) { @@ -74,13 +90,12 @@ export default function ExplorePage() {

Explore

- {/* Species tabs */} - handleSpeciesChange(v === "all" ? null : v)} - className="mb-4" - > + + + + Trending + All {(speciesList ?? []).map((s) => ( @@ -90,7 +105,7 @@ export default function ExplorePage() { - {/* Breed filter — only visible when a species is selected */} + {/* Breed filter — only when a species tab is active */} {activeSpeciesId && breeds.length > 0 && (