diff --git a/mobile/app/(app)/(tabs)/explore.tsx b/mobile/app/(app)/(tabs)/explore.tsx index d3019ae..f32bf21 100644 --- a/mobile/app/(app)/(tabs)/explore.tsx +++ b/mobile/app/(app)/(tabs)/explore.tsx @@ -1,13 +1,14 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { View, Text, FlatList, Pressable, ScrollView, ActivityIndicator, StyleSheet } from "react-native"; import { useRouter } from "expo-router"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useTRPC } from "../../../src/api/trpc"; +import { useTheme } from "../../../src/context/ThemeContext"; import { PetAvatar } from "../../../src/components/PetAvatar"; import { Card } from "../../../src/components/Card"; import { OptionPicker, type PickerOption } from "../../../src/components/OptionPicker"; import { formatPetName } from "../../../src/lib/format"; -import { colors, radii, typography } from "../../../src/theme/tokens"; +import { radii, getTypography, type ColorTokens, type Typography } from "../../../src/theme/tokens"; import type { ExplorePet } from "../../../src/api/types"; /** @@ -19,6 +20,8 @@ import type { ExplorePet } from "../../../src/api/types"; */ export default function ExploreScreen() { const trpc = useTRPC(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); const [speciesId, setSpeciesId] = useState(null); const [speciesName, setSpeciesName] = useState(null); const [breedId, setBreedId] = useState(null); @@ -57,6 +60,11 @@ export default function ExploreScreen() { numColumns={2} columnWrapperStyle={styles.row} contentContainerStyle={styles.list} + // FlatList wraps ListHeaderComponent in its own internal container, + // a sibling of the row containers — zIndex on `filters` itself only + // ranks it among ITS OWN children, so the breed dropdown's absolute + // overlay still painted underneath the grid without this. + ListHeaderComponentStyle={styles.headerWrapper} renderItem={({ item }) => } onEndReachedThreshold={0.5} onEndReached={() => { @@ -90,7 +98,7 @@ export default function ExploreScreen() { ListEmptyComponent={ !isLoading ? ( - + {breedName ? `Noch keine ${breedName}.` : speciesName ? `Noch keine ${speciesName}.` : "Keine Tiere gefunden."} @@ -101,6 +109,8 @@ export default function ExploreScreen() { } function SpeciesTab({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) { + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); return ( {label} @@ -118,6 +128,8 @@ function BreedFilter({ onSelect: (b: PickerOption | null) => void; }) { const trpc = useTRPC(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); const [open, setOpen] = useState(false); const { data: breeds = [], isLoading } = useQuery(trpc.pets.listBreeds.queryOptions({ speciesId, locale: "de" })); @@ -144,6 +156,8 @@ function BreedFilter({ function ExplorePetCard({ pet }: { pet: ExplorePet }) { const router = useRouter(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); return ( router.push(`/pets/${pet.id}`)}> @@ -157,21 +171,28 @@ function ExplorePetCard({ pet }: { pet: ExplorePet }) { ); } -const styles = StyleSheet.create({ - list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, - row: { gap: 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 }, - followers: { ...typography.label, textTransform: "none", letterSpacing: 0 }, - footer: { marginVertical: 16 }, - filters: { gap: 10, marginBottom: 12 }, - speciesTabs: { gap: 8 }, - tab: { paddingHorizontal: 14, height: 36, borderRadius: radii.lg, justifyContent: "center", backgroundColor: colors.whisperGray }, - tabActive: { backgroundColor: colors.marigoldOrange }, - tabText: { ...typography.label, textTransform: "none", letterSpacing: 0, color: colors.ink }, - tabTextActive: { color: colors.pureWhite }, - breedFilter: { width: 220 }, -}); +function makeStyles(colors: ColorTokens) { + const typography: Typography = getTypography(colors); + return StyleSheet.create({ + list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, + row: { gap: 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 }, + followers: { ...typography.label, textTransform: "none", letterSpacing: 0 }, + footer: { marginVertical: 16 }, + // zIndex above the grid rows below it, so the breed dropdown (position: + // absolute inside OptionPicker) overlays the grid instead of being + // painted behind it — rows have no explicit zIndex (default 0). + headerWrapper: { zIndex: 10 }, + filters: { gap: 10, marginBottom: 12 }, + speciesTabs: { gap: 8 }, + tab: { paddingHorizontal: 14, height: 36, borderRadius: radii.lg, justifyContent: "center", backgroundColor: colors.whisperGray }, + tabActive: { backgroundColor: colors.marigoldOrange }, + tabText: { ...typography.label, textTransform: "none", letterSpacing: 0, color: colors.ink }, + tabTextActive: { color: colors.pureWhite }, + breedFilter: { width: 220 }, + }); +} diff --git a/mobile/app/(app)/(tabs)/notifications.tsx b/mobile/app/(app)/(tabs)/notifications.tsx index fd45f40..066abf1 100644 --- a/mobile/app/(app)/(tabs)/notifications.tsx +++ b/mobile/app/(app)/(tabs)/notifications.tsx @@ -1,12 +1,13 @@ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { View, Text, FlatList, Pressable, ActivityIndicator, StyleSheet } from "react-native"; import { useFocusEffect } from "expo-router"; import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTRPC } from "../../../src/api/trpc"; import { useActivePet } from "../../../src/context/ActivePetContext"; +import { useTheme } from "../../../src/context/ThemeContext"; import { PetAvatar } from "../../../src/components/PetAvatar"; import { formatPetName, formatRelativeTime } from "../../../src/lib/format"; -import { colors, typography } from "../../../src/theme/tokens"; +import { getTypography, type ColorTokens, type Typography } from "../../../src/theme/tokens"; import type { NotificationItem } from "../../../src/api/types"; const ACTOR_MESSAGES: Partial string>> = { @@ -37,6 +38,8 @@ export default function NotificationsScreen() { const trpc = useTRPC(); const queryClient = useQueryClient(); const { activePetId } = useActivePet(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage, error } = useInfiniteQuery({ ...trpc.notifications.list.infiniteQueryOptions( @@ -113,37 +116,41 @@ export default function NotificationsScreen() { )} - {getNotificationText(item)} + {getNotificationText(item)} {formatRelativeTime(item.createdAt)} )} ListEmptyComponent={ - Keine Benachrichtigungen. + Keine Benachrichtigungen. } /> ); } -const styles = StyleSheet.create({ - list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, - separator: { height: 8 }, - center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, - errorText: { ...typography.body, color: colors.alertRed }, - row: { flexDirection: "row", gap: 12, padding: 12, borderRadius: 12 }, - rowUnread: { backgroundColor: colors.marigoldTint }, - rowText: { flex: 1, gap: 2 }, - timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0 }, - footer: { marginVertical: 16 }, - selfIcon: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: colors.marigoldTint, - alignItems: "center", - justifyContent: "center", - }, - selfIconText: { fontSize: 18 }, -}); +function makeStyles(colors: ColorTokens) { + const typography: Typography = getTypography(colors); + return StyleSheet.create({ + list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, + separator: { height: 8 }, + center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, + errorText: { ...typography.body, color: colors.alertRed }, + bodyText: { ...typography.body }, + row: { flexDirection: "row", gap: 12, padding: 12, borderRadius: 12 }, + rowUnread: { backgroundColor: colors.marigoldTint }, + rowText: { flex: 1, gap: 2 }, + timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0 }, + footer: { marginVertical: 16 }, + selfIcon: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: colors.marigoldTint, + alignItems: "center", + justifyContent: "center", + }, + selfIconText: { fontSize: 18 }, + }); +} diff --git a/mobile/found.md b/mobile/found.md new file mode 100644 index 0000000..a62a5c8 --- /dev/null +++ b/mobile/found.md @@ -0,0 +1,11 @@ +TODO: +- [x] Fehler: Explore ohne Filterfunktion +- [x] Benachrichtigung - Button klein Alles Gelesen markieren. +- [x] Head-Bereich verdeckt Handy-Infos nicht lesbarer Bereich Uhrzeit, WIFI Battery - Bitte GoogleRichtlinen beachten +- [x] Dark-Mode ? wo ? + +TODO Neu: +- [x] Explore Filter ist jetzt vorhanden, der Rassen-Dropdown, schiebt die Vorhanden elemente nach unten statt darüber zu lappen. +- [x] Explore Seite ist nur Halb im Darkmode +- [x] Benachrichtiungsseite nicht wirklich im Darkmode +- diff --git a/mobile/src/components/OptionPicker.tsx b/mobile/src/components/OptionPicker.tsx index cfe198e..9f9d17b 100644 --- a/mobile/src/components/OptionPicker.tsx +++ b/mobile/src/components/OptionPicker.tsx @@ -1,6 +1,8 @@ +import { useMemo } from "react"; import { View, Text, Pressable, ScrollView, ActivityIndicator, StyleSheet } from "react-native"; import { ChevronDown } from "lucide-react-native"; -import { colors, radii, typography } from "../theme/tokens"; +import { useTheme } from "../context/ThemeContext"; +import { radii, getTypography, type ColorTokens, type Typography } from "../theme/tokens"; export type PickerOption = { id: string; name: string }; @@ -16,15 +18,26 @@ type Props = { onSelect: (option: PickerOption) => void; }; +const BUTTON_HEIGHT = 48; +const LIST_GAP = 4; + /** - * A dropdown-style button that expands into a scrollable option list below - * it — extracted from onboarding/PetCreateForm once explore.tsx's breed - * filter needed the exact same thing. Deliberately a plain ScrollView + map, - * not a FlatList: this is meant to sit inside a screen-level ScrollView, and + * A dropdown-style button that expands into a scrollable option list — + * extracted from onboarding/PetCreateForm once explore.tsx's breed filter + * needed the exact same thing. Deliberately a plain ScrollView + map, not a + * FlatList: this is meant to sit inside a screen-level ScrollView, and * nesting a VirtualizedList inside a ScrollView with the same orientation * breaks touch/scroll handling (RN's "VirtualizedLists should never be * nested..." warning) — these option lists are at most a few dozen items, * so virtualization buys nothing here anyway. + * + * The option list is position:absolute so it overlays whatever comes after + * it instead of pushing it down (found.md: the breed dropdown on Explore was + * shoving the pet grid down the screen). zIndex is applied to the whole + * component root — not just the list — because the sibling content it needs + * to overlay renders after it in the same parent's paint order, and RN only + * lets zIndex reorder stacking among siblings, not push a child above its + * own subtree's siblings from the inside. */ export function OptionPicker({ open, @@ -37,8 +50,11 @@ export function OptionPicker({ emptyLabel, onSelect, }: Props) { + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); + return ( - + {selectedName ?? placeholder} @@ -53,7 +69,7 @@ export function OptionPicker({ {options.map((option) => ( onSelect(option)}> - {option.name} + {option.name} ))} @@ -64,32 +80,45 @@ export function OptionPicker({ ); } -const styles = StyleSheet.create({ - button: { - height: 48, - borderRadius: radii.lg, - borderWidth: 1, - borderColor: colors.hairlineGray, - paddingHorizontal: 14, - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - backgroundColor: colors.pureWhite, - }, - buttonDisabled: { opacity: 0.5 }, - value: { ...typography.body }, - placeholder: { ...typography.body, color: colors.quietGray }, - list: { - borderWidth: 1, - borderColor: colors.hairlineGray, - borderRadius: radii.lg, - marginTop: 4, - maxHeight: 220, - overflow: "hidden", - backgroundColor: colors.pureWhite, - }, - scrollView: { maxHeight: 220 }, - row: { paddingHorizontal: 14, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.hairlineGray }, - loading: { padding: 16 }, - empty: { ...typography.body, color: colors.quietGray, padding: 16 }, -}); +function makeStyles(colors: ColorTokens) { + const typography: Typography = getTypography(colors); + return StyleSheet.create({ + root: { zIndex: 0 }, + rootOpen: { zIndex: 30 }, + button: { + height: BUTTON_HEIGHT, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: colors.hairlineGray, + paddingHorizontal: 14, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + backgroundColor: colors.pureWhite, + }, + buttonDisabled: { opacity: 0.5 }, + value: { ...typography.body }, + placeholder: { ...typography.body, color: colors.quietGray }, + list: { + position: "absolute", + top: BUTTON_HEIGHT + LIST_GAP, + left: 0, + right: 0, + borderWidth: 1, + borderColor: colors.hairlineGray, + borderRadius: radii.lg, + maxHeight: 220, + overflow: "hidden", + backgroundColor: colors.pureWhite, + elevation: 8, + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + }, + scrollView: { maxHeight: 220 }, + row: { paddingHorizontal: 14, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.hairlineGray }, + loading: { padding: 16 }, + empty: { ...typography.body, color: colors.quietGray, padding: 16 }, + }); +}