diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 132750f..9610a4a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -49,6 +49,9 @@
+
? actions;
+ final Widget? leading;
final bool showBackArrow;
final Color? surfaceTint;
final PreferredSizeWidget? bottom;
@@ -25,7 +27,7 @@ class AnyStepAppBar extends StatelessWidget implements PreferredSizeWidget {
backgroundColor: Colors.transparent,
elevation: 0,
actions: actions,
- leading: null,
+ leading: leading,
// titleTextStyle: AnyStepTextStyles.title.copyWith(
// color: Theme.of(context).colorScheme.onSurface,
// ),
diff --git a/lib/core/features/dashboard/presentation/widgets/dashboard_calendar_card.dart b/lib/core/features/dashboard/presentation/widgets/dashboard_calendar_card.dart
index e3d6455..3e3fdc8 100644
--- a/lib/core/features/dashboard/presentation/widgets/dashboard_calendar_card.dart
+++ b/lib/core/features/dashboard/presentation/widgets/dashboard_calendar_card.dart
@@ -43,90 +43,88 @@ class _DashboardCalendarCardState extends ConsumerState {
child: Card(
child: Padding(
padding: const EdgeInsets.all(AnyStepSpacing.md16),
- child: eventsAsync.when(
- loading: () => SizedBox(
- height: 380,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: const [
- SizedBox(height: AnyStepSpacing.md14),
- Center(child: AnyStepShimmer(height: 24, width: 140)),
- SizedBox(height: AnyStepSpacing.sm10),
- AnyStepShimmer(height: 280),
- SizedBox(height: AnyStepSpacing.md12),
- AnyStepShimmer(height: 16, width: 220),
- SizedBox(height: AnyStepSpacing.sm8),
- AnyStepShimmer(height: 16, width: 180),
- ],
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ TableCalendar(
+ firstDay: DateTime.now().subtract(const Duration(days: 365)),
+ lastDay: DateTime.now().add(const Duration(days: 365)),
+ focusedDay: _focusedDay,
+ selectedDayPredicate: (day) => isSameDay(day, _selectedDay),
+ eventLoader: (day) {
+ final data = eventsAsync.value;
+ if (data == null) return const [];
+ final key = DateTime(day.year, day.month, day.day);
+ return data.where((event) {
+ return event.startTime.year == key.year &&
+ event.startTime.month == key.month &&
+ event.startTime.day == key.day;
+ }).toList();
+ },
+ onDaySelected: (selectedDay, focusedDay) {
+ setState(() {
+ _selectedDay = selectedDay;
+ _focusedDay = focusedDay;
+ });
+ },
+ onPageChanged: (focusedDay) {
+ setState(() => _focusedDay = focusedDay);
+ },
+ calendarStyle: CalendarStyle(
+ todayDecoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primary.withAlpha(80),
+ shape: BoxShape.circle,
+ ),
+ selectedDecoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primary,
+ shape: BoxShape.circle,
+ ),
+ markerDecoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.secondary,
+ shape: BoxShape.circle,
+ ),
+ markersMaxCount: 3,
+ ),
+ headerStyle: HeaderStyle(
+ titleCentered: true,
+ formatButtonVisible: false,
+ leftChevronIcon: Icon(
+ Icons.chevron_left,
+ color: Theme.of(context).iconTheme.color,
+ ),
+ rightChevronIcon: Icon(
+ Icons.chevron_right,
+ color: Theme.of(context).iconTheme.color,
+ ),
+ ),
),
- ),
- error: (e, st) => SizedBox(height: 380, child: Center(child: Text(loc.failedToLoad))),
- data: (events) {
- final eventsByDay = >{};
- for (final event in events) {
- final dateKey = DateTime(
- event.startTime.year,
- event.startTime.month,
- event.startTime.day,
- );
- eventsByDay.putIfAbsent(dateKey, () => []).add(event);
- }
- final selected = _selectedDay ?? _focusedDay;
- final selectedKey = DateTime(selected.year, selected.month, selected.day);
- final selectedEvents = eventsByDay[selectedKey] ?? const [];
+ const SizedBox(height: AnyStepSpacing.md12),
+ eventsAsync.when(
+ loading: () => Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: const [
+ AnyStepShimmer(height: 16, width: 220),
+ SizedBox(height: AnyStepSpacing.sm8),
+ AnyStepShimmer(height: 16, width: 180),
+ ],
+ ),
+ error: (_, __) => Text(loc.failedToLoad),
+ data: (events) {
+ final eventsByDay = >{};
+ for (final event in events) {
+ final dateKey = DateTime(
+ event.startTime.year,
+ event.startTime.month,
+ event.startTime.day,
+ );
+ eventsByDay.putIfAbsent(dateKey, () => []).add(event);
+ }
+ final selected = _selectedDay ?? _focusedDay;
+ final selectedKey = DateTime(selected.year, selected.month, selected.day);
+ final selectedEvents = eventsByDay[selectedKey] ?? const [];
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- TableCalendar(
- firstDay: DateTime.now().subtract(const Duration(days: 365)),
- lastDay: DateTime.now().add(const Duration(days: 365)),
- focusedDay: _focusedDay,
- selectedDayPredicate: (day) => isSameDay(day, _selectedDay),
- eventLoader: (day) {
- final key = DateTime(day.year, day.month, day.day);
- return eventsByDay[key] ?? const [];
- },
- onDaySelected: (selectedDay, focusedDay) {
- setState(() {
- _selectedDay = selectedDay;
- _focusedDay = focusedDay;
- });
- },
- onPageChanged: (focusedDay) {
- setState(() => _focusedDay = focusedDay);
- },
- calendarStyle: CalendarStyle(
- todayDecoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primary.withAlpha(80),
- shape: BoxShape.circle,
- ),
- selectedDecoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primary,
- shape: BoxShape.circle,
- ),
- markerDecoration: BoxDecoration(
- color: Theme.of(context).colorScheme.secondary,
- shape: BoxShape.circle,
- ),
- markersMaxCount: 3,
- ),
- headerStyle: HeaderStyle(
- titleCentered: true,
- formatButtonVisible: false,
- leftChevronIcon: Icon(
- Icons.chevron_left,
- color: Theme.of(context).iconTheme.color,
- ),
- rightChevronIcon: Icon(
- Icons.chevron_right,
- color: Theme.of(context).iconTheme.color,
- ),
- ),
- ),
- const SizedBox(height: AnyStepSpacing.md12),
- if (selectedEvents.isEmpty)
- Container(
+ if (selectedEvents.isEmpty) {
+ return Container(
width: double.infinity,
padding: const EdgeInsets.all(AnyStepSpacing.md12),
decoration: BoxDecoration(
@@ -137,21 +135,21 @@ class _DashboardCalendarCardState extends ConsumerState {
loc.dashboardNoEventsCalendar,
style: Theme.of(context).textTheme.bodySmall,
),
- )
- else
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- for (final event in selectedEvents.take(3))
- Padding(
- padding: const EdgeInsets.only(bottom: AnyStepSpacing.sm8),
- child: _EventRow(event: event),
- ),
- ],
- ),
- ],
- );
- },
+ );
+ }
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ for (final event in selectedEvents.take(3))
+ Padding(
+ padding: const EdgeInsets.only(bottom: AnyStepSpacing.sm8),
+ child: _EventRow(event: event),
+ ),
+ ],
+ );
+ },
+ ),
+ ],
),
),
),
diff --git a/lib/core/features/events/presentation/event_detail/event_detail_screen.dart b/lib/core/features/events/presentation/event_detail/event_detail_screen.dart
index 1b66a7d..8270c9e 100644
--- a/lib/core/features/events/presentation/event_detail/event_detail_screen.dart
+++ b/lib/core/features/events/presentation/event_detail/event_detail_screen.dart
@@ -65,13 +65,22 @@ class _EventDetailScreenState extends ConsumerState {
final userAsync = ref.watch(currentUserStreamProvider);
final loc = AppLocalizations.of(context);
+ final leading = context.canPop()
+ ? null
+ : IconButton(
+ icon: const Icon(Icons.chevron_left),
+ onPressed: () {
+ context.go(EventFeedScreen.path);
+ },
+ );
+
return eventAsync.when(
loading: () => AnyStepScaffold(
- appBar: AnyStepAppBar(title: Text(loc.eventDetailTitle)),
+ appBar: AnyStepAppBar(title: Text(loc.eventDetailTitle), leading: leading),
body: const Center(child: AnyStepLoadingIndicator()),
),
error: (e, st) => AnyStepScaffold(
- appBar: AnyStepAppBar(title: Text(loc.eventDetailTitle)),
+ appBar: AnyStepAppBar(title: Text(loc.eventDetailTitle), leading: leading),
body: RefreshIndicator(
onRefresh: () async => ref.invalidate(getEventProvider(widget.id)),
child: ScrollableCenteredContent(child: AnyStepErrorWidget()),
@@ -103,10 +112,7 @@ class _EventDetailScreenState extends ConsumerState {
}
},
itemBuilder: (context) => [
- PopupMenuItem(
- value: _EventDetailMenuAction.share,
- child: Text(loc.shareAction),
- ),
+ PopupMenuItem(value: _EventDetailMenuAction.share, child: Text(loc.shareAction)),
if (!isPast)
PopupMenuItem(
value: _EventDetailMenuAction.addToCalendar,
@@ -125,6 +131,7 @@ class _EventDetailScreenState extends ConsumerState {
appBar: AnyStepAppBar(
title: Text(loc.eventDetailTitle),
actions: isPast ? null : actions,
+ leading: leading,
),
body: MaxWidthContainer(
child: SafeArea(
diff --git a/lib/core/features/location/presentation/any_step_address_form.dart b/lib/core/features/location/presentation/any_step_address_form.dart
index 0eb7101..badf4fe 100644
--- a/lib/core/features/location/presentation/any_step_address_form.dart
+++ b/lib/core/features/location/presentation/any_step_address_form.dart
@@ -298,11 +298,8 @@ class _AnyStepAddressFormState extends ConsumerState {
_debounce?.cancel();
_addressId = null;
form.fields[widget.addressIdFieldName]?.didChange(null);
- if (widget.showNameField) {
- final nameValue = parsed.name ?? prediction.mainText ?? prediction.description;
- form.fields[widget.nameFieldName]?.didChange(nameValue);
- }
- form.fields['street']?.didChange(parsed.street);
+ final streetText = parsed.street.isNotEmpty ? parsed.street : prediction.mainText ?? '';
+ form.fields['street']?.didChange(streetText);
form.fields['streetSecondary']?.didChange(parsed.streetSecondary);
form.fields['city']?.didChange(parsed.city);
form.fields['state']?.didChange(parsed.state);
@@ -311,7 +308,7 @@ class _AnyStepAddressFormState extends ConsumerState {
_placeName = parsed.name;
_latitude = parsed.latitude;
_longitude = parsed.longitude;
- _streetController.text = parsed.street.isNotEmpty ? parsed.street : prediction.description;
+ _streetController.text = streetText;
_streetController.selection = TextSelection.fromPosition(
TextPosition(offset: _streetController.text.length),
);
diff --git a/lib/core/features/notifications/data/notification_repository.dart b/lib/core/features/notifications/data/notification_repository.dart
index 116afa4..c482587 100644
--- a/lib/core/features/notifications/data/notification_repository.dart
+++ b/lib/core/features/notifications/data/notification_repository.dart
@@ -13,6 +13,14 @@ import 'package:flutter/foundation.dart';
part 'notification_repository.g.dart';
+/// TODO:
+/// 1. Implement IRepository in NotificationRepository
+/// 2. Create a provider to fetch and paginate user notifications (scoped by user_id == usermodel.id aka authState sub), sorted by created at date
+/// 3. Create a listview similar to event feed. Handle showing read and unread states.
+/// 4. When the user clicks on a notification it should open a notification detail screen and automatically mark the notification as read.
+///
+/// Entrypoint: Dashboard appbar action IconButton, pushes route. Only visible for authenticated users.
+
const String _eventIdKey = 'event_id';
String _eventDetailPath(int id) => '/events/$id';
@@ -21,9 +29,9 @@ class NotificationRepository {
required FirebaseMessaging messaging,
required GoRouter router,
required SupabaseClient supabase,
- }) : _messaging = messaging,
- _router = router,
- _supabase = supabase;
+ }) : _messaging = messaging,
+ _router = router,
+ _supabase = supabase;
final FirebaseMessaging _messaging;
final GoRouter _router;
@@ -95,10 +103,7 @@ class NotificationRepository {
content: Text(body.isEmpty ? title : '$title\n$body'),
action: eventId == null
? null
- : SnackBarAction(
- label: 'Open',
- onPressed: () => _router.go(_eventDetailPath(eventId)),
- ),
+ : SnackBarAction(label: 'Open', onPressed: () => _router.go(_eventDetailPath(eventId))),
),
);
}
@@ -134,20 +139,16 @@ class NotificationRepository {
if (user == null) return;
try {
final platform = _platformLabel();
- await _supabase.from('user_fcm_tokens').upsert(
- {
- 'user_id': user.id,
- 'token': token,
- 'platform': platform,
- },
- onConflict: 'token',
- );
+ await _supabase.from('user_fcm_tokens').upsert({
+ 'user_id': user.id,
+ 'token': token,
+ 'platform': platform,
+ }, onConflict: 'token');
Log.i('Synced FCM token for user ${user.id} ($platform)');
} catch (e, st) {
Log.e('Failed to sync FCM token', e, st);
}
}
-
}
String _platformLabel() {