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() {