From c1067b3245e2b0257e0ff8b4352b0cdd41105aee Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sat, 5 Sep 2026 14:53:13 +0200 Subject: [PATCH] feat(mobile): add header wordmark and full-screen image viewer Header now shows the PawFeed paw+text logo on the Feed tab (matching web's Sidebar wordmark). Tapping a post image opens a full-screen viewer as a route-based fullScreenModal, with a blurred edge-to-edge copy of the same photo behind the sharp foreground image instead of a flat black backdrop, for a more dynamic Photos-app-style lightbox. --- mobile/app/(app)/(tabs)/_layout.tsx | 20 ++++++++++++-- mobile/app/(app)/_layout.tsx | 4 +++ mobile/app/(app)/image-viewer.tsx | 43 +++++++++++++++++++++++++++++ mobile/src/components/PostCard.tsx | 39 +++++++++++++++++--------- 4 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 mobile/app/(app)/image-viewer.tsx diff --git a/mobile/app/(app)/(tabs)/_layout.tsx b/mobile/app/(app)/(tabs)/_layout.tsx index 75814e6..38429dd 100644 --- a/mobile/app/(app)/(tabs)/_layout.tsx +++ b/mobile/app/(app)/(tabs)/_layout.tsx @@ -1,7 +1,7 @@ import { View, Text, Pressable, StyleSheet } from "react-native"; import { Tabs, useRouter } from "expo-router"; import { useQuery } from "@tanstack/react-query"; -import { Home, Compass, Bell, Heart, MessageCircle } from "lucide-react-native"; +import { Home, Compass, Bell, Heart, MessageCircle, PawPrint } from "lucide-react-native"; import { useTRPC } from "../../../src/api/trpc"; import { useActivePet } from "../../../src/context/ActivePetContext"; import { colors, fontFamily } from "../../../src/theme/tokens"; @@ -29,7 +29,11 @@ export default function TabsLayout() { > }} + options={{ + headerTitle: () => , + tabBarLabel: "Feed", + tabBarIcon: ({ color, size }) => , + }} /> + + PawFeed + + ); +} + function MessagesHeaderButton() { const router = useRouter(); return ( @@ -79,6 +93,8 @@ function NotificationsTabIcon({ color, size }: { color: string; size: number }) const styles = StyleSheet.create({ headerButton: { marginRight: 16 }, + wordmark: { flexDirection: "row", alignItems: "center", gap: 8 }, + wordmarkText: { fontFamily: fontFamily.bold, fontSize: 20, color: colors.ink }, badge: { position: "absolute", top: -4, diff --git a/mobile/app/(app)/_layout.tsx b/mobile/app/(app)/_layout.tsx index 97ee661..1ba7ed2 100644 --- a/mobile/app/(app)/_layout.tsx +++ b/mobile/app/(app)/_layout.tsx @@ -16,6 +16,10 @@ export default function AppLayout() { + ); } diff --git a/mobile/app/(app)/image-viewer.tsx b/mobile/app/(app)/image-viewer.tsx new file mode 100644 index 0000000..473b1be --- /dev/null +++ b/mobile/app/(app)/image-viewer.tsx @@ -0,0 +1,43 @@ +import { View, Pressable, Image, useWindowDimensions, StyleSheet } from "react-native"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { X } from "lucide-react-native"; +import { colors } from "../../src/theme/tokens"; + +/** + * Full-screen "tap to enlarge" viewer for a single post image, pushed as a + * route (presented as `fullScreenModal` in (app)/_layout.tsx) rather than + * React Native's , which didn't reliably size its content full-screen + * during development. Background is a blurred, edge-to-edge copy of the + * same photo (common lightbox pattern — Photos app, Instagram stories) + * instead of a flat black scrim, so the surrounding color still reads even + * when the photo doesn't match the screen's aspect ratio. + * + * Shows only the tapped image, not a swipeable carousel between a post's + * other images — closing and tapping a different thumbnail in the feed + * carousel shows that one instead. Multi-image swipe-in-viewer is a + * fast-follow. + */ +export default function ImageViewerScreen() { + const { images, index } = useLocalSearchParams<{ images: string; index: string }>(); + const router = useRouter(); + const { width, height } = useWindowDimensions(); + const uris: string[] = JSON.parse(images); + const uri = uris[Number(index) || 0]; + + return ( + router.back()}> + + + + router.back()} hitSlop={12}> + + + + ); +} + +const styles = StyleSheet.create({ + backdrop: { flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: colors.ink }, + scrim: { backgroundColor: "rgba(0,0,0,0.35)" }, + closeButton: { position: "absolute", top: 48, right: 20, zIndex: 1, padding: 8 }, +}); diff --git a/mobile/src/components/PostCard.tsx b/mobile/src/components/PostCard.tsx index 8e25d5d..28c6836 100644 --- a/mobile/src/components/PostCard.tsx +++ b/mobile/src/components/PostCard.tsx @@ -62,6 +62,11 @@ export function PostCard({ post, actingPetId }: Props) { const router = useRouter(); const cardWidth = width - 32; // matches the 16px screen padding used by the feed list + const openViewer = (index: number) => { + const uris = post.images.map((image) => getMediaUrl(image.storageKey)); + router.push({ pathname: "/image-viewer", params: { images: JSON.stringify(uris), index: String(index) } }); + }; + return ( router.push(`/pets/${post.pet.id}`)}> @@ -80,7 +85,7 @@ export function PostCard({ post, actingPetId }: Props) { ) : ( - + )} {post.caption ? {post.caption} : null} @@ -100,34 +105,42 @@ export function PostCard({ post, actingPetId }: Props) { ); } -function PostImages({ images, width }: { images: PostCardImage[]; width: number }) { +type PostImagesProps = { + images: PostCardImage[]; + width: number; + onPressImage: (index: number) => void; +}; + +function PostImages({ images, width, onPressImage }: PostImagesProps) { if (images.length === 0) return null; if (images.length === 1) { - return ; + return onPressImage(0)} />; } return ( - {images.map((image) => ( - + {images.map((image, index) => ( + onPressImage(index)} /> ))} ); } -function PostImage({ image, width }: { image: PostCardImage; width: number }) { +function PostImage({ image, width, onPress }: { image: PostCardImage; width: number; onPress: () => void }) { const rawRatio = image.width && image.height ? image.width / image.height : 1; const aspectRatio = Math.min(1.91, Math.max(0.75, rawRatio)); return ( - + + + ); }