From 44aaa322962573979c25de90cee358d9c8d36fc7 Mon Sep 17 00:00:00 2001 From: fegger Date: Tue, 19 May 2026 00:06:53 +0200 Subject: [PATCH] Add edit icon button to event list item Remove emoji icons from journey display and replace with FontAwesome icons Add FontAwesome icon imports and styles for new icon usage Update calendar service to use FontAwesome icons instead of emojis Remove redundant emoji field from CalendarSourceInfo type Adjust journey scoring weights to prioritize fewer changes Remove emoji icons and simplify event metadata display --- apps/mobile/src/components/BikeSection.tsx | 21 +++++- apps/mobile/src/components/JourneyList.tsx | 6 +- apps/mobile/src/components/NearbyStops.tsx | 10 ++- apps/mobile/src/screens/AddEventScreen.tsx | 5 +- .../src/screens/CalendarImportScreen.tsx | 64 ++++++++++------ apps/mobile/src/screens/EventDetailScreen.tsx | 25 ++++++- apps/mobile/src/screens/EventListScreen.tsx | 38 +++------- apps/mobile/src/screens/SettingsScreen.tsx | 39 ++++++++-- apps/mobile/src/services/calendar.ts | 73 ++++++++----------- apps/mobile/src/store/eventStore.ts | 2 +- packages/core/src/journey-scoring.ts | 4 +- packages/core/src/types.ts | 2 - 12 files changed, 168 insertions(+), 121 deletions(-) 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. */