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(() => {