first version of the cruise lovers app, build 1.0.1, ready to be published on the app store

This commit is contained in:
2026-07-22 09:09:26 +01:00
parent 104566ded3
commit 4f5c5cc6d2
23 changed files with 1225 additions and 858 deletions

View File

@@ -0,0 +1,76 @@
import { LoadingSpinner } from '@/assets/components/LoadingSpinner';
import { colors } from '@/assets/styles/colors';
import styles from '@/styles/screens/tabs/contactos.styles';
import { FontAwesome } from '@expo/vector-icons';
import Constants, { ExecutionEnvironment } from 'expo-constants';
import { lazy, Suspense, useMemo, useState } from 'react';
import { Image, Platform, Pressable, Text, View } from 'react-native';
const MAP_ZOOM = 15;
const isExpoGo = Constants.executionEnvironment === ExecutionEnvironment.StoreClient;
const useNativeMaps = !isExpoGo && (Platform.OS === 'ios' || Platform.OS === 'android');
const NativeMapPreview = useNativeMaps
? lazy(() => import('./ContactMapPreviewNative'))
: null;
const buildStaticMapUrl = (lat: number, lng: number): string => {
const delta = 0.012;
const bbox = [lng - delta, lat - delta, lng + delta, lat + delta].join(',');
return `https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/export?bbox=${bbox}&bboxSR=4326&size=600,300&format=png&f=image`;
};
type Props = {
lat: number;
lng: number;
title?: string;
onPress: () => void;
};
function StaticMapFallback({ lat, lng, onPress }: Props) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const mapUrl = useMemo(() => buildStaticMapUrl(lat, lng), [lat, lng]);
return (
<Pressable onPress={onPress} style={styles.mapImageWrap}>
<Image
source={{ uri: mapUrl }}
style={styles.mapImage}
resizeMode="cover"
onLoadStart={() => {
setLoading(true);
setError(false);
}}
onLoad={() => setLoading(false)}
onError={() => {
setLoading(false);
setError(true);
}}
/>
{loading && !error && (
<View style={styles.mapOverlay}>
<LoadingSpinner size="small" />
</View>
)}
{error && (
<View style={styles.mapOverlay}>
<FontAwesome name="map-marker" size={28} color={colors.vermelho} />
<Text style={styles.mapErrorText}>Mapa indisponível</Text>
</View>
)}
</Pressable>
);
}
export function ContactMapPreview(props: Props) {
if (NativeMapPreview) {
return (
<Suspense fallback={<StaticMapFallback {...props} />}>
<NativeMapPreview {...props} />
</Suspense>
);
}
return <StaticMapFallback {...props} />;
}

View File

@@ -0,0 +1,81 @@
import { colors } from '@/assets/styles/colors';
import styles from '@/styles/screens/tabs/contactos.styles';
import { AppleMaps, GoogleMaps } from 'expo-maps';
import { useMemo } from 'react';
import { Platform, Pressable, View } from 'react-native';
const MAP_ZOOM = 15;
const disabledGoogleUi = {
scrollGesturesEnabled: false,
zoomGesturesEnabled: false,
rotationGesturesEnabled: false,
tiltGesturesEnabled: false,
zoomControlsEnabled: false,
mapToolbarEnabled: false,
compassEnabled: false,
scaleBarEnabled: false,
};
const disabledAppleUi = {
compassEnabled: false,
scaleBarEnabled: false,
myLocationButtonEnabled: false,
togglePitchEnabled: false,
};
type Props = {
lat: number;
lng: number;
title?: string;
onPress: () => void;
};
export default function ContactMapPreviewNative({ lat, lng, title, onPress }: Props) {
const cameraPosition = useMemo(
() => ({
coordinates: { latitude: lat, longitude: lng },
zoom: MAP_ZOOM,
}),
[lat, lng]
);
const marker = useMemo(
() => ({
id: 'agency',
coordinates: { latitude: lat, longitude: lng },
title,
}),
[lat, lng, title]
);
return (
<Pressable onPress={onPress} style={styles.mapImageWrap}>
<View style={styles.mapImage} pointerEvents="none">
{Platform.OS === 'ios' ? (
<AppleMaps.View
style={styles.mapImage}
cameraPosition={cameraPosition}
markers={[
{
...marker,
systemImage: 'mappin',
tintColor: colors.vermelho,
},
]}
properties={{ selectionEnabled: false }}
uiSettings={disabledAppleUi}
/>
) : (
<GoogleMaps.View
style={styles.mapImage}
cameraPosition={cameraPosition}
markers={[marker]}
properties={{ selectionEnabled: false }}
uiSettings={disabledGoogleUi}
/>
)}
</View>
</Pressable>
);
}

View File

@@ -1,8 +1,8 @@
import { useAuth } from '@/assets/contexts/useAuth';
import { getCachedContacts } from '@/assets/services/offlineStorage';
import PhoneIcon from '@/assets/icons/phone-solid.svg';
import { colors } from '@/assets/styles/colors';
import { fonts } from '@/assets/styles/fonts';
import { FontAwesome } from '@expo/vector-icons';
import { useEffect, useState } from 'react';
import { Alert, Linking, Platform, StyleSheet, Text, useWindowDimensions } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
@@ -27,7 +27,7 @@ export function EmergencyButton() {
const tabBarHeight = (Platform.OS === 'ios' ? 54 : 64) + insets.bottom;
const maxX = screenWidth - BTN_SIZE - MARGIN;
const minY = insets.top + MARGIN;
const maxY = screenHeight - tabBarHeight - BTN_SIZE - MARGIN;
const maxY = screenHeight - tabBarHeight - BTN_SIZE + MARGIN;
const x = useSharedValue(screenWidth - BTN_SIZE - MARGIN);
const y = useSharedValue(screenHeight - tabBarHeight - BTN_SIZE - MARGIN);
@@ -49,7 +49,7 @@ export function EmergencyButton() {
if (!emergencyPhone) return;
const url = `tel:${emergencyPhone.replace(/\s/g, '')}`;
Alert.alert(
'Linha de Emergência 24h',
'Ajuda ao viajante 24h',
`Ligar para ${emergencyPhone}?`,
[
{ text: 'Cancelar', style: 'cancel' },
@@ -116,8 +116,8 @@ export function EmergencyButton() {
return (
<GestureDetector gesture={composed}>
<Animated.View style={[styles.btn, animatedStyle]}>
<FontAwesome name="phone" size={18} color={colors.branco} />
<Text style={styles.label}>SOS</Text>
<PhoneIcon width={18} height={18} fill={colors.branco} />
<Text style={styles.label}>Ajuda</Text>
</Animated.View>
</GestureDetector>
);