From a737a179766d4e5b47c799f37aa45218b8b7fa2c Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sat, 5 Sep 2026 13:47:20 +0200 Subject: [PATCH] fix(mobile): feed separation, adaptive launcher icon; feat: full pet profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes from user testing on a real device: - Feed: replaced an invisible spacing-only separator with an actual hairline divider between posts. - Android launcher icon showed Expo's default placeholder (a blue arrow) because the app icon and the Android adaptive-icon layers (foreground/background/monochrome) are two separate asset sets — only icon.png/favicon.png had been replaced. Added scripts/derive-adaptive- icon.js to crop the paw glyph out of icon.png (detected by its marigold color) and generate all three adaptive-icon layers plus the splash icon; this is an explicit placeholder per user's own call, not final art. New: full pet profile (app/(app)/pets/[petId]/), matching web's src/app/(app)/pets/[petId]/page.tsx: - View: identity, species, bio/adoption story, follower/following counts (tappable → list screens), the pet's own posts below. - Follow/unfollow with real initial state (follows.isFollowing) and optimistic toggle — unlike feed reactions, this endpoint actually exists, so no need to fake an initial state. - Edit screen (name/nickname/bio/adoption story via pets.update) for the owner's own pets. - Tapping a pet (feed header, Explore card, Pets-tab row) now opens this profile instead of nowhere; the Pets tab's active-pet switch is now a separate checkmark control instead of overloading the whole row tap. PostCard's post-shape prop is now structural rather than tied to feed.getFeed's output type, since posts.byPetId selects `pet` via the narrower petIdentitySelect (no isVerified/owner) — both need to satisfy the same component. Verified end-to-end on-device: feed separator visible, own-pet profile shows a working Bearbeiten button whose form is correctly prefilled (a stray edit was made to Bianca's nickname field by mistake during testing and was discarded via back-navigation without saving — never sent to the server), someone else's pet profile shows a working Follow/Unfollow toggle, followers list navigates correctly, and the home-screen launcher icon now shows the paw instead of the default Expo icon. --- mobile/app.json | 2 +- mobile/app/(app)/(tabs)/explore.tsx | 23 +-- mobile/app/(app)/(tabs)/index.tsx | 9 +- mobile/app/(app)/(tabs)/pets.tsx | 55 ++++++- mobile/app/(app)/_layout.tsx | 20 +-- mobile/app/(app)/pets/[petId]/edit.tsx | 100 ++++++++++++ mobile/app/(app)/pets/[petId]/followers.tsx | 50 ++++++ mobile/app/(app)/pets/[petId]/following.tsx | 50 ++++++ mobile/app/(app)/pets/[petId]/index.tsx | 159 ++++++++++++++++++++ mobile/assets/android-icon-background.png | Bin 17549 -> 22970 bytes mobile/assets/android-icon-foreground.png | Bin 78796 -> 50869 bytes mobile/assets/android-icon-monochrome.png | Bin 4140 -> 20722 bytes mobile/assets/splash-icon.png | Bin 17547 -> 50869 bytes mobile/scripts/derive-adaptive-icon.js | 102 +++++++++++++ mobile/src/components/PostCard.tsx | 49 +++++- 15 files changed, 583 insertions(+), 36 deletions(-) create mode 100644 mobile/app/(app)/pets/[petId]/edit.tsx create mode 100644 mobile/app/(app)/pets/[petId]/followers.tsx create mode 100644 mobile/app/(app)/pets/[petId]/following.tsx create mode 100644 mobile/app/(app)/pets/[petId]/index.tsx create mode 100644 mobile/scripts/derive-adaptive-icon.js diff --git a/mobile/app.json b/mobile/app.json index d4cce29..20a878e 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -15,7 +15,7 @@ "android": { "package": "org.pawfeed.app", "adaptiveIcon": { - "backgroundColor": "#E6F4FE", + "backgroundColor": "#fafafa", "foregroundImage": "./assets/android-icon-foreground.png", "backgroundImage": "./assets/android-icon-background.png", "monochromeImage": "./assets/android-icon-monochrome.png" diff --git a/mobile/app/(app)/(tabs)/explore.tsx b/mobile/app/(app)/(tabs)/explore.tsx index 6b8fcf1..c876567 100644 --- a/mobile/app/(app)/(tabs)/explore.tsx +++ b/mobile/app/(app)/(tabs)/explore.tsx @@ -1,4 +1,5 @@ -import { View, Text, FlatList, ActivityIndicator, StyleSheet } from "react-native"; +import { View, Text, FlatList, Pressable, ActivityIndicator, StyleSheet } from "react-native"; +import { useRouter } from "expo-router"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useTRPC } from "../../../src/api/trpc"; import { PetAvatar } from "../../../src/components/PetAvatar"; @@ -62,21 +63,25 @@ export default function ExploreScreen() { } function ExplorePetCard({ pet }: { pet: ExplorePet }) { + const router = useRouter(); return ( - - - - {formatPetName(pet)} - - {pet._count.followers} Follower - + router.push(`/pets/${pet.id}`)}> + + + + {formatPetName(pet)} + + {pet._count.followers} Follower + + ); } const styles = StyleSheet.create({ list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, row: { gap: 12 }, - card: { flex: 1, alignItems: "center", gap: 6, marginBottom: 12 }, + cardPress: { flex: 1 }, + card: { alignItems: "center", gap: 6, marginBottom: 12 }, center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, errorText: { ...typography.body, color: colors.alertRed }, name: { ...typography.body, fontFamily: typography.title.fontFamily, fontSize: 14 }, diff --git a/mobile/app/(app)/(tabs)/index.tsx b/mobile/app/(app)/(tabs)/index.tsx index d85d0ec..127d308 100644 --- a/mobile/app/(app)/(tabs)/index.tsx +++ b/mobile/app/(app)/(tabs)/index.tsx @@ -47,7 +47,11 @@ export default function FeedScreen() { keyExtractor={(post) => post.id} contentContainerStyle={styles.list} renderItem={({ item }) => } - ItemSeparatorComponent={() => } + ItemSeparatorComponent={() => ( + + + + )} refreshControl={} onEndReachedThreshold={0.5} onEndReached={() => { @@ -65,7 +69,8 @@ export default function FeedScreen() { const styles = StyleSheet.create({ list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, - separator: { height: 24 }, + separatorWrap: { paddingVertical: 16 }, + separatorLine: { height: 1, backgroundColor: colors.hairlineGray }, center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, errorText: { ...typography.body, color: colors.alertRed }, footer: { marginVertical: 16 }, diff --git a/mobile/app/(app)/(tabs)/pets.tsx b/mobile/app/(app)/(tabs)/pets.tsx index 89e05b1..e967b97 100644 --- a/mobile/app/(app)/(tabs)/pets.tsx +++ b/mobile/app/(app)/(tabs)/pets.tsx @@ -1,4 +1,5 @@ import { View, Text, FlatList, Pressable, ActivityIndicator, StyleSheet } from "react-native"; +import { useRouter } from "expo-router"; import { Check } from "lucide-react-native"; import { useActivePet } from "../../../src/context/ActivePetContext"; import { PetAvatar } from "../../../src/components/PetAvatar"; @@ -9,13 +10,15 @@ import { colors, typography } from "../../../src/theme/tokens"; import type { Pet } from "../../../src/api/types"; /** - * List of the owner's own pets (pets.list()). Tapping a pet sets it as the - * "active pet" used for every pet-scoped action elsewhere in the app — - * mobile's stand-in for web's separate pet-switcher dropdown, since there's - * no header chrome to put one in yet. Tapping through to a pet's public - * profile is a later phase (getProfile screen not built yet). + * List of the owner's own pets (pets.list()). Tapping a row opens that + * pet's profile (app/(app)/pets/[petId]/index.tsx); the checkmark is a + * separate tap target for setting the "active pet" used by every + * pet-scoped action elsewhere in the app — these are two different actions + * and shouldn't share one tap target (a pet you're not actively posting as + * should still be viewable/editable). */ export default function PetsScreen() { + const router = useRouter(); const { pets, activePetId, setActivePetId, isLoading } = useActivePet(); if (isLoading) { @@ -33,7 +36,12 @@ export default function PetsScreen() { contentContainerStyle={styles.list} ItemSeparatorComponent={() => } renderItem={({ item }) => ( - setActivePetId(item.id)} /> + router.push(`/pets/${item.id}`)} + onSetActive={() => setActivePetId(item.id)} + /> )} ListEmptyComponent={ @@ -44,7 +52,17 @@ export default function PetsScreen() { ); } -function PetRow({ pet, isActive, onPress }: { pet: Pet; isActive: boolean; onPress: () => void }) { +function PetRow({ + pet, + isActive, + onPress, + onSetActive, +}: { + pet: Pet; + isActive: boolean; + onPress: () => void; + onSetActive: () => void; +}) { return ( @@ -56,7 +74,20 @@ function PetRow({ pet, isActive, onPress }: { pet: Pet; isActive: boolean; onPre {pet.breed?.name ?? pet.species.name} - {isActive && } + { + e.stopPropagation(); + onSetActive(); + }} + hitSlop={12} + style={styles.activeToggle} + > + {isActive ? ( + + ) : ( + + )} + ); @@ -71,4 +102,12 @@ const styles = StyleSheet.create({ nameRow: { flexDirection: "row", alignItems: "center" }, name: { ...typography.title, fontSize: 16 }, breed: { ...typography.body, color: colors.quietGray }, + activeToggle: { padding: 4 }, + activeToggleEmpty: { + width: 20, + height: 20, + borderRadius: 10, + borderWidth: 1, + borderColor: colors.hairlineGray, + }, }); diff --git a/mobile/app/(app)/_layout.tsx b/mobile/app/(app)/_layout.tsx index be4b37c..97ee661 100644 --- a/mobile/app/(app)/_layout.tsx +++ b/mobile/app/(app)/_layout.tsx @@ -1,19 +1,21 @@ import { Stack } from "expo-router"; import { colors, fontFamily } from "../../src/theme/tokens"; +const headerOptions = { + headerShown: true, + headerStyle: { backgroundColor: colors.pureWhite }, + headerTitleStyle: { fontFamily: fontFamily.bold }, +} as const; + export default function AppLayout() { return ( - + + + + + ); } diff --git a/mobile/app/(app)/pets/[petId]/edit.tsx b/mobile/app/(app)/pets/[petId]/edit.tsx new file mode 100644 index 0000000..aab5c5f --- /dev/null +++ b/mobile/app/(app)/pets/[petId]/edit.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from "react"; +import { View, Text, ScrollView, ActivityIndicator, StyleSheet } from "react-native"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useTRPC } from "../../../../src/api/trpc"; +import { TextField } from "../../../../src/components/TextField"; +import { Button } from "../../../../src/components/Button"; +import { colors, typography } from "../../../../src/theme/tokens"; + +/** + * Basic edit form (name, nickname, bio, adoption story) — a subset of web's + * src/app/(app)/pets/[petId]/edit/page.tsx. Breed/DM-policy/follower- + * visibility/birthday/avatar upload aren't editable here yet. + */ +export default function EditPetScreen() { + const { petId } = useLocalSearchParams<{ petId: string }>(); + const router = useRouter(); + const trpc = useTRPC(); + const queryClient = useQueryClient(); + + const { data: pet, isLoading } = useQuery(trpc.pets.byId.queryOptions({ petId })); + + const [name, setName] = useState(""); + const [nickname, setNickname] = useState(""); + const [bio, setBio] = useState(""); + const [adoptionStory, setAdoptionStory] = useState(""); + const [error, setError] = useState(null); + + useEffect(() => { + if (!pet) return; + setName(pet.name); + setNickname(pet.nickname ?? ""); + setBio(pet.bio ?? ""); + setAdoptionStory(pet.adoptionStory ?? ""); + }, [pet]); + + const updateMutation = useMutation( + trpc.pets.update.mutationOptions({ + onSuccess: async () => { + await queryClient.invalidateQueries(trpc.pets.pathFilter()); + router.back(); + }, + onError: (err) => setError(err.message), + }) + ); + + if (isLoading || !pet) { + return ( + + + + ); + } + + return ( + + Name + + + Rufname + + + Bio + + + Adoptionsgeschichte + + + {error ? {error} : null} + +