feat(activity-days): add Timetable view#1196
Conversation
endTime in ActivityDaysTimetableEntry is nullable so sometimes dash isn't present
Greptile SummaryElo żelo solvrowiczu! This PR adds a Timetable view to the Activity Days feature, reusing
Confidence Score: 4/5Safe to merge with the mock in place, but the production parse path has an unverified API format assumption that will break when the mock is removed. The repository now parses the lib/features/activity_days/data/repository/activity_days_repository.dart — specifically the production parse path after the mock guard Important Files Changed
Sequence DiagramsequenceDiagram
participant View as ActivityDaysView
participant Timetable as ActivityDaysTimetable
participant UseCase as activityDaysTimetableUseCase
participant Repo as activityDaysRepository
participant API as /das API
View->>Timetable: render (tab 2)
Timetable->>UseCase: watch(activityDaysTimetableUseCaseProvider)
UseCase->>Repo: watch(activityDaysRepositoryProvider)
alt mock active (if true)
Repo-->>UseCase: hardcoded ActivityDaysResponse
else production path
Repo->>API: GET /das
API-->>Repo: "{"data": [...]} (expected new format)"
Repo-->>UseCase: ActivityDaysResponse?
end
UseCase->>UseCase: "group entries by day → IList<TimetableDay>"
UseCase-->>Timetable: "AsyncData(IList<TimetableDay>)"
Timetable-->>View: _ActivityDaysTimetableContent (pinned day headers + CalendarTile list)
Reviews (3): Last reviewed commit: "chore: format" | Re-trigger Greptile |
| class ActivityDaysTimetable extends ConsumerWidget { | ||
| const ActivityDaysTimetable({super.key}); | ||
|
|
||
| @override | ||
| Widget build(BuildContext context, WidgetRef ref) { | ||
| final timetable = ref.watch(activityDaysTimetableUseCaseProvider); | ||
|
|
||
| return switch (timetable) { | ||
| AsyncError(:final error, :final stackTrace) => MyErrorWidget(error, stackTrace: stackTrace), | ||
| AsyncData(:final value) => _ActivityDaysTimetableContent(days: value), | ||
| _ => const Center(child: CircularProgressIndicator()), | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| class _ActivityDaysTimetableContent extends StatelessWidget { | ||
| const _ActivityDaysTimetableContent({required this.days}); | ||
|
|
||
| final IList<TimetableDay> days; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| if (days.isEmpty) { | ||
| return SearchNotFound(message: context.localize.calendar_events_not_found); | ||
| } | ||
|
|
||
| final locale = Localizations.localeOf(context).languageCode; | ||
|
|
||
| return CustomScrollView( | ||
| slivers: days.map((day) { | ||
| final label = DateFormat("EEEE, d MMMM", locale).format(day.date); | ||
| return MultiSliver( | ||
| pushPinnedChildren: true, | ||
| children: [ | ||
| SliverPersistentHeader( | ||
| pinned: true, | ||
| delegate: CalendarHeaderDelegate(text: "${label[0].toUpperCase()}${label.substring(1)}"), | ||
| ), | ||
| SliverPadding( | ||
| padding: const EdgeInsets.symmetric( | ||
| horizontal: HomeViewConfig.paddingLarge, | ||
| vertical: HomeViewConfig.paddingSmall, | ||
| ), | ||
| sliver: SliverList( | ||
| delegate: SliverChildBuilderDelegate( | ||
| (context, index) => Padding( | ||
| padding: const EdgeInsets.only(bottom: HomeViewConfig.paddingSmall), | ||
| child: CalendarTile(day.events[index]), | ||
| ), | ||
| childCount: day.events.length, | ||
| ), | ||
| ), | ||
| ), | ||
| ], | ||
| ); | ||
| }).toList(), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing widgetbook story for
ActivityDaysTimetable
The project rules require every widget with UI to have a widgetbook story in the packages/widgetbook package. ActivityDaysTimetable and its content widget _ActivityDaysTimetableContent don't have a corresponding story. A story can simply override activityDaysTimetableUseCaseProvider with hardcoded TimetableDay entries to keep it HTTP-free.
Rule Used: All widgets and views that are not pure logic and ... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…itory.dart Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
| final response = await ref.getAndCacheData( | ||
| url, | ||
| ActivityDaysResponse.fromJson, | ||
| ActivityDaysListResponse.fromJson, | ||
| extraValidityCheck: (_) => true, | ||
| onRetry: ref.invalidateSelf, | ||
| ); | ||
| final events = response.castAsList; | ||
| final events = response.castAsObject.data; |
There was a problem hiding this comment.
API response format mismatch on production path
The old code used ActivityDaysResponse.fromJson + castAsList, meaning it expected the /das endpoint to return a JSON array ([{...}, {...}]). The new code uses ActivityDaysListResponse.fromJson + castAsObject.data, which requires the endpoint to return an object envelope ({"data": [{...}]}). These two shapes are mutually exclusive — if the API still returns an array, parseJSON will produce a ListJSON and castAsObject throws ArgumentError at runtime. This is currently masked by the if (true) guard, but will surface the moment the mock is removed. Please confirm the real /das response format matches the new {"data": [...]} expectation.
I used a design similar to Calendar view and it behaves similarly to calendar in terms of different font sizes for example. It is designed to handle events across several days. In case if it will be only one day and the date text shouldn't be visible, it can be easily deleted. Also mocked a couple of entries for testing. Lacks search function, not sure if it is needed in this view.