Skip to content

feat(activity-days): add Timetable view#1196

Open
mikolaj-r wants to merge 5 commits into
mainfrom
feat/das-timetable
Open

feat(activity-days): add Timetable view#1196
mikolaj-r wants to merge 5 commits into
mainfrom
feat/das-timetable

Conversation

@mikolaj-r

@mikolaj-r mikolaj-r commented Jun 10, 2026

Copy link
Copy Markdown
Member

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.

image

endTime in ActivityDaysTimetableEntry is nullable so sometimes dash
isn't present
@mikolaj-r mikolaj-r self-assigned this Jun 10, 2026
@mikolaj-r mikolaj-r linked an issue Jun 10, 2026 that may be closed by this pull request
@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Elo żelo solvrowiczu! This PR adds a Timetable view to the Activity Days feature, reusing CalendarTile and CalendarHeaderDelegate components from the Calendar feature. The implementation follows project conventions (Freezed models, IList, Riverpod codegen, named records for DTOs).

  • Adds ActivityDaysTimetableEntry Freezed model and extends ActivityDaysTimetable with an entries field; a new use case groups entries by day and maps them to SingleCalendarItem for display.
  • Introduces ActivityDaysListResponse as a JSON wrapper and switches the production parse path from an expected JSON array (castAsList) to an object envelope (castAsObject.data), which needs to be verified against the real API.
  • Updates CalendarTile's bold-font trigger from contains(\"-\") to contains(\":\") so timetable entries with only a start time are also displayed in bold, matching the intended design.

Confidence Score: 4/5

Safe 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 /das response as {"data": [...]} while the previous code expected a bare JSON array. If the backend hasn't changed its response shape, castAsObject will throw at runtime the moment the if (true) guard is lifted. Everything else — the new use case, models, and widget — is solid.

lib/features/activity_days/data/repository/activity_days_repository.dart — specifically the production parse path after the mock guard

Important Files Changed

Filename Overview
lib/features/activity_days/business/activity_days_timetable_use_case.dart New use case: groups timetable entries by day, sorts them, and maps to SingleCalendarItem using named records, IList, and @riverpod codegen — all aligned with project rules.
lib/features/activity_days/data/models/activity_days_response.dart Adds ActivityDaysListResponse (Freezed wrapper for API list) and ActivityDaysTimetableEntry; extends ActivityDaysTimetable with an entries field. The ActivityDaysListResponse assumes a {"data": [...]} response envelope that needs to match the real API.
lib/features/activity_days/data/repository/activity_days_repository.dart Switches production parse path from ActivityDaysResponse.fromJson+castAsList (expects JSON array) to ActivityDaysListResponse.fromJson+castAsObject.data (expects {"data":[]}); currently masked by if (true) mock but will throw if the real API still returns an array.
lib/features/activity_days/presentation/widgets/activity_days_timetable.dart New widget reusing CalendarHeaderDelegate and CalendarTile; handles loading/error/empty states cleanly with pinned day headers and a sliver list.
lib/features/activity_days/presentation/activity_days_view.dart One-line change: replaces the second _PlaceholderTab with ActivityDaysTimetable() — straightforward and correct.
lib/features/calendar/presentation/widgets/calendar_tile.dart Widens bold-font trigger from contains("-") (range-only) to contains(":") (any time string), so timetable entries with only a start time are also rendered bold.

Sequence Diagram

sequenceDiagram
    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)
Loading

Reviews (3): Last reviewed commit: "chore: format" | Re-trigger Greptile

Comment on lines +15 to +73
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(),
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Comment thread lib/features/activity_days/data/repository/activity_days_repository.dart Outdated
mikolaj-r and others added 2 commits June 10, 2026 18:34
…itory.dart

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Comment on lines 54 to +60
final response = await ref.getAndCacheData(
url,
ActivityDaysResponse.fromJson,
ActivityDaysListResponse.fromJson,
extraValidityCheck: (_) => true,
onRetry: ref.invalidateSelf,
);
final events = response.castAsList;
final events = response.castAsObject.data;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(activity-days): add Timetable view

1 participant