- Sidebar + MobileNav: auto-call setActivePet(pets[0]) when activePetId is null so Follow/reaction buttons work immediately after onboarding - PostCard detail mode: reduce image max-h from 70dvh to 50dvh so the footer (paw + comment buttons) stays visible within the dialog - ProfileTabs dialog: reduce max-h from 90dvh to 85dvh - Auth pages: remove layout.logoPlacement and colorText (not in Clerk Variables type, caused Docker build failure) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
5.2 KiB
TypeScript
138 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback } from "react";
|
|
import { Layers2 } from "lucide-react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useTRPC } from "@/trpc/client";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
import { MilestoneCard } from "@/components/feed/MilestoneCard";
|
|
import { PostCard } from "@/components/feed/PostCard";
|
|
import { getMediaUrl } from "@/lib/media-url";
|
|
import type { PostCardPost } from "@/components/feed/PostCard";
|
|
|
|
interface ProfileTabsProps {
|
|
petId: string;
|
|
}
|
|
|
|
/**
|
|
* ProfileTabs — Posts (3-col photo grid) + Milestones (chronological list) (MILE-02).
|
|
*
|
|
* Posts tab: photo posts fetched via posts.byPetId; carousel posts get a Layers2 overlay.
|
|
* Milestones tab: milestone posts via milestones.listByPet rendered as MilestoneCard.
|
|
* Active tab has orange-500 underline (shadcn Tabs default with --primary: orange-500).
|
|
*/
|
|
export function ProfileTabs({ petId }: ProfileTabsProps) {
|
|
const trpc = useTRPC();
|
|
const [_activeTab, setActiveTab] = useState("posts");
|
|
const [selectedPost, setSelectedPost] = useState<PostCardPost | null>(null);
|
|
|
|
const { data: postsData } = useQuery(
|
|
trpc.posts.byPetId.queryOptions({ petId, limit: 30 })
|
|
);
|
|
|
|
const { data: milestonesData } = useQuery(
|
|
trpc.milestones.listByPet.queryOptions({ petId, limit: 30 })
|
|
);
|
|
|
|
const posts = postsData?.posts ?? [];
|
|
const milestones = (milestonesData ?? []) as PostCardPost[];
|
|
|
|
const handleTabChange = useCallback((tab: string) => {
|
|
setActiveTab(tab);
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<Tabs defaultValue="posts" onValueChange={handleTabChange}>
|
|
<TabsList className="w-full rounded-none border-b bg-transparent h-auto p-0">
|
|
<TabsTrigger
|
|
value="posts"
|
|
className="flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-orange-500 data-[state=active]:text-foreground data-[state=active]:shadow-none pb-2"
|
|
>
|
|
Posts
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="milestones"
|
|
className="flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-orange-500 data-[state=active]:text-foreground data-[state=active]:shadow-none pb-2"
|
|
>
|
|
Milestones
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* Posts tab — 3-column photo grid, tap to open full PostCard */}
|
|
<TabsContent value="posts" className="mt-0">
|
|
{posts.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground text-center py-10">
|
|
No posts yet
|
|
</p>
|
|
) : (
|
|
<div className="grid grid-cols-3 gap-0.5 mt-0.5">
|
|
{posts.map((post) => {
|
|
const firstImage = post.images?.[0];
|
|
const isCarousel = (post.images?.length ?? 0) > 1;
|
|
const mediaUrl = firstImage ? getMediaUrl(firstImage.storageKey) : null;
|
|
|
|
return (
|
|
<button
|
|
key={post.id}
|
|
type="button"
|
|
onClick={() => setSelectedPost(post)}
|
|
className="relative aspect-square overflow-hidden bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-500"
|
|
aria-label={post.caption ?? "View post"}
|
|
>
|
|
{mediaUrl ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={mediaUrl}
|
|
alt={post.caption ?? "Post"}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full bg-muted" />
|
|
)}
|
|
{isCarousel && (
|
|
<div className="absolute top-1.5 right-1.5">
|
|
<Layers2 className="h-4 w-4 text-white drop-shadow" aria-hidden="true" />
|
|
</div>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
|
|
{/* Milestones tab — chronological MilestoneCard list */}
|
|
<TabsContent value="milestones" className="mt-4 space-y-4">
|
|
{milestones.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground text-center py-10">
|
|
No milestones yet
|
|
</p>
|
|
) : (
|
|
milestones.map((post) => (
|
|
<MilestoneCard key={post.id} post={post} />
|
|
))
|
|
)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* Post detail dialog — centered overlay, no X button (3-dots in PostCard header handles options) */}
|
|
<Dialog open={!!selectedPost} onOpenChange={(open) => { if (!open) setSelectedPost(null); }}>
|
|
<DialogContent
|
|
showCloseButton={false}
|
|
className="max-w-[560px] w-full p-0 overflow-y-auto max-h-[85dvh] rounded-xl"
|
|
>
|
|
{selectedPost && (
|
|
<PostCard
|
|
post={selectedPost}
|
|
variant="detail"
|
|
onDeleted={() => setSelectedPost(null)}
|
|
/>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|