6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
594 lines
20 KiB
TypeScript
594 lines
20 KiB
TypeScript
/**
|
|
* MissionDetailScreen — uppdragsinfo, karta, acceptera-knapp, kamera-upload vid leverans
|
|
* Live markör, distance overlay, heading indicator
|
|
*/
|
|
import { useEffect, useState, useRef, useCallback } from 'react'
|
|
import {
|
|
View, Text, ScrollView, TouchableOpacity,
|
|
StyleSheet, ActivityIndicator, Alert,
|
|
Image, Platform,
|
|
} from 'react-native'
|
|
import MapView, { Marker, Region, PROVIDER_DEFAULT } from 'react-native-maps'
|
|
import * as ImagePicker from 'expo-image-picker'
|
|
import { useNavigation, useRoute, type RouteProp } from '@react-navigation/native'
|
|
import { missions, type MissionDetail } from '../lib/api'
|
|
import { useLocationStore } from '../src/stores/locationStore'
|
|
import { UserLocationMarker } from '../src/components/gps/UserLocationMarker'
|
|
import { FollowUserButton } from '../src/components/gps/FollowUserButton'
|
|
|
|
type RouteParams = { MissionDetail: { missionId: string } }
|
|
|
|
const CATEGORY_EMOJI: Record<string, string> = {
|
|
infrastruktur: '🏗️',
|
|
vägar: '🛤️',
|
|
hamnar: '⚓',
|
|
bryggor: '🚢',
|
|
fritidshus: '🏠',
|
|
miljö: '🌿',
|
|
övrigt: '📍',
|
|
}
|
|
|
|
/** Calculate distance between two coordinates in meters */
|
|
function haversineDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
|
const R = 6371e3 // Earth radius in meters
|
|
const φ1 = (lat1 * Math.PI) / 180
|
|
const φ2 = (lat2 * Math.PI) / 180
|
|
const Δφ = ((lat2 - lat1) * Math.PI) / 180
|
|
const Δλ = ((lng2 - lng1) * Math.PI) / 180
|
|
|
|
const a =
|
|
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
|
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2)
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
|
|
|
return R * c
|
|
}
|
|
|
|
function formatDistance(meters: number): string {
|
|
if (meters < 1000) {
|
|
return `${Math.round(meters)} m`
|
|
}
|
|
return `${(meters / 1000).toFixed(1)} km`
|
|
}
|
|
|
|
/** Calculate bearing from point 1 to point 2 in degrees */
|
|
function calculateBearing(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
|
const φ1 = (lat1 * Math.PI) / 180
|
|
const φ2 = (lat2 * Math.PI) / 180
|
|
const Δλ = ((lng2 - lng1) * Math.PI) / 180
|
|
|
|
const y = Math.sin(Δλ) * Math.cos(φ2)
|
|
const x =
|
|
Math.cos(φ1) * Math.sin(φ2) -
|
|
Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ)
|
|
|
|
const θ = Math.atan2(y, x)
|
|
return ((θ * 180) / Math.PI + 360) % 360
|
|
}
|
|
|
|
function getDirectionArrow(degrees: number): string {
|
|
if (degrees >= 337.5 || degrees < 22.5) return '↑'
|
|
if (degrees >= 22.5 && degrees < 67.5) return '↗'
|
|
if (degrees >= 67.5 && degrees < 112.5) return '→'
|
|
if (degrees >= 112.5 && degrees < 157.5) return '↘'
|
|
if (degrees >= 157.5 && degrees < 202.5) return '↓'
|
|
if (degrees >= 202.5 && degrees < 247.5) return '↙'
|
|
if (degrees >= 247.5 && degrees < 292.5) return '←'
|
|
return '↖'
|
|
}
|
|
|
|
export function MissionDetailScreen() {
|
|
const navigation = useNavigation<any>()
|
|
const route = useRoute<RouteProp<RouteParams, 'MissionDetail'>>()
|
|
const { missionId } = route.params
|
|
const mapRef = useRef<MapView>(null)
|
|
|
|
// Live location from store
|
|
const {
|
|
currentPosition,
|
|
heading,
|
|
followUser,
|
|
setUserPannedMap,
|
|
} = useLocationStore()
|
|
|
|
const [mission, setMission] = useState<MissionDetail | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [accepting, setAccepting] = useState(false)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [accepted, setAccepted] = useState(false)
|
|
const [mediaUris, setMediaUris] = useState<string[]>([])
|
|
const [distance, setDistance] = useState<number | null>(null)
|
|
const [bearing, setBearing] = useState<number | null>(null)
|
|
|
|
useEffect(() => {
|
|
missions.get(missionId)
|
|
.then(m => { setMission(m); setLoading(false) })
|
|
.catch(() => { Alert.alert('Fel', 'Kunde inte hämta uppdraget.'); navigation.goBack() })
|
|
}, [missionId])
|
|
|
|
// Update distance and bearing when position or mission changes
|
|
useEffect(() => {
|
|
if (currentPosition && mission) {
|
|
const dist = haversineDistance(
|
|
currentPosition.coords.latitude,
|
|
currentPosition.coords.longitude,
|
|
mission.latitude,
|
|
mission.longitude
|
|
)
|
|
setDistance(dist)
|
|
|
|
const bear = calculateBearing(
|
|
currentPosition.coords.latitude,
|
|
currentPosition.coords.longitude,
|
|
mission.latitude,
|
|
mission.longitude
|
|
)
|
|
setBearing(bear)
|
|
}
|
|
}, [currentPosition, mission])
|
|
|
|
// Auto-follow user when followUser is true
|
|
useEffect(() => {
|
|
if (followUser && currentPosition && mapRef.current) {
|
|
const region: Region = {
|
|
latitude: currentPosition.coords.latitude,
|
|
longitude: currentPosition.coords.longitude,
|
|
latitudeDelta: 0.05,
|
|
longitudeDelta: 0.05,
|
|
}
|
|
mapRef.current.animateToRegion(region, 300)
|
|
}
|
|
}, [currentPosition, followUser])
|
|
|
|
const onPanDrag = useCallback(() => {
|
|
setUserPannedMap(true)
|
|
}, [setUserPannedMap])
|
|
|
|
const handleAccept = async () => {
|
|
if (!mission) return
|
|
setAccepting(true)
|
|
try {
|
|
await missions.accept(mission.id)
|
|
setAccepted(true)
|
|
Alert.alert('✅ Uppdraget accepterat!', 'Ta dig till platsen och leverera när du är framme.')
|
|
} catch (e: any) {
|
|
Alert.alert('Fel', e?.message ?? 'Kunde inte acceptera uppdraget.')
|
|
} finally {
|
|
setAccepting(false)
|
|
}
|
|
}
|
|
|
|
const handlePickMedia = async () => {
|
|
const { status } = await ImagePicker.requestCameraPermissionsAsync()
|
|
if (status !== 'granted') {
|
|
Alert.alert('Kamera-åtkomst krävs', 'Tillåt kameraåtkomst i telefonens inställningar.')
|
|
return
|
|
}
|
|
|
|
Alert.alert('Välj källa', '', [
|
|
{
|
|
text: 'Ta foto',
|
|
onPress: async () => {
|
|
const result = await ImagePicker.launchCameraAsync({
|
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
quality: 0.85,
|
|
allowsEditing: false,
|
|
})
|
|
if (!result.canceled && result.assets[0]) {
|
|
setMediaUris(prev => [...prev, result.assets[0].uri])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
text: 'Välj från bibliotek',
|
|
onPress: async () => {
|
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
quality: 0.85,
|
|
allowsMultipleSelection: true,
|
|
selectionLimit: 10,
|
|
})
|
|
if (!result.canceled) {
|
|
setMediaUris(prev => [...prev, ...result.assets.map(a => a.uri)])
|
|
}
|
|
},
|
|
},
|
|
{ text: 'Avbryt', style: 'cancel' },
|
|
])
|
|
}
|
|
|
|
const handleSubmit = async () => {
|
|
if (!mission) return
|
|
if (mediaUris.length === 0) {
|
|
Alert.alert('Inga bilder', 'Lägg till minst en bild innan du skickar in.')
|
|
return
|
|
}
|
|
|
|
Alert.alert(
|
|
'Skicka in uppdraget?',
|
|
`${mediaUris.length} bild${mediaUris.length > 1 ? 'er' : ''} kommer att skickas.`,
|
|
[
|
|
{ text: 'Avbryt', style: 'cancel' },
|
|
{
|
|
text: 'Skicka in',
|
|
onPress: async () => {
|
|
setSubmitting(true)
|
|
try {
|
|
await missions.submit(mission.id, mediaUris)
|
|
Alert.alert(
|
|
'🎉 Inlämnat!',
|
|
`Dina bilder granskas. Om allt godkänns får du ${(mission.reward_amount / 100).toFixed(0)} kr.`,
|
|
[{ text: 'Tillbaka', onPress: () => navigation.goBack() }],
|
|
)
|
|
} catch (e: any) {
|
|
Alert.alert('Fel', e?.message ?? 'Kunde inte skicka in uppdraget.')
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
},
|
|
},
|
|
],
|
|
)
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<ActivityIndicator color="#6366f1" size="large" />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
if (!mission) return null
|
|
|
|
const isExpired = mission.deadline_at ? new Date(mission.deadline_at) < new Date() : false
|
|
const spotsLeft = mission.max_submissions - mission.submission_count
|
|
|
|
// Calculate relative bearing for navigation arrow
|
|
const relativeBearing = bearing != null && heading != null
|
|
? ((bearing - heading + 360) % 360)
|
|
: null
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 120 }}>
|
|
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
|
<Text style={styles.backBtnText}>← Tillbaka</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Map with live tracking */}
|
|
<View style={styles.mapContainer}>
|
|
<MapView
|
|
ref={mapRef}
|
|
style={styles.map}
|
|
provider={PROVIDER_DEFAULT}
|
|
region={{
|
|
latitude: mission.latitude,
|
|
longitude: mission.longitude,
|
|
latitudeDelta: 0.05,
|
|
longitudeDelta: 0.05,
|
|
}}
|
|
onPanDrag={onPanDrag}
|
|
showsUserLocation={false} // Custom marker
|
|
rotateEnabled={false}
|
|
>
|
|
{/* Mission marker */}
|
|
<Marker
|
|
coordinate={{ latitude: mission.latitude, longitude: mission.longitude }}
|
|
pinColor="#10b981"
|
|
title={mission.title}
|
|
/>
|
|
|
|
{/* Live user location marker */}
|
|
{currentPosition && (
|
|
<Marker
|
|
coordinate={{
|
|
latitude: currentPosition.coords.latitude,
|
|
longitude: currentPosition.coords.longitude,
|
|
}}
|
|
anchor={{ x: 0.5, y: 0.5 }}
|
|
flat={true}
|
|
>
|
|
<UserLocationMarker />
|
|
</Marker>
|
|
)}
|
|
</MapView>
|
|
|
|
{/* Follow user button */}
|
|
<View style={styles.followBtnContainer}>
|
|
<FollowUserButton />
|
|
</View>
|
|
|
|
{/* Distance overlay */}
|
|
{distance != null && (
|
|
<View style={styles.distanceOverlay}>
|
|
<View style={styles.distanceRow}>
|
|
<Text style={styles.distanceLabel}>Avstånd till uppdrag</Text>
|
|
<Text style={styles.distanceValue}>{formatDistance(distance)}</Text>
|
|
</View>
|
|
{relativeBearing != null && (
|
|
<View style={styles.distanceRow}>
|
|
<Text style={styles.distanceLabel}>Riktning</Text>
|
|
<Text style={styles.distanceValue}>
|
|
{getDirectionArrow(relativeBearing)} {relativeBearing.toFixed(0)}°
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{heading != null && (
|
|
<View style={styles.distanceRow}>
|
|
<Text style={styles.distanceLabel}>Din kurs</Text>
|
|
<Text style={styles.distanceValue}>{heading.toFixed(0)}°</Text>
|
|
</View>
|
|
)}
|
|
{currentPosition?.coords.speed != null && (
|
|
<View style={styles.distanceRow}>
|
|
<Text style={styles.distanceLabel}>Hastighet</Text>
|
|
<Text style={styles.distanceValue}>
|
|
{(currentPosition.coords.speed * 3.6).toFixed(1)} km/h
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Content */}
|
|
<View style={styles.content}>
|
|
{/* Category + status row */}
|
|
<View style={styles.metaRow}>
|
|
<View style={styles.categoryBadge}>
|
|
<Text style={styles.categoryEmoji}>{CATEGORY_EMOJI[mission.category] ?? '📍'}</Text>
|
|
<Text style={styles.categoryText}>{mission.category}</Text>
|
|
</View>
|
|
{isExpired && <View style={styles.expiredBadge}><Text style={styles.expiredText}>Utgången</Text></View>}
|
|
{spotsLeft <= 3 && !isExpired && (
|
|
<View style={styles.urgentBadge}>
|
|
<Text style={styles.urgentText}>Bara {spotsLeft} platser kvar</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Title */}
|
|
<Text style={styles.title}>{mission.title}</Text>
|
|
|
|
{/* Reward + deadline */}
|
|
<View style={styles.rewardRow}>
|
|
<View style={styles.rewardBox}>
|
|
<Text style={styles.rewardLabel}>Ersättning</Text>
|
|
<Text style={styles.rewardValue}>{(mission.reward_amount / 100).toFixed(0)} SEK</Text>
|
|
</View>
|
|
{mission.deadline_at && (
|
|
<View style={styles.rewardBox}>
|
|
<Text style={styles.rewardLabel}>Deadline</Text>
|
|
<Text style={styles.rewardValue}>
|
|
{new Date(mission.deadline_at).toLocaleDateString('sv-SE', {
|
|
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
|
})}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
<View style={styles.rewardBox}>
|
|
<Text style={styles.rewardLabel}>Plats</Text>
|
|
<Text style={styles.rewardValue}>{mission.city || 'Skärgården'}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Description */}
|
|
<Text style={styles.sectionTitle}>Beskrivning</Text>
|
|
<Text style={styles.desc}>{mission.description}</Text>
|
|
|
|
{/* Instructions */}
|
|
{mission.instructions && (
|
|
<>
|
|
<Text style={styles.sectionTitle}>Instruktioner</Text>
|
|
<Text style={styles.desc}>{mission.instructions}</Text>
|
|
</>
|
|
)}
|
|
|
|
{/* Requirements */}
|
|
{mission.requirements?.length > 0 && (
|
|
<>
|
|
<Text style={styles.sectionTitle}>Krav</Text>
|
|
{mission.requirements.map((r, i) => (
|
|
<View key={i} style={styles.reqRow}>
|
|
<Text style={styles.reqBullet}>✓</Text>
|
|
<Text style={styles.reqText}>{r}</Text>
|
|
</View>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{/* Media picker (only when accepted) */}
|
|
{accepted && (
|
|
<>
|
|
<Text style={styles.sectionTitle}>Dina bilder</Text>
|
|
<View style={styles.mediaGrid}>
|
|
{mediaUris.map((uri, i) => (
|
|
<View key={i} style={styles.mediaThumbWrap}>
|
|
<Image source={{ uri }} style={styles.mediaThumb} />
|
|
<TouchableOpacity
|
|
style={styles.removeBtn}
|
|
onPress={() => setMediaUris(prev => prev.filter((_, j) => j !== i))}
|
|
>
|
|
<Text style={styles.removeBtnText}>✕</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
))}
|
|
<TouchableOpacity style={styles.addMediaBtn} onPress={handlePickMedia}>
|
|
<Text style={styles.addMediaIcon}>📸</Text>
|
|
<Text style={styles.addMediaText}>Lägg till bild</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</>
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* Bottom action bar */}
|
|
{!isExpired && (
|
|
<View style={styles.bottomBar}>
|
|
{!accepted ? (
|
|
<TouchableOpacity
|
|
style={[styles.actionBtn, accepting && styles.actionBtnDisabled]}
|
|
onPress={handleAccept}
|
|
disabled={accepting}
|
|
>
|
|
{accepting
|
|
? <ActivityIndicator color="#fff" />
|
|
: <Text style={styles.actionBtnText}>🎯 Acceptera uppdraget</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
) : (
|
|
<TouchableOpacity
|
|
style={[styles.actionBtn, styles.submitBtn, (submitting || mediaUris.length === 0) && styles.actionBtnDisabled]}
|
|
onPress={handleSubmit}
|
|
disabled={submitting || mediaUris.length === 0}
|
|
>
|
|
{submitting
|
|
? <ActivityIndicator color="#fff" />
|
|
: <Text style={styles.actionBtnText}>
|
|
📤 Skicka in ({mediaUris.length} bild{mediaUris.length !== 1 ? 'er' : ''})
|
|
</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
)}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#0A0A1B' },
|
|
center: { flex: 1, backgroundColor: '#0A0A1B', alignItems: 'center', justifyContent: 'center' },
|
|
|
|
header: {
|
|
paddingTop: Platform.OS === 'ios' ? 56 : 16,
|
|
paddingHorizontal: 16,
|
|
paddingBottom: 8,
|
|
},
|
|
backBtn: { paddingVertical: 4 },
|
|
backBtnText: { color: '#6366f1', fontSize: 15, fontWeight: '600' },
|
|
|
|
mapContainer: {
|
|
height: 280,
|
|
position: 'relative',
|
|
},
|
|
map: {
|
|
flex: 1,
|
|
},
|
|
|
|
followBtnContainer: {
|
|
position: 'absolute',
|
|
bottom: 16,
|
|
right: 16,
|
|
},
|
|
|
|
distanceOverlay: {
|
|
position: 'absolute',
|
|
top: 16,
|
|
left: 16,
|
|
backgroundColor: 'rgba(10,10,27,0.85)',
|
|
borderRadius: 12,
|
|
padding: 12,
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255,255,255,0.1)',
|
|
minWidth: 160,
|
|
},
|
|
distanceRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: 4,
|
|
},
|
|
distanceLabel: {
|
|
color: 'rgba(255,255,255,0.45)',
|
|
fontSize: 11,
|
|
marginRight: 12,
|
|
},
|
|
distanceValue: {
|
|
color: '#fff',
|
|
fontSize: 13,
|
|
fontWeight: '700',
|
|
},
|
|
|
|
content: { padding: 20 },
|
|
|
|
metaRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
|
|
categoryBadge: {
|
|
flexDirection: 'row', alignItems: 'center', gap: 6,
|
|
backgroundColor: 'rgba(99,102,241,0.15)',
|
|
borderRadius: 20, paddingHorizontal: 12, paddingVertical: 6,
|
|
},
|
|
categoryEmoji: { fontSize: 14 },
|
|
categoryText: { color: '#a5b4fc', fontSize: 12, fontWeight: '600', textTransform: 'capitalize' },
|
|
expiredBadge: {
|
|
backgroundColor: 'rgba(239,68,68,0.15)',
|
|
borderRadius: 20, paddingHorizontal: 12, paddingVertical: 6,
|
|
},
|
|
expiredText: { color: '#ef4444', fontSize: 12, fontWeight: '600' },
|
|
urgentBadge: {
|
|
backgroundColor: 'rgba(245,158,11,0.15)',
|
|
borderRadius: 20, paddingHorizontal: 12, paddingVertical: 6,
|
|
},
|
|
urgentText: { color: '#f59e0b', fontSize: 12, fontWeight: '600' },
|
|
|
|
title: { color: '#fff', fontSize: 24, fontWeight: '800', lineHeight: 30, marginBottom: 20, letterSpacing: -0.5 },
|
|
|
|
rewardRow: { flexDirection: 'row', gap: 12, marginBottom: 24 },
|
|
rewardBox: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
borderRadius: 14, padding: 14,
|
|
borderWidth: 1, borderColor: 'rgba(255,255,255,0.07)',
|
|
},
|
|
rewardLabel: { color: 'rgba(255,255,255,0.4)', fontSize: 11, marginBottom: 4 },
|
|
rewardValue: { color: '#fff', fontSize: 15, fontWeight: '700' },
|
|
|
|
sectionTitle: { color: 'rgba(255,255,255,0.5)', fontSize: 12, fontWeight: '700', letterSpacing: 1, textTransform: 'uppercase', marginBottom: 8, marginTop: 4 },
|
|
desc: { color: 'rgba(255,255,255,0.7)', fontSize: 15, lineHeight: 24, marginBottom: 20 },
|
|
|
|
reqRow: { flexDirection: 'row', gap: 10, marginBottom: 8 },
|
|
reqBullet: { color: '#10b981', fontWeight: '700', fontSize: 14 },
|
|
reqText: { color: 'rgba(255,255,255,0.65)', fontSize: 14, lineHeight: 20, flex: 1 },
|
|
|
|
mediaGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginBottom: 8 },
|
|
mediaThumbWrap: { position: 'relative' },
|
|
mediaThumb: { width: 90, height: 90, borderRadius: 12 },
|
|
removeBtn: {
|
|
position: 'absolute', top: -6, right: -6,
|
|
width: 22, height: 22, borderRadius: 11,
|
|
backgroundColor: '#ef4444', alignItems: 'center', justifyContent: 'center',
|
|
},
|
|
removeBtnText: { color: '#fff', fontSize: 10, fontWeight: '800' },
|
|
addMediaBtn: {
|
|
width: 90, height: 90, borderRadius: 12,
|
|
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
borderWidth: 2, borderColor: 'rgba(255,255,255,0.12)',
|
|
borderStyle: 'dashed',
|
|
alignItems: 'center', justifyContent: 'center', gap: 4,
|
|
},
|
|
addMediaIcon: { fontSize: 24 },
|
|
addMediaText: { color: 'rgba(255,255,255,0.4)', fontSize: 11 },
|
|
|
|
bottomBar: {
|
|
position: 'absolute', bottom: 0, left: 0, right: 0,
|
|
padding: 16,
|
|
paddingBottom: Platform.OS === 'ios' ? 32 : 16,
|
|
backgroundColor: 'rgba(10,10,27,0.95)',
|
|
borderTopWidth: 1, borderTopColor: 'rgba(255,255,255,0.08)',
|
|
},
|
|
actionBtn: {
|
|
backgroundColor: '#6366f1',
|
|
borderRadius: 16, paddingVertical: 16,
|
|
alignItems: 'center', justifyContent: 'center',
|
|
},
|
|
submitBtn: { backgroundColor: '#10b981' },
|
|
actionBtnDisabled: { opacity: 0.5 },
|
|
actionBtnText: { color: '#fff', fontWeight: '700', fontSize: 16 },
|
|
})
|