180 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			180 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:easy_localization/easy_localization.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
						|
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
						|
import 'package:island/models/account.dart';
 | 
						|
import 'package:island/models/activity.dart';
 | 
						|
import 'package:material_symbols_icons/symbols.dart';
 | 
						|
import 'package:styled_widget/styled_widget.dart';
 | 
						|
import 'package:island/widgets/account/event_details_widget.dart';
 | 
						|
import 'package:table_calendar/table_calendar.dart';
 | 
						|
 | 
						|
/// A reusable widget for displaying an event calendar with event details
 | 
						|
/// This can be used in various places throughout the app
 | 
						|
class EventCalendarWidget extends HookConsumerWidget {
 | 
						|
  /// The list of calendar entries to display
 | 
						|
  final AsyncValue<List<SnEventCalendarEntry>> events;
 | 
						|
 | 
						|
  /// Initial date to focus on
 | 
						|
  final DateTime? initialDate;
 | 
						|
 | 
						|
  /// Whether to show the event details below the calendar
 | 
						|
  final bool showEventDetails;
 | 
						|
 | 
						|
  /// Whether to constrain the width of the calendar
 | 
						|
  final bool constrainWidth;
 | 
						|
 | 
						|
  /// Maximum width constraint when constrainWidth is true
 | 
						|
  final double maxWidth;
 | 
						|
 | 
						|
  /// Callback when a day is selected
 | 
						|
  final void Function(DateTime)? onDaySelected;
 | 
						|
 | 
						|
  /// Callback when the focused month changes
 | 
						|
  final void Function(int year, int month)? onMonthChanged;
 | 
						|
 | 
						|
  const EventCalendarWidget({
 | 
						|
    super.key,
 | 
						|
    required this.events,
 | 
						|
    this.initialDate,
 | 
						|
    this.showEventDetails = true,
 | 
						|
    this.constrainWidth = false,
 | 
						|
    this.maxWidth = 480,
 | 
						|
    this.onDaySelected,
 | 
						|
    this.onMonthChanged,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final selectedMonth = useState(initialDate?.month ?? DateTime.now().month);
 | 
						|
    final selectedYear = useState(initialDate?.year ?? DateTime.now().year);
 | 
						|
    final selectedDay = useState(initialDate ?? DateTime.now());
 | 
						|
 | 
						|
    final content = Column(
 | 
						|
      children: [
 | 
						|
        TableCalendar(
 | 
						|
          locale: EasyLocalization.of(context)!.locale.toString(),
 | 
						|
          firstDay: DateTime.now().add(Duration(days: -3650)),
 | 
						|
          lastDay: DateTime.now().add(Duration(days: 3650)),
 | 
						|
          focusedDay: DateTime.utc(
 | 
						|
            selectedYear.value,
 | 
						|
            selectedMonth.value,
 | 
						|
            selectedDay.value.day,
 | 
						|
          ),
 | 
						|
          weekNumbersVisible: false,
 | 
						|
          calendarFormat: CalendarFormat.month,
 | 
						|
          selectedDayPredicate: (day) {
 | 
						|
            return isSameDay(selectedDay.value, day);
 | 
						|
          },
 | 
						|
          onDaySelected: (value, _) {
 | 
						|
            selectedDay.value = value;
 | 
						|
            onDaySelected?.call(value);
 | 
						|
          },
 | 
						|
          onPageChanged: (focusedDay) {
 | 
						|
            selectedMonth.value = focusedDay.month;
 | 
						|
            selectedYear.value = focusedDay.year;
 | 
						|
            onMonthChanged?.call(focusedDay.year, focusedDay.month);
 | 
						|
          },
 | 
						|
          eventLoader: (day) {
 | 
						|
            return events.value
 | 
						|
                    ?.where((e) => isSameDay(e.date, day))
 | 
						|
                    .expand((e) => [...e.statuses, e.checkInResult])
 | 
						|
                    .where((e) => e != null)
 | 
						|
                    .toList() ??
 | 
						|
                [];
 | 
						|
          },
 | 
						|
          calendarBuilders: CalendarBuilders(
 | 
						|
            dowBuilder: (context, day) {
 | 
						|
              final text = DateFormat.EEEEE().format(day);
 | 
						|
              return Center(child: Text(text));
 | 
						|
            },
 | 
						|
            markerBuilder: (context, day, events) {
 | 
						|
              final checkInResult =
 | 
						|
                  events.whereType<SnCheckInResult>().firstOrNull;
 | 
						|
              final statuses = events.whereType<SnAccountStatus>().toList();
 | 
						|
 | 
						|
              final textColor =
 | 
						|
                  isSameDay(selectedDay.value, day)
 | 
						|
                      ? Colors.white
 | 
						|
                      : isSameDay(DateTime.now(), day)
 | 
						|
                      ? Colors.white
 | 
						|
                      : Theme.of(context).colorScheme.onSurface;
 | 
						|
 | 
						|
              final shadow =
 | 
						|
                  isSameDay(selectedDay.value, day) ||
 | 
						|
                          isSameDay(DateTime.now(), day)
 | 
						|
                      ? [
 | 
						|
                        Shadow(
 | 
						|
                          color: Colors.black.withOpacity(0.5),
 | 
						|
                          offset: const Offset(0, 1),
 | 
						|
                          blurRadius: 4,
 | 
						|
                        ),
 | 
						|
                      ]
 | 
						|
                      : null;
 | 
						|
 | 
						|
              if (checkInResult != null) {
 | 
						|
                return Positioned(
 | 
						|
                  top: 32,
 | 
						|
                  child: Row(
 | 
						|
                    spacing: 2,
 | 
						|
                    children: [
 | 
						|
                      Text(
 | 
						|
                        'checkInResultT${checkInResult.level}'.tr(),
 | 
						|
                        style: TextStyle(
 | 
						|
                          fontSize: 9,
 | 
						|
                          color: textColor,
 | 
						|
                          shadows: shadow,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                      if (statuses.isNotEmpty) ...[
 | 
						|
                        Icon(
 | 
						|
                          switch (statuses.first.attitude) {
 | 
						|
                            0 => Symbols.sentiment_satisfied,
 | 
						|
                            2 => Symbols.sentiment_dissatisfied,
 | 
						|
                            _ => Symbols.sentiment_neutral,
 | 
						|
                          },
 | 
						|
                          size: 12,
 | 
						|
                          color: textColor,
 | 
						|
                          shadows: shadow,
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                );
 | 
						|
              }
 | 
						|
              return null;
 | 
						|
            },
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
        if (showEventDetails) ...[
 | 
						|
          const Divider(height: 1).padding(top: 8),
 | 
						|
          AnimatedSwitcher(
 | 
						|
            duration: const Duration(milliseconds: 300),
 | 
						|
            child: Builder(
 | 
						|
              builder: (context) {
 | 
						|
                final event =
 | 
						|
                    events.value
 | 
						|
                        ?.where((e) => isSameDay(e.date, selectedDay.value))
 | 
						|
                        .firstOrNull;
 | 
						|
                return EventDetailsWidget(
 | 
						|
                  selectedDay: selectedDay.value,
 | 
						|
                  event: event,
 | 
						|
                );
 | 
						|
              },
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ],
 | 
						|
    );
 | 
						|
 | 
						|
    if (constrainWidth) {
 | 
						|
      return ConstrainedBox(
 | 
						|
        constraints: BoxConstraints(maxWidth: maxWidth),
 | 
						|
        child: Card(margin: EdgeInsets.all(16), child: content),
 | 
						|
      ).center();
 | 
						|
    }
 | 
						|
 | 
						|
    return content;
 | 
						|
  }
 | 
						|
}
 |