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} + +