Skip to content

Commit 6bc106a

Browse files
committed
feat(mobile/mosques): hero photo + gallery on detail screen
Renders photos[0] as full-bleed 4:3 hero with back and save buttons overlaid via absolute-positioned safe-area view; photo strip below when photos.length > 1; thumbnail in bottom sheet.
1 parent 38f5407 commit 6bc106a

6 files changed

Lines changed: 111 additions & 35 deletions

File tree

apps/mobile/features/mosques/components/mosque-detail-content.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { MosqueDetailTabs, type MosqueTab } from "./mosque-detail-tabs";
99
import { MosqueEventsTab } from "./mosque-events-tab";
1010
import { MosqueMetaRow } from "./mosque-meta-row";
1111
import { MosqueOverviewTab } from "./mosque-overview-tab";
12+
import { MosquePhotoHero, MosquePhotoStrip } from "./mosque-photo-gallery";
1213
import { MosqueReviewsTab } from "./mosque-reviews-tab";
1314

1415
export function MosqueDetailContent({ data }: { data: MosqueDetail }) {
@@ -31,7 +32,9 @@ export function MosqueDetailContent({ data }: { data: MosqueDetail }) {
3132
/>
3233
}
3334
>
34-
<View className="px-s-6 pt-s-2">
35+
<MosquePhotoHero photos={mosque.photos} />
36+
37+
<View className="px-s-6 pt-s-5">
3538
<Text variant="display-lg">{mosque.name}</Text>
3639
{mosque.subtitle ? (
3740
<Text variant="body" tone="muted" className="mt-s-1">
@@ -48,6 +51,12 @@ export function MosqueDetailContent({ data }: { data: MosqueDetail }) {
4851
</View>
4952
</View>
5053

54+
{mosque.photos.length > 1 ? (
55+
<View className="mt-s-5">
56+
<MosquePhotoStrip photos={mosque.photos} />
57+
</View>
58+
) : null}
59+
5160
<View className="mt-s-5">
5261
<MosqueDetailTabs active={tab} onChange={setTab} />
5362
</View>

apps/mobile/features/mosques/components/mosque-detail-header.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { router } from "expo-router";
2+
import { View } from "react-native";
3+
import { SafeAreaView } from "react-native-safe-area-context";
4+
import { IconButton } from "@/components/ui/icon-button";
5+
import { MosqueSaveButton } from "./mosque-save-button";
6+
7+
type Props = {
8+
mosqueId?: string;
9+
isSaved?: boolean;
10+
};
11+
12+
export function MosqueDetailOverlay({ mosqueId, isSaved }: Props) {
13+
return (
14+
<View pointerEvents="box-none" className="absolute inset-x-0 top-0 z-10">
15+
<SafeAreaView edges={["top"]} pointerEvents="box-none">
16+
<View className="flex-row items-center justify-between px-s-5 py-s-3">
17+
<IconButton
18+
icon="back"
19+
onPress={() => router.back()}
20+
accessibilityLabel="Back"
21+
/>
22+
{mosqueId ? (
23+
<MosqueSaveButton mosqueId={mosqueId} isSaved={isSaved ?? false} />
24+
) : (
25+
<View className="h-10 w-10" />
26+
)}
27+
</View>
28+
</SafeAreaView>
29+
</View>
30+
);
31+
}

apps/mobile/features/mosques/components/mosque-detail-screen.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,18 @@ import { useLocalSearchParams } from "expo-router";
22
import { StatusBar } from "expo-status-bar";
33
import { useEffect } from "react";
44
import { View } from "react-native";
5-
import { useThemeScheme } from "@/features/theme/hooks/use-theme-scheme";
65
import { useMosque } from "../hooks/use-mosques";
76
import { usePushRecentMosque } from "../hooks/use-recent-mosques";
87
import { MosqueDetailContent } from "./mosque-detail-content";
98
import { MosqueDetailError } from "./mosque-detail-error";
10-
import { MosqueDetailHeader } from "./mosque-detail-header";
9+
import { MosqueDetailOverlay } from "./mosque-detail-overlay";
1110
import { MosqueDetailSkeleton } from "./mosque-detail-skeleton";
1211
import { MosqueDirectionsFooter } from "./mosque-directions-footer";
1312

1413
export function MosqueDetailScreen() {
1514
const { id } = useLocalSearchParams<{ id: string }>();
1615
const { data, isLoading, error, refetch } = useMosque(id);
1716
const pushRecent = usePushRecentMosque();
18-
const scheme = useThemeScheme();
1917

2018
// biome-ignore lint/correctness/useExhaustiveDependencies: pushRecent is a stable mutation; we only want to fire when the id changes
2119
useEffect(() => {
@@ -24,8 +22,7 @@ export function MosqueDetailScreen() {
2422

2523
return (
2624
<View className="flex-1 bg-cream">
27-
<StatusBar style={scheme === "dark" ? "light" : "dark"} />
28-
<MosqueDetailHeader mosqueId={data?.mosque.id} isSaved={data?.isSaved} />
25+
<StatusBar style="light" />
2926

3027
{isLoading ? (
3128
<MosqueDetailSkeleton />
@@ -37,6 +34,8 @@ export function MosqueDetailScreen() {
3734
<MosqueDirectionsFooter mosque={data.mosque} />
3835
</>
3936
)}
37+
38+
<MosqueDetailOverlay mosqueId={data?.mosque.id} isSaved={data?.isSaved} />
4039
</View>
4140
);
4241
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Image } from "expo-image";
2+
import { FlatList, View } from "react-native";
3+
import { MosqueMark } from "@/components/ui/mosque-mark";
4+
5+
type Props = {
6+
photos: string[];
7+
};
8+
9+
export function MosquePhotoHero({ photos }: Props) {
10+
const uri = photos[0];
11+
12+
if (!uri) {
13+
return (
14+
<View className="aspect-[4/3] items-center justify-center bg-green-tint">
15+
<MosqueMark size="xl" />
16+
</View>
17+
);
18+
}
19+
20+
return (
21+
<View className="aspect-[4/3] bg-line">
22+
<Image
23+
source={{ uri }}
24+
contentFit="cover"
25+
style={{ width: "100%", height: "100%" }}
26+
transition={200}
27+
/>
28+
</View>
29+
);
30+
}
31+
32+
export function MosquePhotoStrip({ photos }: Props) {
33+
const rest = photos.slice(1);
34+
if (rest.length === 0) return null;
35+
36+
return (
37+
<FlatList
38+
data={rest}
39+
keyExtractor={(uri) => uri}
40+
horizontal
41+
showsHorizontalScrollIndicator={false}
42+
contentContainerStyle={{ paddingHorizontal: 24, gap: 8 }}
43+
renderItem={({ item }) => (
44+
<Image
45+
source={{ uri: item }}
46+
contentFit="cover"
47+
style={{ width: 120, height: 88, borderRadius: 8 }}
48+
transition={200}
49+
/>
50+
)}
51+
/>
52+
);
53+
}

apps/mobile/features/mosques/components/mosque-sheet-content.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
2+
import { Image } from "expo-image";
23
import { useMemo } from "react";
34
import { View } from "react-native";
5+
import { MosqueMark } from "@/components/ui/mosque-mark";
46
import { Text } from "@/components/ui/text";
57
import { useSavedMosques } from "../hooks/use-mosques";
68
import type { MosqueListItem } from "../lib/types";
@@ -16,6 +18,7 @@ export function MosqueSheetContent({ mosque }: { mosque: MosqueListItem }) {
1618
() => Boolean(saved.data?.data.some((m) => m.id === mosque.id)),
1719
[saved.data, mosque.id],
1820
);
21+
const thumb = mosque.photos?.[0];
1922

2023
return (
2124
<BottomSheetScrollView
@@ -24,6 +27,16 @@ export function MosqueSheetContent({ mosque }: { mosque: MosqueListItem }) {
2427
>
2528
<View className="px-s-6 pt-s-2">
2629
<View className="flex-row items-start gap-s-3">
30+
{thumb ? (
31+
<Image
32+
source={{ uri: thumb }}
33+
contentFit="cover"
34+
style={{ width: 56, height: 56, borderRadius: 8 }}
35+
transition={200}
36+
/>
37+
) : (
38+
<MosqueMark size="md" />
39+
)}
2740
<View className="flex-1">
2841
<Text variant="display-md">{mosque.name}</Text>
2942
{mosque.subtitle ? (

0 commit comments

Comments
 (0)