From 909f795f6bafd0c80e78de1f22d6bf1520f2ac1b Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Sun, 31 Aug 2025 23:40:24 +0900 Subject: [PATCH 1/6] feat: implement MonthCalendar component and integrate into HomeScreenTmp - Introduced a new MonthCalendar component to manage monthly schedules. - Replaced the previous inline calendar implementation in HomeScreenTmp with the new MonthCalendar for better modularity and maintainability. - Added widgetbook use cases for MonthCalendar and TodaysScheduleTile to facilitate UI testing and documentation. --- .../home/components/month_calendar.dart | 122 ++++++++++++++++++ .../home/screens/home_screen_tmp.dart | 119 ++--------------- widgetbook/lib/month_calendar.dart | 40 ++++++ widgetbook/lib/todays_schedule_tile.dart | 56 ++++++++ widgetbook/lib/week_calendar.dart | 34 +++++ 5 files changed, 261 insertions(+), 110 deletions(-) create mode 100644 lib/presentation/home/components/month_calendar.dart create mode 100644 widgetbook/lib/month_calendar.dart create mode 100644 widgetbook/lib/todays_schedule_tile.dart create mode 100644 widgetbook/lib/week_calendar.dart diff --git a/lib/presentation/home/components/month_calendar.dart b/lib/presentation/home/components/month_calendar.dart new file mode 100644 index 00000000..d93df054 --- /dev/null +++ b/lib/presentation/home/components/month_calendar.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:on_time_front/presentation/calendar/bloc/monthly_schedules_bloc.dart'; +import 'package:on_time_front/presentation/shared/components/calendar/centered_calendar_header.dart'; +import 'package:on_time_front/presentation/shared/theme/calendar_theme.dart'; +import 'package:table_calendar/table_calendar.dart'; + +class MonthCalendar extends StatefulWidget { + const MonthCalendar({ + super.key, + required this.monthlySchedulesState, + this.dispatchBlocEvents = true, + }); + + final MonthlySchedulesState monthlySchedulesState; + final bool dispatchBlocEvents; + + @override + State createState() => _MonthCalendarState(); +} + +class _MonthCalendarState extends State { + late DateTime _focusedDay; + + @override + void initState() { + super.initState(); + _focusedDay = DateTime.now(); + } + + void _onLeftArrowTap() { + final DateTime nextFocusedDay = + DateTime(_focusedDay.year, _focusedDay.month - 1, 1); + setState(() { + _focusedDay = nextFocusedDay; + }); + if (widget.dispatchBlocEvents) { + context.read().add(MonthlySchedulesMonthAdded( + date: DateTime(nextFocusedDay.year, nextFocusedDay.month, 1))); + } + } + + void _onRightArrowTap() { + final DateTime nextFocusedDay = + DateTime(_focusedDay.year, _focusedDay.month + 1, 1); + setState(() { + _focusedDay = nextFocusedDay; + }); + if (widget.dispatchBlocEvents) { + context.read().add(MonthlySchedulesMonthAdded( + date: DateTime(nextFocusedDay.year, nextFocusedDay.month, 1))); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final calendarTheme = theme.extension()!; + + return Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(11), + ), + child: TableCalendar( + locale: Localizations.localeOf(context).toString(), + eventLoader: (day) { + day = DateTime(day.year, day.month, day.day); + return widget.monthlySchedulesState.schedules[day] ?? []; + }, + sixWeekMonthsEnforced: true, + rowHeight: 50, + availableGestures: AvailableGestures.none, + focusedDay: _focusedDay, + firstDay: DateTime(2024, 1, 1), + lastDay: DateTime(2025, 12, 31), + calendarFormat: CalendarFormat.month, + headerStyle: calendarTheme.headerStyle, + daysOfWeekStyle: calendarTheme.daysOfWeekStyle, + daysOfWeekHeight: 40, + calendarStyle: calendarTheme.calendarStyle, + onDaySelected: (selectedDay, focusedDay) { + // Handle day selection if needed + }, + onPageChanged: (focusedDay) { + setState(() { + _focusedDay = focusedDay; + }); + if (widget.dispatchBlocEvents) { + context.read().add(MonthlySchedulesMonthAdded( + date: DateTime( + focusedDay.year, focusedDay.month, focusedDay.day))); + } + }, + calendarBuilders: CalendarBuilders( + headerTitleBuilder: (context, date) { + return CenteredCalendarHeader( + focusedMonth: date, + onLeftArrowTap: _onLeftArrowTap, + onRightArrowTap: _onRightArrowTap, + titleTextStyle: calendarTheme.headerStyle.titleTextStyle, + leftIcon: calendarTheme.headerStyle.leftChevronIcon, + rightIcon: calendarTheme.headerStyle.rightChevronIcon, + ); + }, + todayBuilder: (context, day, focusedDay) => Container( + margin: const EdgeInsets.all(4.0), + alignment: Alignment.center, + decoration: calendarTheme.todayDecoration, + child: Text( + day.day.toString(), + style: textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onPrimary, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/home/screens/home_screen_tmp.dart b/lib/presentation/home/screens/home_screen_tmp.dart index 42055bb1..eca79c3d 100644 --- a/lib/presentation/home/screens/home_screen_tmp.dart +++ b/lib/presentation/home/screens/home_screen_tmp.dart @@ -7,11 +7,9 @@ import 'package:on_time_front/presentation/app/bloc/app_bloc.dart'; import 'package:on_time_front/presentation/calendar/bloc/monthly_schedules_bloc.dart'; import 'package:on_time_front/presentation/home/components/todays_schedule_tile.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:on_time_front/presentation/shared/components/calendar/centered_calendar_header.dart'; -import 'package:table_calendar/table_calendar.dart'; import 'package:on_time_front/presentation/shared/components/arc_indicator.dart'; import 'package:on_time_front/presentation/shared/theme/theme.dart'; -import 'package:on_time_front/presentation/shared/theme/calendar_theme.dart'; +import 'package:on_time_front/presentation/home/components/month_calendar.dart'; class HomeScreenTmp extends StatefulWidget { const HomeScreenTmp({super.key}); @@ -89,10 +87,12 @@ class _HomeScreenTmpState extends State { padding: const EdgeInsets.only(top: 49.0), child: Container( decoration: BoxDecoration( - color: colorScheme.surface, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16))), + color: colorScheme.surface, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), ), ), ), @@ -142,7 +142,7 @@ class _MonthlySchedule extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _MonthlyScheduleHeader(), - _MonthCalendar( + MonthCalendar( monthlySchedulesState: monthlySchedulesState, ), ], @@ -193,108 +193,7 @@ class _MonthlyScheduleHeader extends StatelessWidget { } } -class _MonthCalendar extends StatefulWidget { - const _MonthCalendar({required this.monthlySchedulesState}); - - final MonthlySchedulesState monthlySchedulesState; - - @override - State<_MonthCalendar> createState() => _MonthCalendarState(); -} - -class _MonthCalendarState extends State<_MonthCalendar> { - late DateTime _focusedDay; - - @override - void initState() { - super.initState(); - _focusedDay = DateTime.now(); - } - - void _onLeftArrowTap() { - setState(() { - _focusedDay = DateTime(_focusedDay.year, _focusedDay.month - 1, 1); - }); - } - - void _onRightArrowTap() { - setState(() { - _focusedDay = DateTime(_focusedDay.year, _focusedDay.month + 1, 1); - }); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - final calendarTheme = theme.extension()!; - - if (widget.monthlySchedulesState.schedules.isEmpty) { - if (widget.monthlySchedulesState.status == - MonthlySchedulesStatus.loading) { - return CircularProgressIndicator(); - } else if (widget.monthlySchedulesState.status != - MonthlySchedulesStatus.success) { - return const SizedBox(); - } - } - - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(11), - ), - child: TableCalendar( - locale: Localizations.localeOf(context).toString(), - eventLoader: (day) { - day = DateTime(day.year, day.month, day.day); - return widget.monthlySchedulesState.schedules[day] ?? []; - }, - focusedDay: _focusedDay, - firstDay: DateTime(2024, 1, 1), - lastDay: DateTime(2025, 12, 31), - calendarFormat: CalendarFormat.month, - headerStyle: calendarTheme.headerStyle, - daysOfWeekStyle: calendarTheme.daysOfWeekStyle, - daysOfWeekHeight: 40, - calendarStyle: calendarTheme.calendarStyle, - onDaySelected: (selectedDay, focusedDay) { - // Handle day selection if needed - }, - onPageChanged: (focusedDay) { - setState(() { - _focusedDay = focusedDay; - }); - context.read().add(MonthlySchedulesMonthAdded( - date: - DateTime(focusedDay.year, focusedDay.month, focusedDay.day))); - }, - calendarBuilders: CalendarBuilders( - headerTitleBuilder: (context, date) { - return CenteredCalendarHeader( - focusedMonth: date, - onLeftArrowTap: _onLeftArrowTap, - onRightArrowTap: _onRightArrowTap, - titleTextStyle: calendarTheme.headerStyle.titleTextStyle, - leftIcon: calendarTheme.headerStyle.leftChevronIcon, - rightIcon: calendarTheme.headerStyle.rightChevronIcon, - ); - }, - todayBuilder: (context, day, focusedDay) => Container( - margin: const EdgeInsets.all(4.0), - alignment: Alignment.center, - decoration: calendarTheme.todayDecoration, - child: Text( - day.day.toString(), - style: textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onPrimary, - ), - ), - ), - ), - ), - ); - } -} +// Moved MonthCalendar into components/month_calendar.dart class _CharacterSection extends StatelessWidget { const _CharacterSection({ diff --git a/widgetbook/lib/month_calendar.dart b/widgetbook/lib/month_calendar.dart new file mode 100644 index 00000000..35eb8731 --- /dev/null +++ b/widgetbook/lib/month_calendar.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:on_time_front/presentation/home/components/month_calendar.dart'; +import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/presentation/calendar/bloc/monthly_schedules_bloc.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase( + name: 'Default', + type: MonthCalendar, +) +Widget monthCalendarUseCase(BuildContext context) { + // Build a simple fake schedules map for demonstration + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final Map> schedules = { + today: [], + DateTime(now.year, now.month, (now.day + 1)): [], + }; + + final MonthlySchedulesState state = MonthlySchedulesState( + status: MonthlySchedulesStatus.success, + schedules: schedules, + startDate: DateTime(now.year, now.month, 1), + endDate: DateTime(now.year, now.month + 1, 0), + ); + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MonthCalendar( + monthlySchedulesState: state, + dispatchBlocEvents: false, + ), + ], + ), + ); +} diff --git a/widgetbook/lib/todays_schedule_tile.dart b/widgetbook/lib/todays_schedule_tile.dart new file mode 100644 index 00000000..b1e8b813 --- /dev/null +++ b/widgetbook/lib/todays_schedule_tile.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:on_time_front/domain/entities/place_entity.dart'; +import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/presentation/home/components/todays_schedule_tile.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase( + name: 'No Schedule', + type: TodaysScheduleTile, +) +Widget todaysScheduleTileNoSchedule(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: TodaysScheduleTile(), + ); +} + +@widgetbook.UseCase( + name: 'With Schedule', + type: TodaysScheduleTile, +) +Widget todaysScheduleTileWithSchedule(BuildContext context) { + final scheduleName = context.knobs.string( + label: 'Schedule Name', + initialValue: '팀 미팅', + ); + final placeName = context.knobs.string( + label: 'Place Name', + initialValue: '강남역 스타벅스', + ); + final scheduleTime = context.knobs.dateTime( + label: 'Schedule Time', + initialValue: DateTime.now().add(const Duration(hours: 2)), + start: DateTime(2020, 1, 1), + end: DateTime(2030, 12, 31), + ); + + final schedule = ScheduleEntity( + id: 'schedule_1', + place: PlaceEntity(id: 'place_1', placeName: placeName), + scheduleName: scheduleName, + scheduleTime: scheduleTime, + moveTime: const Duration(minutes: 25), + isChanged: false, + isStarted: false, + scheduleSpareTime: const Duration(minutes: 15), + scheduleNote: '', + latenessTime: 0, + ); + + return Padding( + padding: const EdgeInsets.all(16.0), + child: TodaysScheduleTile(schedule: schedule), + ); +} diff --git a/widgetbook/lib/week_calendar.dart b/widgetbook/lib/week_calendar.dart new file mode 100644 index 00000000..317c68c3 --- /dev/null +++ b/widgetbook/lib/week_calendar.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:on_time_front/presentation/home/components/week_calendar.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase( + name: 'Default', + type: WeekCalendar, +) +Widget weekCalendarUseCase(BuildContext context) { + final now = DateTime.now(); + final highlightedCount = context.knobs.double + .slider( + label: 'Highlighted Days', + min: 0, + max: 7, + initialValue: 2, + ) + .toInt(); + + final highlighted = List.generate( + highlightedCount, + (i) => DateTime(now.year, now.month, now.day + i), + ); + + return Padding( + padding: const EdgeInsets.all(16.0), + child: WeekCalendar( + date: now, + highlightedDates: highlighted, + onDateSelected: (d) {}, + ), + ); +} From 7eed5806d3f3e09d7d6dd943ceb7c82ab6aaee61 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 1 Sep 2025 00:51:34 +0900 Subject: [PATCH 2/6] feat: configure dependency injection and improve media query handling in WidgetbookApp - Added dependency injection setup by calling `configureDependencies()` in the main function. - Enhanced the background builder in WidgetbookApp to handle MediaQuery, ensuring proper padding adjustments for child widgets. --- widgetbook/lib/main.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/widgetbook/lib/main.dart b/widgetbook/lib/main.dart index c6a36385..a0822db5 100644 --- a/widgetbook/lib/main.dart +++ b/widgetbook/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:on_time_front/presentation/shared/theme/theme.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:widgetbook/widgetbook.dart'; +import 'package:on_time_front/core/di/di_setup.dart'; import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; // This file does not exist yet, @@ -9,6 +10,7 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; import 'main.directories.g.dart'; void main() { + configureDependencies(); runApp(const WidgetbookApp()); } @@ -44,11 +46,22 @@ class WidgetbookApp extends StatelessWidget { BuilderAddon( name: 'background', builder: (context, child) { + final mediaQuery = MediaQuery.maybeOf(context); + final wrappedChild = mediaQuery == null + ? child + : MediaQuery( + data: mediaQuery.copyWith( + padding: EdgeInsets.zero, + viewPadding: EdgeInsets.zero, + ), + child: child, + ); + return Container( height: double.infinity, width: double.infinity, color: Colors.white, - child: child, + child: wrappedChild, ); }, ), From 24299334b944c8bbc084f54731e4351f90dee26d Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 1 Sep 2025 19:01:53 +0900 Subject: [PATCH 3/6] feat: add home banner image asset - Introduced a new home banner image asset to enhance the visual appeal of the application. --- assets/home_banner.png | Bin 0 -> 43239 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/home_banner.png diff --git a/assets/home_banner.png b/assets/home_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..55c63c099c2c9ca4aa20ed968decdc6f0dc22e17 GIT binary patch literal 43239 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 literal 0 HcmV?d00001 From 0a55ab5e2b04ded4cb1ebbee2a0f9a399d1c54c0 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 1 Sep 2025 19:02:13 +0900 Subject: [PATCH 4/6] feat: enhance ScheduleTimerBloc with initial timer management - Added an initial timer to synchronize with minute boundaries, improving the accuracy of scheduled events. - Implemented checks to ensure events are only added if the bloc is active, preventing potential errors. - Updated the close method to cancel the initial timer when the bloc is closed, ensuring proper resource management. --- .../home/bloc/schedule_timer_bloc.dart | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/presentation/home/bloc/schedule_timer_bloc.dart b/lib/presentation/home/bloc/schedule_timer_bloc.dart index 8ea2adf4..3cec7714 100644 --- a/lib/presentation/home/bloc/schedule_timer_bloc.dart +++ b/lib/presentation/home/bloc/schedule_timer_bloc.dart @@ -9,6 +9,7 @@ part 'schedule_timer_state.dart'; class ScheduleTimerBloc extends Bloc { StreamSubscription? _tickerSubscription; + Timer? _initialTimer; DateTime? _scheduleTime; ScheduleTimerBloc() : super(const ScheduleTimerInitial()) { @@ -21,6 +22,7 @@ class ScheduleTimerBloc extends Bloc { @override Future close() { _tickerSubscription?.cancel(); + _initialTimer?.cancel(); return super.close(); } @@ -28,6 +30,7 @@ class ScheduleTimerBloc extends Bloc { ScheduleTimerStarted event, Emitter emit) { _scheduleTime = event.scheduleTime; _tickerSubscription?.cancel(); + _initialTimer?.cancel(); // Calculate initial time difference final now = DateTime.now(); @@ -48,7 +51,10 @@ class ScheduleTimerBloc extends Bloc { final secondsUntilNextMinute = 60 - now.second; // Create an initial timer to sync with minute boundaries - Timer(Duration(seconds: secondsUntilNextMinute), () { + _initialTimer = Timer(Duration(seconds: secondsUntilNextMinute), () { + // Check if bloc is still active before adding events + if (isClosed) return; + // After the initial sync, start the regular minute timer add(ScheduleTimerTicked(DateTime.now())); @@ -57,7 +63,10 @@ class ScheduleTimerBloc extends Bloc { const Duration(minutes: 1), (_) => DateTime.now(), ).listen((currentTime) { - add(ScheduleTimerTicked(currentTime)); + // Check if bloc is still active before adding events + if (!isClosed) { + add(ScheduleTimerTicked(currentTime)); + } }); }); } @@ -83,6 +92,7 @@ class ScheduleTimerBloc extends Bloc { void _onTimerStopped( ScheduleTimerStopped event, Emitter emit) { _tickerSubscription?.cancel(); + _initialTimer?.cancel(); _scheduleTime = null; emit(const ScheduleTimerInitial()); } @@ -91,6 +101,7 @@ class ScheduleTimerBloc extends Bloc { ScheduleTimerUpdated event, Emitter emit) { if (event.scheduleTime == null) { _tickerSubscription?.cancel(); + _initialTimer?.cancel(); _scheduleTime = null; emit(const ScheduleTimerInitial()); } else { From 6621e74e38087ef4f7cf6db634e483d63de1006b Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 1 Sep 2025 19:02:26 +0900 Subject: [PATCH 5/6] refactor: convert HomeScreenTmp to StatelessWidget and modularize content - Changed HomeScreenTmp from StatefulWidget to StatelessWidget for improved performance and simplicity. - Extracted home screen content into a new HomeScreenContent widget for better modularity and testability. - Introduced a separate _TodaysScheduleOverlay widget to enhance code organization and readability. - Updated the layout to utilize the new home banner image asset. --- .../home/screens/home_screen_tmp.dart | 112 +++++++++--------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/lib/presentation/home/screens/home_screen_tmp.dart b/lib/presentation/home/screens/home_screen_tmp.dart index eca79c3d..69cbecd8 100644 --- a/lib/presentation/home/screens/home_screen_tmp.dart +++ b/lib/presentation/home/screens/home_screen_tmp.dart @@ -11,65 +11,84 @@ import 'package:on_time_front/presentation/shared/components/arc_indicator.dart' import 'package:on_time_front/presentation/shared/theme/theme.dart'; import 'package:on_time_front/presentation/home/components/month_calendar.dart'; -class HomeScreenTmp extends StatefulWidget { +/// Wrapper widget that provides the BlocProvider for HomeScreenTmp +class HomeScreenTmp extends StatelessWidget { const HomeScreenTmp({super.key}); - @override - State createState() => _HomeScreenTmpState(); -} - -class _HomeScreenTmpState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { final dateOfToday = DateTime( DateTime.now().year, DateTime.now().month, DateTime.now().day, 0, 0, 0); - final double score = context.select((AppBloc bloc) => - bloc.state.user.mapOrNull((user) => user.score) ?? -1); - final colorScheme = Theme.of(context).colorScheme; return BlocProvider( create: (context) => getIt.get() ..add(MonthlySchedulesSubscriptionRequested(date: dateOfToday)), child: BlocBuilder( builder: (context, state) { - return SingleChildScrollView( + return HomeScreenContent(state: state); + }, + ), + ); + } +} + +/// The actual home screen content that can be tested independently +class HomeScreenContent extends StatelessWidget { + const HomeScreenContent({ + super.key, + required this.state, + this.userScore, + }); + + final MonthlySchedulesState state; + final double? userScore; + + @override + Widget build(BuildContext context) { + final double score = userScore ?? + context.select((AppBloc bloc) => + bloc.state.user.mapOrNull((user) => user.score) ?? -1); + final colorScheme = Theme.of(context).colorScheme; + + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + color: colorScheme.primary, + padding: const EdgeInsets.only(top: 58.0), child: Column( - mainAxisSize: MainAxisSize.max, children: [ - Container( - color: colorScheme.primary, - padding: const EdgeInsets.only(top: 58.0), - child: Column( - children: [ - _CharacterSection(score: score), - todaysScheduleOverlayBuilder(state), - ], - ), - ), - Container( - padding: const EdgeInsets.only( - top: 0.0, left: 16.0, right: 16.0, bottom: 24.0), - decoration: BoxDecoration( - color: colorScheme.surface, - ), - child: _MonthlySchedule( - monthlySchedulesState: state, - ), - ), + _CharacterSection(score: score), + _TodaysScheduleOverlay(state: state), ], ), - ); - }, + ), + Container( + padding: const EdgeInsets.only( + top: 0.0, left: 16.0, right: 16.0, bottom: 24.0), + decoration: BoxDecoration( + color: colorScheme.surface, + ), + child: _MonthlySchedule( + monthlySchedulesState: state, + ), + ), + ], ), ); } +} - Widget todaysScheduleOverlayBuilder(MonthlySchedulesState state) { +class _TodaysScheduleOverlay extends StatelessWidget { + const _TodaysScheduleOverlay({ + required this.state, + }); + + final MonthlySchedulesState state; + + @override + Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = Theme.of(context).colorScheme; @@ -204,20 +223,7 @@ class _CharacterSection extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 17.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(top: 27.0), - child: _Slogan(comment: AppLocalizations.of(context)!.slogan), - ), - _Character(), - ], - ), - ); + return Image.asset('home_banner.png', package: 'assets'); } } From 104d1dcdc7df4d15dc45862b7458cb48e2eb1f47 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 1 Sep 2025 19:02:34 +0900 Subject: [PATCH 6/6] feat: add mock data and use cases for HomeScreenContent in Widgetbook - Introduced a new file `home_screen_tmp.dart` containing mock data helper functions for `PlaceEntity` and `ScheduleEntity`. - Created multiple use cases for `HomeScreenContent` to demonstrate various states: with schedules, empty state, loading state, and today-only schedule. - Enhanced testing and documentation capabilities for the home screen component in Widgetbook. --- widgetbook/lib/home_screen_tmp.dart | 178 ++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 widgetbook/lib/home_screen_tmp.dart diff --git a/widgetbook/lib/home_screen_tmp.dart b/widgetbook/lib/home_screen_tmp.dart new file mode 100644 index 00000000..7e16b285 --- /dev/null +++ b/widgetbook/lib/home_screen_tmp.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:on_time_front/domain/entities/place_entity.dart'; +import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/presentation/calendar/bloc/monthly_schedules_bloc.dart'; +import 'package:on_time_front/presentation/home/screens/home_screen_tmp.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +/// Mock data helper functions +PlaceEntity _createMockPlace({ + String id = "mock-place-1", + String name = "Gangnam Station", +}) { + return PlaceEntity( + id: id, + placeName: name, + ); +} + +ScheduleEntity _createMockSchedule({ + String id = "mock-schedule-1", + String name = "Team Meeting", + DateTime? scheduleTime, + PlaceEntity? place, + Duration moveTime = const Duration(minutes: 30), + Duration spareTime = const Duration(minutes: 15), + String note = "Important meeting with the team", +}) { + return ScheduleEntity( + id: id, + place: place ?? _createMockPlace(), + scheduleName: name, + scheduleTime: scheduleTime ?? DateTime.now().add(const Duration(hours: 2)), + moveTime: moveTime, + isChanged: false, + isStarted: false, + scheduleSpareTime: spareTime, + scheduleNote: note, + latenessTime: 0, + ); +} + +MonthlySchedulesState _createMockStateWithSchedules() { + final today = DateTime.now(); + final todayKey = DateTime(today.year, today.month, today.day); + + final mockSchedules = { + todayKey: [ + _createMockSchedule( + id: "schedule-1", + name: "Morning Meeting", + scheduleTime: DateTime(today.year, today.month, today.day, 9, 30), + place: _createMockPlace(id: "place-1", name: "Conference Room A"), + ), + _createMockSchedule( + id: "schedule-2", + name: "Lunch with Client", + scheduleTime: DateTime(today.year, today.month, today.day, 12, 30), + place: _createMockPlace(id: "place-2", name: "Restaurant Downtown"), + ), + ], + DateTime(today.year, today.month, today.day + 1): [ + _createMockSchedule( + id: "schedule-3", + name: "Project Review", + scheduleTime: DateTime(today.year, today.month, today.day + 1, 14, 0), + place: _createMockPlace(id: "place-3", name: "Office Building"), + ), + ], + DateTime(today.year, today.month, today.day + 2): [ + _createMockSchedule( + id: "schedule-4", + name: "Doctor Appointment", + scheduleTime: DateTime(today.year, today.month, today.day + 2, 10, 0), + place: _createMockPlace(id: "place-4", name: "Seoul Hospital"), + ), + _createMockSchedule( + id: "schedule-5", + name: "Gym Session", + scheduleTime: DateTime(today.year, today.month, today.day + 2, 18, 0), + place: _createMockPlace(id: "place-5", name: "Fitness Center"), + ), + ], + }; + + return MonthlySchedulesState( + status: MonthlySchedulesStatus.success, + schedules: mockSchedules, + startDate: DateTime(today.year, today.month, 1), + endDate: DateTime(today.year, today.month + 1, 0), + ); +} + +MonthlySchedulesState _createEmptyMockState() { + final today = DateTime.now(); + return MonthlySchedulesState( + status: MonthlySchedulesStatus.success, + schedules: const {}, + startDate: DateTime(today.year, today.month, 1), + endDate: DateTime(today.year, today.month + 1, 0), + ); +} + +MonthlySchedulesState _createLoadingMockState() { + return const MonthlySchedulesState( + status: MonthlySchedulesStatus.loading, + schedules: {}, + ); +} + +@widgetbook.UseCase( + name: 'With Multiple Schedules', + type: HomeScreenContent, +) +Widget homeScreenContentWithSchedulesUseCase(BuildContext context) { + return Scaffold( + body: HomeScreenContent( + state: _createMockStateWithSchedules(), + userScore: 85.0, + ), + ); +} + +@widgetbook.UseCase( + name: 'Empty State', + type: HomeScreenContent, +) +Widget homeScreenContentEmptyUseCase(BuildContext context) { + return Scaffold( + body: HomeScreenContent( + state: _createEmptyMockState(), + userScore: 75.0, + ), + ); +} + +@widgetbook.UseCase( + name: 'Loading State', + type: HomeScreenContent, +) +Widget homeScreenContentLoadingUseCase(BuildContext context) { + return Scaffold( + body: HomeScreenContent( + state: _createLoadingMockState(), + userScore: 90.0, + ), + ); +} + +@widgetbook.UseCase( + name: 'Today Only Schedule', + type: HomeScreenContent, +) +Widget homeScreenContentTodayOnlyUseCase(BuildContext context) { + final today = DateTime.now(); + final todayKey = DateTime(today.year, today.month, today.day); + + final mockState = MonthlySchedulesState( + status: MonthlySchedulesStatus.success, + schedules: { + todayKey: [ + _createMockSchedule( + name: "Important Meeting", + scheduleTime: DateTime(today.year, today.month, today.day, 15, 0), + place: _createMockPlace(name: "Conference Room"), + ), + ], + }, + startDate: DateTime(today.year, today.month, 1), + endDate: DateTime(today.year, today.month + 1, 0), + ); + + return Scaffold( + body: HomeScreenContent( + state: mockState, + userScore: 95.0, + ), + ); +}