Files
App/lib/widgets/account/event_calendar.dart
LittleSheep e6c58b7b63 👽 Update API to microservices
♻️ Refactor router pushes
2025-07-17 14:35:09 +08:00

146 lines
5.0 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/activity.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) {
var checkInResult =
events.whereType<SnCheckInResult>().firstOrNull;
if (checkInResult != null) {
return Positioned(
top: 32,
child: Text(
'checkInResultT${checkInResult.level}'.tr(),
style: TextStyle(
fontSize: 9,
color:
isSameDay(selectedDay.value, day)
? Theme.of(context).colorScheme.onPrimaryContainer
: isSameDay(DateTime.now(), day)
? Theme.of(
context,
).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
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;
}
}