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>
);

View File

@@ -6,8 +6,8 @@
*/
// Endpoint base da API
export const API_BASE_URL = "https://apmtests.webclientes.com/pt/app";
export const API_BASE_URL_DEV = "https://apmtests.webclientes.com/pt/app";
export const API_BASE_URL = "https://cruiselovers.webclientes.com/pt/app";
export const API_BASE_URL_DEV = "https://cruiselovers.webclientes.com/pt/app";
/**
* Endpoints da aplicação
@@ -47,7 +47,6 @@ export const API_ENDPOINTS = {
*
* @example
* buildApiUrl(API_ENDPOINTS.USER_LOGIN)
* // Retorna: "https://finalguru.webclientes.com/pt/app/UserLogin"
*/
export function buildApiUrl(endpoint: string): string {
// Remove barras duplicadas e garante que há apenas uma barra entre base e endpoint

BIN
assets/icons/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,4 @@
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15.4507" cy="15.4507" r="15.4507" fill="#123572"/>
<path d="M14.1065 8.21875V10.75H17.4815V8.21875C17.4815 8.07812 17.3409 7.9375 17.2003 7.9375H14.3878C14.212 7.9375 14.1065 8.07812 14.1065 8.21875ZM20.294 10.75C21.5245 10.75 22.544 11.7695 22.544 13V20.875C22.544 22.1406 21.5245 23.125 20.294 23.125C20.294 23.7578 19.7667 24.25 19.169 24.25C18.5362 24.25 18.044 23.7578 18.044 23.125H13.544C13.544 23.7578 13.0167 24.25 12.419 24.25C11.7862 24.25 11.294 23.7578 11.294 23.125C10.0284 23.125 9.04401 22.1406 9.04401 20.875V13C9.04401 11.7695 10.0284 10.75 11.294 10.75H12.419V8.21875C12.419 7.16406 13.2979 6.25 14.3878 6.25H17.2003C18.2549 6.25 19.169 7.16406 19.169 8.21875V10.75H20.294ZM12.9815 14.125C12.6651 14.125 12.419 14.4062 12.419 14.6875C12.419 15.0039 12.6651 15.25 12.9815 15.25H18.6065C18.8878 15.25 19.169 15.0039 19.169 14.6875C19.169 14.4062 18.8878 14.125 18.6065 14.125H12.9815ZM12.9815 18.625C12.6651 18.625 12.419 18.9062 12.419 19.1875C12.419 19.5039 12.6651 19.75 12.9815 19.75H18.6065C18.8878 19.75 19.169 19.5039 19.169 19.1875C19.169 18.9062 18.8878 18.625 18.6065 18.625H12.9815Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,4 @@
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15.4507" cy="15.4507" r="15.4507" fill="white"/>
<path d="M10.544 10.9375C10.544 10.2266 11.1182 9.625 11.8565 9.625H19.7315C20.4424 9.625 21.044 10.2266 21.044 10.9375V19.25H10.544V10.9375ZM19.1026 12.2227C18.8292 11.9766 18.419 11.9766 18.1729 12.2227L15.1378 15.2578L13.8526 13.9727C13.5792 13.7266 13.169 13.7266 12.9229 13.9727C12.6495 14.2461 12.6495 14.6562 12.9229 14.9023L14.6729 16.6523C14.919 16.9258 15.3292 16.9258 15.6026 16.6523L19.1026 13.1523C19.3487 12.9062 19.3487 12.4961 19.1026 12.25V12.2227ZM7.91901 17.9375C7.91901 17.2266 8.49323 16.625 9.23151 16.625H9.66901V20.125H21.919V16.625H22.3565C23.0674 16.625 23.669 17.2266 23.669 17.9375V20.5625C23.669 21.3008 23.0674 21.875 22.3565 21.875H9.23151C8.49323 21.875 7.91901 21.3008 7.91901 20.5625V17.9375Z" fill="#123572"/>
</svg>

After

Width:  |  Height:  |  Size: 909 B

View File

@@ -0,0 +1,4 @@
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15.4507" cy="15.4507" r="15.4507" fill="white"/>
<path d="M13.169 8.75C13.6339 8.75 14.044 9.16016 14.044 9.625V10.5H17.544V9.625C17.544 9.16016 17.9268 8.75 18.419 8.75C18.8839 8.75 19.294 9.16016 19.294 9.625V10.5H20.6065C21.3174 10.5 21.919 11.1016 21.919 11.8125V13.125H9.66901V11.8125C9.66901 11.1016 10.2432 10.5 10.9815 10.5H12.294V9.625C12.294 9.16016 12.6768 8.75 13.169 8.75ZM9.66901 14H21.919V21.4375C21.919 22.1758 21.3174 22.75 20.6065 22.75H10.9815C10.2432 22.75 9.66901 22.1758 9.66901 21.4375V14ZM11.419 16.1875V17.0625C11.419 17.3086 11.6104 17.5 11.8565 17.5H12.7315C12.9503 17.5 13.169 17.3086 13.169 17.0625V16.1875C13.169 15.9688 12.9503 15.75 12.7315 15.75H11.8565C11.6104 15.75 11.419 15.9688 11.419 16.1875ZM14.919 16.1875V17.0625C14.919 17.3086 15.1104 17.5 15.3565 17.5H16.2315C16.4503 17.5 16.669 17.3086 16.669 17.0625V16.1875C16.669 15.9688 16.4503 15.75 16.2315 15.75H15.3565C15.1104 15.75 14.919 15.9688 14.919 16.1875ZM18.8565 15.75C18.6104 15.75 18.419 15.9688 18.419 16.1875V17.0625C18.419 17.3086 18.6104 17.5 18.8565 17.5H19.7315C19.9503 17.5 20.169 17.3086 20.169 17.0625V16.1875C20.169 15.9688 19.9503 15.75 19.7315 15.75H18.8565ZM11.419 19.6875V20.5625C11.419 20.8086 11.6104 21 11.8565 21H12.7315C12.9503 21 13.169 20.8086 13.169 20.5625V19.6875C13.169 19.4688 12.9503 19.25 12.7315 19.25H11.8565C11.6104 19.25 11.419 19.4688 11.419 19.6875ZM15.3565 19.25C15.1104 19.25 14.919 19.4688 14.919 19.6875V20.5625C14.919 20.8086 15.1104 21 15.3565 21H16.2315C16.4503 21 16.669 20.8086 16.669 20.5625V19.6875C16.669 19.4688 16.4503 19.25 16.2315 19.25H15.3565ZM18.419 19.6875V20.5625C18.419 20.8086 18.6104 21 18.8565 21H19.7315C19.9503 21 20.169 20.8086 20.169 20.5625V19.6875C20.169 19.4688 19.9503 19.25 19.7315 19.25H18.8565C18.6104 19.25 18.419 19.4688 18.419 19.6875Z" fill="#123572"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M164.9 24.6c-7.7-18.6-28-28.5-47.4-23.2l-88 24C12.1 30.2 0 46 0 64C0 311.4 200.6 512 448 512c18 0 33.8-12.1 38.6-29.5l24-88c5.3-19.4-4.6-39.7-23.2-47.4l-96-40c-16.3-6.8-35.2-2.1-46.3 11.6L304.7 368C234.3 334.7 177.3 277.7 144 207.3L193.3 167c13.7-11.2 18.4-30 11.6-46.3l-40-96z"/></svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M54.2 202.9C123.2 136.7 216.8 96 320 96s196.8 40.7 265.8 106.9c12.8 12.2 33 11.8 45.2-.9s11.8-33-.9-45.2C549.7 79.5 440.4 32 320 32S90.3 79.5 9.8 156.7C-2.9 169-3.3 189.2 8.9 202s32.5 13.2 45.2 .9zM320 256c56.8 0 108.6 21.1 148.2 56c13.3 11.7 33.5 10.4 45.2-2.8s10.4-33.5-2.8-45.2C459.8 219.2 393 192 320 192s-139.8 27.2-190.5 72c-13.3 11.7-14.5 31.9-2.8 45.2s31.9 14.5 45.2 2.8c39.5-34.9 91.3-56 148.2-56zm64 160a64 64 0 1 0 -128 0 64 64 0 1 0 128 0z"/></svg>

After

Width:  |  Height:  |  Size: 682 B