fix(mobile): dark mode on Explore/Notifications, breed dropdown overlay

- Explore and Notifications tabs now read colors from ThemeContext
  instead of the static light-only tokens
- OptionPicker's option list is position:absolute so it overlays
  following content instead of pushing it down, with zIndex on the
  component root (and FlatList's ListHeaderComponentStyle on Explore)
  so it actually paints above sibling content instead of underneath it
- check off found.md's resolved items, record the three newly reported ones
This commit is contained in:
admin committed 2026-09-06 16:30:02 +02:00
1 parent 0408be2063
commit 0c6421c8de
4 files changed
+149 -81

No files matched your search

+42 -21
View File
@@ -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<string | null>(null);
const [speciesName, setSpeciesName] = useState<string | null>(null);
const [breedId, setBreedId] = useState<string | null>(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 }) => <ExplorePetCard pet={item} />}
onEndReachedThreshold={0.5}
onEndReached={() => {
@@ -90,7 +98,7 @@ export default function ExploreScreen() {
ListEmptyComponent={
!isLoading ? (
<View style={styles.center}>
<Text style={typography.body}>
<Text style={getTypography(colors).body}>
{breedName ? `Noch keine ${breedName}.` : speciesName ? `Noch keine ${speciesName}.` : "Keine Tiere gefunden."}
</Text>
</View>
@@ -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 (
<Pressable style={[styles.tab, active && styles.tabActive]} onPress={onPress}>
<Text style={[styles.tabText, active && styles.tabTextActive]}>{label}</Text>
@@ -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 (
<Pressable style={styles.cardPress} onPress={() => router.push(`/pets/${pet.id}`)}>
<Card style={styles.card}>
@@ -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 },
});
}
+31 -24
View File
@@ -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<Record<NotificationItem["type"], (actor: string) => 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() {
</View>
)}
<View style={styles.rowText}>
<Text style={typography.body}>{getNotificationText(item)}</Text>
<Text style={styles.bodyText}>{getNotificationText(item)}</Text>
<Text style={styles.timestamp}>{formatRelativeTime(item.createdAt)}</Text>
</View>
</Pressable>
)}
ListEmptyComponent={
<View style={styles.center}>
<Text style={typography.body}>Keine Benachrichtigungen.</Text>
<Text style={styles.bodyText}>Keine Benachrichtigungen.</Text>
</View>
}
/>
);
}
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 },
});
}
+11
View File
@@ -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
-
+65 -36
View File
@@ -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 (
<View>
<View style={open ? styles.rootOpen : styles.root}>
<Pressable style={[styles.button, disabled && styles.buttonDisabled]} onPress={onToggle} disabled={disabled}>
<Text style={selectedName ? styles.value : styles.placeholder}>{selectedName ?? placeholder}</Text>
<ChevronDown color={colors.quietGray} size={18} />
@@ -53,7 +69,7 @@ export function OptionPicker({
<ScrollView style={styles.scrollView} nestedScrollEnabled>
{options.map((option) => (
<Pressable key={option.id} style={styles.row} onPress={() => onSelect(option)}>
<Text style={typography.body}>{option.name}</Text>
<Text style={getTypography(colors).body}>{option.name}</Text>
</Pressable>
))}
</ScrollView>
@@ -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 },
});
}