diff --git a/src/app/(app)/pets/[petId]/insights/page.tsx b/src/app/(app)/pets/[petId]/insights/page.tsx index d47d810..f19c148 100644 --- a/src/app/(app)/pets/[petId]/insights/page.tsx +++ b/src/app/(app)/pets/[petId]/insights/page.tsx @@ -27,5 +27,5 @@ export default async function InsightsPage({ params }: PageProps) { redirect(`/pets/${petId}`); } - return ; + return ; } diff --git a/src/components/insights/InsightsDashboard.tsx b/src/components/insights/InsightsDashboard.tsx index f74bcaa..02d749b 100644 --- a/src/components/insights/InsightsDashboard.tsx +++ b/src/components/insights/InsightsDashboard.tsx @@ -1,15 +1,20 @@ "use client"; +import { useEffect, useState } from "react"; import { PawPrint, MessageCircle, ImageOff, Loader2 } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; import { useTRPC } from "@/trpc/client"; import { getMediaUrl } from "@/lib/media-url"; import { BlurImage } from "@/components/ui/blur-image"; +import { PostDetailDialog } from "@/components/feed/PostDetailDialog"; +import type { PostCardPost } from "@/components/feed/PostCard"; +import { StoryViewer } from "@/components/stories/StoryViewer"; interface InsightsDashboardProps { petId: string; petName: string; + petAvatarKey: string | null; } function StatTile({ label, value }: { label: string; value: number }) { @@ -27,11 +32,22 @@ interface SpotlightCardProps { thumbnailBlurhash: string | null; subtitle: string | null; emptyLabel: string; + onClick?: () => void; } -function SpotlightCard({ title, thumbnailUrl, thumbnailBlurhash, subtitle, emptyLabel }: SpotlightCardProps) { +/** + * Gitea #30 follow-up — spotlight items are clickable when they point at a + * real post/story (onClick set); the empty state (no item yet) renders the + * same card as a disabled, non-interactive button. + */ +function SpotlightCard({ title, thumbnailUrl, thumbnailBlurhash, subtitle, emptyLabel, onClick }: SpotlightCardProps) { return ( -
+
+ ); } @@ -63,12 +79,33 @@ function SpotlightCard({ title, thumbnailUrl, thumbnailBlurhash, subtitle, empty * and comments across a pet's posts + active stories, plus which post/story * is resonating most. Reads insights.getPetInsights (owner-only). */ -export function InsightsDashboard({ petId, petName }: InsightsDashboardProps) { +export function InsightsDashboard({ petId, petName, petAvatarKey }: InsightsDashboardProps) { const t = useTranslations("Insights"); const trpc = useTRPC(); const { data, isLoading } = useQuery(trpc.insights.getPetInsights.queryOptions({ petId })); + // Post spotlight click — same "fetch by id, open in dialog" pattern as the + // notifications page (PostDetailDialog needs the full post shape, not the + // trimmed insights payload). + const [openPostId, setOpenPostId] = useState(null); + const [selectedPost, setSelectedPost] = useState(null); + const { data: fetchedPost } = useQuery({ + ...trpc.posts.getById.queryOptions({ postId: openPostId ?? "" }), + enabled: !!openPostId, + }); + useEffect(() => { + if (fetchedPost) setSelectedPost(fetchedPost as PostCardPost); + }, [fetchedPost]); + function closePostDialog() { + setOpenPostId(null); + setSelectedPost(null); + } + + // Story spotlight click — StoryViewer takes an initialStoryId to jump + // straight to the right story instead of always starting at index 0. + const [openStoryId, setOpenStoryId] = useState(null); + if (isLoading || !data) { return (
@@ -109,6 +146,11 @@ export function InsightsDashboard({ petId, petName }: InsightsDashboardProps) { : null } emptyLabel={t("noPostsYet")} + onClick={ + data.topPostByReactions + ? () => setOpenPostId(data.topPostByReactions!.id) + : undefined + } /> setOpenPostId(data.topPostByComments!.id) + : undefined + } />
@@ -146,6 +193,11 @@ export function InsightsDashboard({ petId, petName }: InsightsDashboardProps) { : null } emptyLabel={t("noActiveStoriesYet")} + onClick={ + data.topStoryByReactions + ? () => setOpenStoryId(data.topStoryByReactions!.id) + : undefined + } /> setOpenStoryId(data.topStoryByComments!.id) + : undefined + } />

{t("storiesExpireHint")}

+ + + + {openStoryId && ( + !o && setOpenStoryId(null)} + initialStoryId={openStoryId} + /> + )} ); } diff --git a/src/components/stories/StoryViewer.tsx b/src/components/stories/StoryViewer.tsx index 7a995f1..1ed35a8 100644 --- a/src/components/stories/StoryViewer.tsx +++ b/src/components/stories/StoryViewer.tsx @@ -35,6 +35,8 @@ interface StoryViewerProps { petAvatarKey: string | null; open: boolean; onOpenChange: (open: boolean) => void; + /** Gitea #30 — jump straight to this story (e.g. from the Insights spotlight) instead of always starting at index 0. Falls back to 0 if not found among the active stories. */ + initialStoryId?: string; } const IMAGE_STORY_DURATION_MS = 5000; @@ -56,6 +58,7 @@ export function StoryViewer({ petAvatarKey, open, onOpenChange, + initialStoryId, }: StoryViewerProps) { const t = useTranslations("StoryViewer"); const trpc = useTRPC(); @@ -174,14 +177,19 @@ export function StoryViewer({ advance(); }, [advance]); - // Reset index when viewer opens + // Reset index when viewer opens — jump straight to initialStoryId (if given + // and already among the loaded stories) instead of always starting at 0. useEffect(() => { if (open) { - setCurrentIndex(0); + const initialIndex = initialStoryId + ? Math.max(0, stories.findIndex((s) => s.id === initialStoryId)) + : 0; + setCurrentIndex(initialIndex); setProgress(0); setIsMuted(true); setCommentsOpen(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); const handleClose = useCallback(() => {