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. */