diff --git a/apps/mobile/src/components/BikeSection.tsx b/apps/mobile/src/components/BikeSection.tsx index 185ebcf..7ca8fc8 100644 --- a/apps/mobile/src/components/BikeSection.tsx +++ b/apps/mobile/src/components/BikeSection.tsx @@ -1,4 +1,6 @@ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faClock, faRuler, faMap } from '@fortawesome/free-solid-svg-icons'; import { formatDuration, formatDistance } from '@timetoleave/core'; import type { BikeRoute, Station } from '@timetoleave/core'; import type { AppColors } from '../hooks/useColors'; @@ -29,15 +31,24 @@ export function BikeSection({ bikeRoute, loading, origin, colors }: Props) { ) : bikeRoute ? ( - โฑ Dauer + + + Dauer + {formatDuration(bikeRoute.duration)} - ๐Ÿ“ Distanz + + + Distanz + {formatDistance(bikeRoute.distance)} - ๐Ÿ—บ Map (post-MVP) + + + Map (post-MVP) + ) : ( @@ -56,9 +67,11 @@ const styles = StyleSheet.create({ hint: { fontSize: 15, marginTop: 12 }, empty: { fontSize: 14 }, card: { borderRadius: 10, padding: 14, borderWidth: 1 }, - row: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 6 }, + row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 6 }, + labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, label: { fontSize: 15, fontWeight: '500' }, value: { fontSize: 15, fontWeight: '600' }, + mapTextRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, mapPlaceholder: { marginTop: 10, height: 100, diff --git a/apps/mobile/src/components/JourneyList.tsx b/apps/mobile/src/components/JourneyList.tsx index 0814f44..e107d77 100644 --- a/apps/mobile/src/components/JourneyList.tsx +++ b/apps/mobile/src/components/JourneyList.tsx @@ -123,13 +123,13 @@ export function JourneyList({ {showWalkingOption && walkRoute && ( - ๐Ÿšถ Final walk + Final walk - โฑ Duration + Duration {formatDuration(walkRoute.duration)} - ๐Ÿ“ Distance + Distance {formatDistance(walkRoute.distance)} diff --git a/apps/mobile/src/components/NearbyStops.tsx b/apps/mobile/src/components/NearbyStops.tsx index 85e6db0..7e0ebef 100644 --- a/apps/mobile/src/components/NearbyStops.tsx +++ b/apps/mobile/src/components/NearbyStops.tsx @@ -1,4 +1,6 @@ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faBusSimple } from '@fortawesome/free-solid-svg-icons'; import type { WienerLinienStop } from '@timetoleave/core'; import type { DepartureRow } from '../hooks/useWienerLinien'; import type { AppColors } from '../hooks/useColors'; @@ -22,7 +24,10 @@ export function NearbyStops({ stops, departures, loading, error, colors }: Props return ( - ๐Ÿš Public transit near destination + + + Public transit near destination + {loading ? ( @@ -63,7 +68,8 @@ export function NearbyStops({ stops, departures, loading, error, colors }: Props const styles = StyleSheet.create({ container: { padding: 20 }, - sectionTitle: { fontSize: 18, fontWeight: '600', marginBottom: 12 }, + sectionTitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 12 }, + sectionTitle: { fontSize: 18, fontWeight: '600' }, centered: { alignItems: 'center', gap: 8 }, hint: { fontSize: 14 }, depCard: { borderRadius: 10, padding: 10, marginBottom: 6, borderWidth: 1 }, diff --git a/apps/mobile/src/screens/AddEventScreen.tsx b/apps/mobile/src/screens/AddEventScreen.tsx index 6372e99..88c3d07 100644 --- a/apps/mobile/src/screens/AddEventScreen.tsx +++ b/apps/mobile/src/screens/AddEventScreen.tsx @@ -6,6 +6,8 @@ import { TouchableOpacity, View, } from 'react-native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; import { loadEvents, addEvent, updateEvent } from '../store/eventStore'; @@ -154,7 +156,7 @@ export function AddEventScreen({ navigation, route }: ScreenProps) { - โœ“ + Success! @@ -210,7 +212,6 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, - checkmark: { color: '#fff', fontSize: 24, fontWeight: 'bold' }, successTitle: { fontSize: 18, fontWeight: '600' }, successSubtitle: { fontSize: 14 }, }); diff --git a/apps/mobile/src/screens/CalendarImportScreen.tsx b/apps/mobile/src/screens/CalendarImportScreen.tsx index 5b51ec3..9ae95c5 100644 --- a/apps/mobile/src/screens/CalendarImportScreen.tsx +++ b/apps/mobile/src/screens/CalendarImportScreen.tsx @@ -8,10 +8,12 @@ import { TouchableOpacity, View, } from 'react-native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faArrowLeft, faCalendarDays, faCheck } from '@fortawesome/free-solid-svg-icons'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; import { api } from '../services/api'; -import { fetchNativeEvents, getSelectableCalendars, groupCalendarsByType } from '../services/calendar'; +import { accountTypeIcon, fetchNativeEvents, getSelectableCalendars, groupCalendarsByType } from '../services/calendar'; import { addEvent, getSelectedCalendarIds, hasCalendarSelection, loadEvents, saveSelectedCalendarIds } from '../store/eventStore'; import type { Event as CalendarEvent, CalendarAccountType, SelectableCalendar } from '@timetoleave/core'; import type { RootStack } from '../types/navigation'; @@ -74,12 +76,13 @@ function CalendarCheckbox({ accessibilityLabel={`${calendar.name} โ€“ ${calendar.sourceInfo.label}`} > - {selected && โœ“} + {selected && } - - {calendar.sourceInfo.emoji} {calendar.name} - + + + {calendar.name} + {calendar.sourceInfo.badge} @@ -313,9 +316,12 @@ export function CalendarImportScreen({ navigation }: ScreenProps) { return ( - - {group[0].sourceInfo.emoji} {group[0].sourceInfo.label} ({group.length}) - + + + + {group[0].sourceInfo.label} ({group.length}) + + {group.map((cal) => ( - ๐Ÿ“… Calendar Sync + + + Calendar Sync + @@ -364,9 +373,10 @@ export function CalendarImportScreen({ navigation }: ScreenProps) { {count !== null && ( - - โœ“ {count} event(s) successfully imported! - + + + {count} event(s) successfully imported! + )} @@ -374,7 +384,10 @@ export function CalendarImportScreen({ navigation }: ScreenProps) { style={styles.backBtn} onPress={() => navigation.goBack()} > - โ† Back + + + Back + @@ -413,12 +426,15 @@ const styles = StyleSheet.create({ borderRadius: 12, alignItems: 'center', }, + buttonContent: { flexDirection: 'row', alignItems: 'center', gap: 8 }, importBtnDisabled: { opacity: 0.6 }, importBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' }, + bannerContent: { flexDirection: 'row', alignItems: 'center', gap: 8 }, backBtn: { paddingVertical: 10, alignItems: 'center', }, + backBtnContent: { flexDirection: 'row', alignItems: 'center', gap: 6 }, backBtnText: { fontSize: 15 }, // Calendar selection selectionActions: { @@ -436,13 +452,18 @@ const styles = StyleSheet.create({ group: { marginBottom: 12, }, + groupLabelRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + marginBottom: 6, + marginTop: 4, + }, groupLabel: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 0.5, - marginBottom: 6, - marginTop: 4, }, calendarRow: { flexDirection: 'row', @@ -467,18 +488,19 @@ const styles = StyleSheet.create({ backgroundColor: '#8B5CF6', borderColor: '#8B5CF6', }, - checkMark: { - color: '#fff', - fontSize: 14, - fontWeight: '700', - }, calendarInfo: { flex: 1, }, + calendarNameRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginBottom: 2, + }, calendarName: { fontSize: 15, fontWeight: '500', - marginBottom: 2, + flex: 1, }, badgeRow: { flexDirection: 'row', diff --git a/apps/mobile/src/screens/EventDetailScreen.tsx b/apps/mobile/src/screens/EventDetailScreen.tsx index 60ce587..9ac3e18 100644 --- a/apps/mobile/src/screens/EventDetailScreen.tsx +++ b/apps/mobile/src/screens/EventDetailScreen.tsx @@ -7,6 +7,8 @@ import { TouchableOpacity, View, } from 'react-native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faArrowsRotate, faBicycle, faTrain, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; import { loadEvents, loadOriginStation, loadNotificationSettings } from '../store/eventStore'; @@ -193,7 +195,10 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) { {error && ( - โš  {error} + + + {error} + )} @@ -218,7 +223,10 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) { onPress={() => setActiveMode('train')} > - ๐Ÿš† Train + + + Train + {effectiveMode === 'train' && ( Active @@ -240,7 +248,10 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) { disabled={bikeDisabled} > - ๐Ÿšฒ Bike + + + Bike + {effectiveMode === 'bike' && ( Active @@ -293,7 +304,10 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) { style={[styles.refreshBtn, { backgroundColor: colors.border }]} onPress={handleRefresh} > - ๐Ÿ”„ Refresh + + + Refresh + @@ -307,6 +321,7 @@ const styles = StyleSheet.create({ loadingText: { marginTop: 12, fontSize: 15 }, errorBanner: { padding: 12, marginBottom: 12 }, warningBanner: { padding: 12, marginBottom: 12 }, + bannerContent: { flexDirection: 'row', alignItems: 'center', gap: 8 }, bannerText: { color: '#fff', fontSize: 14 }, bannerLink: { color: '#fff', fontWeight: '700', textDecorationLine: 'underline' }, modeSelector: { @@ -319,10 +334,12 @@ const styles = StyleSheet.create({ }, modeButton: { flex: 1, padding: 12, borderRadius: 10, borderWidth: 1, borderColor: 'transparent' }, modeHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, + modeLabelRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, modeLabel: { fontSize: 15, fontWeight: '600' }, modeMeta: { fontSize: 11, marginTop: 4 }, activeBadge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10 }, activeBadgeText: { color: '#fff', fontSize: 10, fontWeight: '700' }, refreshBtn: { alignSelf: 'center', marginTop: 20, paddingVertical: 12, paddingHorizontal: 24, borderRadius: 12 }, + refreshBtnContent: { flexDirection: 'row', alignItems: 'center', gap: 8 }, refreshBtnText: { fontSize: 15, fontWeight: '600' }, }); diff --git a/apps/mobile/src/screens/EventListScreen.tsx b/apps/mobile/src/screens/EventListScreen.tsx index a8a8d59..f8ea88b 100644 --- a/apps/mobile/src/screens/EventListScreen.tsx +++ b/apps/mobile/src/screens/EventListScreen.tsx @@ -7,10 +7,12 @@ import { TouchableOpacity, View, } from 'react-native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faPenToSquare } from '@fortawesome/free-solid-svg-icons'; import { useFocusEffect } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; -import { loadEvents, loadNotificationSettings, loadOriginStation, removeEvent } from '../store/eventStore'; +import { loadEvents, loadNotificationSettings, loadOriginStation } from '../store/eventStore'; import { calculateCountdown, formatTime } from '@timetoleave/core'; import type { Event as CalendarEvent, Journey, Station } from '@timetoleave/core'; import type { RootStack } from '../types/navigation'; @@ -216,38 +218,21 @@ export function EventListScreen({ navigation }: ScreenProps) { {selectedJourney ? ( Departure {formatTime(selectedJourney.rD)} - {selectedJourney.platform ? ` ยท Platform ${selectedJourney.platform}` : ''} - {' ยท '} - Arrival {formatTime(selectedJourney.rA)} - {' ยท '} - {selectedJourney.changes === 0 ? 'Direct' : `${selectedJourney.changes} transfers`} ) : ( {journeysError ?? (origin ? 'Best connection for the next event' : 'Set origin station')} )} - {originWalk.walkRoute && originWalk.walkRoute.duration > 30 && ( - - Incl. walk to station: {Math.ceil(originWalk.walkRoute.duration / 60)} min - - )} + navigation.navigate('AddEvent', { editEventId: item.id })} + style={styles.editIconBtn} + > + + - - {/* Edit button */} - navigation.navigate('AddEvent', { editEventId: item.id })} - style={styles.editBtn} - > - edit - - - {/* Delete button */} - removeEvent(item.id, reload)} style={styles.deleteBtn}> - delete - ); }; @@ -312,10 +297,7 @@ const styles = StyleSheet.create({ trainTitle: { fontSize: 16, fontWeight: '700' }, trainMeta: { fontSize: 13, marginTop: 6, lineHeight: 18 }, status: { fontSize: 13, marginTop: 2, fontWeight: '500' }, - editBtn: { alignSelf: 'flex-start', marginTop: 4 }, - editText: { fontSize: 13, fontWeight: '500' }, - deleteBtn: { alignSelf: 'flex-start', marginTop: 2, marginBottom: 4 }, - deleteText: { fontSize: 13 }, + editIconBtn: { alignSelf: 'flex-end', padding: 4 }, center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, empty: { fontSize: 20, marginBottom: 16 }, addBtn: { backgroundColor: '#8B5CF6', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 12 }, diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index c2dd7e8..e6ecb4b 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -10,6 +10,8 @@ import { TouchableOpacity, View, } from 'react-native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faCheck, faChevronDown, faChevronUp, faLocationDot, faXmark } from '@fortawesome/free-solid-svg-icons'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; import * as Location from 'expo-location'; @@ -267,11 +269,27 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) { ))} - ๐Ÿ“ Use current location + + + Use current location + - - Location: {locPermission === 'granted' ? 'Granted โœ“' : locPermission === 'denied' ? 'Denied โœ—' : 'Not yet requested'} - + + Location: + {locPermission === 'granted' ? ( + <> + Granted + + + ) : locPermission === 'denied' ? ( + <> + Denied + + + ) : ( + Not yet requested + )} + {/* Notification Settings */} @@ -299,9 +317,12 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) { - - {showAdvanced ? 'โ†‘ Show fewer options' : 'โ†“ Show more options'} - + + + + {showAdvanced ? 'Show fewer options' : 'Show more options'} + + {showAdvanced && ( @@ -368,7 +389,9 @@ const styles = StyleSheet.create({ alignItems: 'center', }, locBtnText: { fontSize: 15, fontWeight: '500' }, - locStatus: { fontSize: 12, marginTop: 6 }, + buttonContent: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + locStatusRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 6 }, + locStatus: { fontSize: 12 }, errorText: { fontSize: 13, marginTop: 6 }, advancedToggle: { marginTop: 12, diff --git a/apps/mobile/src/services/calendar.ts b/apps/mobile/src/services/calendar.ts index fda858a..e75087b 100644 --- a/apps/mobile/src/services/calendar.ts +++ b/apps/mobile/src/services/calendar.ts @@ -1,4 +1,8 @@ import * as Calendar from 'expo-calendar'; +import { + faLink, faMobile, faCalendar, faBuilding, faRss, faHardDrive, faUser, faArrowsRotate, faCalendarDays, +} from '@fortawesome/free-solid-svg-icons'; +import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'; import type { Event as CoreEvent, CalendarAccountType, CalendarSourceInfo, SelectableCalendar } from '@timetoleave/core'; /** @@ -17,54 +21,35 @@ import type { Event as CoreEvent, CalendarAccountType, CalendarSourceInfo, Selec /** Maps expo-calendar account types to human-readable info. */ const SOURCE_INFO: Record = { - caldav: { - label: 'CalDAV / DAVx', - badge: 'CalDAV', - emoji: '๐Ÿ”—', - }, - mobileme: { - label: 'Apple Calendar', - badge: 'Apple', - emoji: '๐ŸŽ', - }, - google: { - label: 'Google Calendar', - badge: 'Google', - emoji: '๐Ÿ“…', - }, - exchange: { - label: 'Microsoft Exchange', - badge: 'Exchange', - emoji: '๐Ÿข', - }, - subscriptions: { - label: 'Subscribed', - badge: 'Sub', - emoji: '๐Ÿ“ก', - }, - local: { - label: 'On My Device', - badge: 'Local', - emoji: '๐Ÿ“ฑ', - }, - carddav: { - label: 'CardDAV', - badge: 'CardDAV', - emoji: '๐Ÿ‘ค', - }, - activesync: { - label: 'ActiveSync', - badge: 'Sync', - emoji: '๐Ÿ”„', - }, + caldav: { label: 'CalDAV / DAVx', badge: 'CalDAV' }, + mobileme: { label: 'Apple Calendar', badge: 'Apple' }, + google: { label: 'Google Calendar', badge: 'Google' }, + exchange: { label: 'Microsoft Exchange', badge: 'Exchange' }, + subscriptions: { label: 'Subscribed', badge: 'Sub' }, + local: { label: 'On My Device', badge: 'Local' }, + carddav: { label: 'CardDAV', badge: 'CardDAV' }, + activesync: { label: 'ActiveSync', badge: 'Sync' }, }; -const DEFAULT_SOURCE_INFO: CalendarSourceInfo = { - label: 'Other', - badge: 'Other', - emoji: '๐Ÿ“†', +const DEFAULT_SOURCE_INFO: CalendarSourceInfo = { label: 'Other', badge: 'Other' }; + +/** FontAwesome icon for each calendar account type. */ +export const ACCOUNT_TYPE_ICONS: Record = { + caldav: faLink, + mobileme: faMobile, + google: faCalendar, + exchange: faBuilding, + subscriptions: faRss, + local: faHardDrive, + carddav: faUser, + activesync: faArrowsRotate, }; +/** Returns the FA icon for a calendar account type, falling back to a generic calendar icon. */ +export function accountTypeIcon(type: string): IconDefinition { + return ACCOUNT_TYPE_ICONS[type] ?? faCalendarDays; +} + /** * Resolve the account type string from expo-calendar into a known * CalendarAccountType. Returns 'other' for unknown types. diff --git a/apps/mobile/src/store/eventStore.ts b/apps/mobile/src/store/eventStore.ts index 474dfbe..a729384 100644 --- a/apps/mobile/src/store/eventStore.ts +++ b/apps/mobile/src/store/eventStore.ts @@ -82,7 +82,7 @@ async function fireNotificationsForEvent(event: Event, leaveByTime: Date): Promi await Notifications.scheduleNotificationAsync({ content: { - title: `๐Ÿš† ${event.title}`, + title: event.title, body: minutesBefore === 0 ? 'Zeit zu gehen!' : `${minutesBefore} Minuten bis du losmusst`, diff --git a/packages/core/src/journey-scoring.ts b/packages/core/src/journey-scoring.ts index 447719e..34fc7f9 100644 --- a/packages/core/src/journey-scoring.ts +++ b/packages/core/src/journey-scoring.ts @@ -40,11 +40,11 @@ export function rankJourneys( const duration = durationMinutes(journey); const arrivalScore = Math.max(0, 100 - deltaMinutes * 4) - lateMinutes * 10; - const changesScore = Math.max(0, 100 - journey.changes * 35); + const changesScore = Math.max(0, 100 - journey.changes * 55); const durationScore = 100 - ((duration - minDuration) / durationRange) * 100; const cancelledPenalty = journey.cancelled ? 1_000 : 0; - const score = arrivalScore * 0.55 + changesScore * 0.3 + durationScore * 0.15 - cancelledPenalty; + const score = arrivalScore * 0.45 + changesScore * 0.4 + durationScore * 0.15 - cancelledPenalty; return { journey, score }; }) .sort((a, b) => { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1510fd5..6d73cc9 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -164,8 +164,6 @@ export type CalendarAccountType = export interface CalendarSourceInfo { label: string; badge: string; - /** Icon emoji shown next to the calendar name. */ - emoji: string; } /** Minimal calendar descriptor used for selection UI. */