feat: tap map thumbnail to open full-screen interactive map
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { Camera, GeoJSONSource, Layer, Map } from '@maplibre/maplibre-react-native';
|
import { Camera, GeoJSONSource, Layer, Map } from '@maplibre/maplibre-react-native';
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
import { ActivityIndicator, Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||||
import Svg, { Defs, LinearGradient, Path, Stop } from 'react-native-svg';
|
import Svg, { Defs, LinearGradient, Path, Stop } from 'react-native-svg';
|
||||||
import { useActivity, useSetting } from '@/db/queries';
|
import { useActivity, useSetting } from '@/db/queries';
|
||||||
|
|
||||||
@@ -121,6 +121,8 @@ export default function ActivityScreen() {
|
|||||||
// ── Map ───────────────────────────────────────────────────────────────────────
|
// ── Map ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function RouteMap({ geojson, loading }: { geojson: object | null; loading: boolean }) {
|
function RouteMap({ geojson, loading }: { geojson: object | null; loading: boolean }) {
|
||||||
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.mapPlaceholder}>
|
<View style={styles.mapPlaceholder}>
|
||||||
@@ -131,35 +133,58 @@ function RouteMap({ geojson, loading }: { geojson: object | null; loading: boole
|
|||||||
if (!geojson) return null;
|
if (!geojson) return null;
|
||||||
|
|
||||||
const bounds = geoJsonBounds(geojson);
|
const bounds = geoJsonBounds(geojson);
|
||||||
|
const routeSource = (
|
||||||
|
<GeoJSONSource id="route" data={geojson as GeoJSON.FeatureCollection}>
|
||||||
|
<Layer
|
||||||
|
type="line"
|
||||||
|
id="route-line"
|
||||||
|
paint={{ 'line-color': '#60a5fa', 'line-width': 3 }}
|
||||||
|
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
||||||
|
/>
|
||||||
|
</GeoJSONSource>
|
||||||
|
);
|
||||||
|
const camera = bounds ? (
|
||||||
|
<Camera
|
||||||
|
initialViewState={{
|
||||||
|
bounds,
|
||||||
|
padding: { top: 24, bottom: 24, left: 24, right: 24 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.mapContainer}>
|
<>
|
||||||
<Map
|
{/* Thumbnail — tap to expand */}
|
||||||
style={styles.map}
|
<Pressable style={styles.mapContainer} onPress={() => setFullscreen(true)}>
|
||||||
mapStyle={MAP_STYLE}
|
<Map
|
||||||
dragPan={false}
|
style={styles.map}
|
||||||
touchZoom={false}
|
mapStyle={MAP_STYLE}
|
||||||
touchPitch={false}
|
dragPan={false}
|
||||||
touchRotate={false}
|
touchZoom={false}
|
||||||
>
|
touchPitch={false}
|
||||||
{bounds && (
|
touchRotate={false}
|
||||||
<Camera
|
>
|
||||||
initialViewState={{
|
{camera}
|
||||||
bounds,
|
{routeSource}
|
||||||
padding: { top: 24, bottom: 24, left: 24, right: 24 },
|
</Map>
|
||||||
}}
|
<View style={styles.mapExpandHint}>
|
||||||
/>
|
<Text style={styles.mapExpandText}>⤢ tap to explore</Text>
|
||||||
)}
|
</View>
|
||||||
<GeoJSONSource id="route" data={geojson as GeoJSON.FeatureCollection}>
|
</Pressable>
|
||||||
<Layer
|
|
||||||
type="line"
|
{/* Full-screen interactive map */}
|
||||||
id="route-line"
|
<Modal visible={fullscreen} animationType="slide" onRequestClose={() => setFullscreen(false)}>
|
||||||
paint={{ 'line-color': '#60a5fa', 'line-width': 3 }}
|
<View style={styles.fullscreenMap}>
|
||||||
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
<Map style={styles.map} mapStyle={MAP_STYLE}>
|
||||||
/>
|
{camera}
|
||||||
</GeoJSONSource>
|
{routeSource}
|
||||||
</Map>
|
</Map>
|
||||||
</View>
|
<Pressable style={styles.closeButton} onPress={() => setFullscreen(false)}>
|
||||||
|
<Text style={styles.closeText}>✕</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,9 +303,14 @@ const styles = StyleSheet.create({
|
|||||||
sport: { color: '#71717a', fontSize: 12, fontWeight: '600', letterSpacing: 0.8, paddingHorizontal: 16, marginBottom: 4 },
|
sport: { color: '#71717a', fontSize: 12, fontWeight: '600', letterSpacing: 0.8, paddingHorizontal: 16, marginBottom: 4 },
|
||||||
title: { color: '#f4f4f5', fontSize: 22, fontWeight: '700', paddingHorizontal: 16, marginBottom: 4 },
|
title: { color: '#f4f4f5', fontSize: 22, fontWeight: '700', paddingHorizontal: 16, marginBottom: 4 },
|
||||||
date: { color: '#71717a', fontSize: 13, paddingHorizontal: 16, marginBottom: 16 },
|
date: { color: '#71717a', fontSize: 13, paddingHorizontal: 16, marginBottom: 16 },
|
||||||
mapContainer: { height: 220, marginBottom: 16, borderTopWidth: 1, borderBottomWidth: 1, borderColor: '#27272a' },
|
mapContainer: { height: 220, marginBottom: 16, borderTopWidth: 1, borderBottomWidth: 1, borderColor: '#27272a' },
|
||||||
map: { flex: 1 },
|
map: { flex: 1 },
|
||||||
mapPlaceholder: { height: 220, backgroundColor: '#18181b', alignItems: 'center', justifyContent: 'center', borderTopWidth: 1, borderBottomWidth: 1, borderColor: '#27272a', marginBottom: 16 },
|
mapPlaceholder: { height: 220, backgroundColor: '#18181b', alignItems: 'center', justifyContent: 'center', borderTopWidth: 1, borderBottomWidth: 1, borderColor: '#27272a', marginBottom: 16 },
|
||||||
|
mapExpandHint: { position: 'absolute', bottom: 8, right: 8, backgroundColor: 'rgba(0,0,0,0.55)', borderRadius: 6, paddingHorizontal: 8, paddingVertical: 4 },
|
||||||
|
mapExpandText: { color: '#a1a1aa', fontSize: 11 },
|
||||||
|
fullscreenMap: { flex: 1, backgroundColor: '#09090b' },
|
||||||
|
closeButton: { position: 'absolute', top: 56, right: 16, backgroundColor: 'rgba(0,0,0,0.6)', borderRadius: 20, width: 36, height: 36, alignItems: 'center', justifyContent: 'center' },
|
||||||
|
closeText: { color: '#fff', fontSize: 16 },
|
||||||
chartContainer: { marginHorizontal: 16, marginBottom: 16, backgroundColor: '#18181b', borderRadius: 10, borderWidth: 1, borderColor: '#27272a', padding: 12, alignItems: 'flex-start' },
|
chartContainer: { marginHorizontal: 16, marginBottom: 16, backgroundColor: '#18181b', borderRadius: 10, borderWidth: 1, borderColor: '#27272a', padding: 12, alignItems: 'flex-start' },
|
||||||
chartPlaceholder: { height: 120, backgroundColor: '#18181b', alignItems: 'center', justifyContent: 'center', borderRadius: 10, borderWidth: 1, borderColor: '#27272a', marginHorizontal: 16, marginBottom: 16 },
|
chartPlaceholder: { height: 120, backgroundColor: '#18181b', alignItems: 'center', justifyContent: 'center', borderRadius: 10, borderWidth: 1, borderColor: '#27272a', marginHorizontal: 16, marginBottom: 16 },
|
||||||
chartLabel: { color: '#3f3f46', fontSize: 10, marginBottom: 2 },
|
chartLabel: { color: '#3f3f46', fontSize: 10, marginBottom: 2 },
|
||||||
|
|||||||
Reference in New Issue
Block a user