// lib/screens/calendar_screen.dart import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../features/events/services/events_service.dart'; import '../features/events/models/event_models.dart'; import 'learn_more_screen.dart'; import '../core/app_decoration.dart'; class CalendarScreen extends StatefulWidget { const CalendarScreen({Key? key}) : super(key: key); @override State createState() => _CalendarScreenState(); } class _CalendarScreenState extends State { DateTime visibleMonth = DateTime.now(); DateTime selectedDate = DateTime.now(); final EventsService _service = EventsService(); bool _loadingMonth = true; bool _loadingDay = false; final Set _markedDates = {}; final Map _dateCounts = {}; final Map> _dateThumbnails = {}; List _eventsOfDay = []; // Scroll controller for the calendar grid final ScrollController _calendarGridController = ScrollController(); static const List monthNames = [ '', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; @override void initState() { super.initState(); _loadMonth(visibleMonth); selectedDate = DateTime.now(); WidgetsBinding.instance.addPostFrameCallback((_) => _onSelectDate(_ymKey(selectedDate))); } @override void dispose() { _calendarGridController.dispose(); super.dispose(); } Future _loadMonth(DateTime dt) async { setState(() { _loadingMonth = true; _markedDates.clear(); _dateCounts.clear(); _dateThumbnails.clear(); _eventsOfDay = []; }); final monthName = DateFormat.MMMM().format(dt); final year = dt.year; try { final res = await _service.getEventsByMonthYear(monthName, year); final datesRaw = res['dates']; if (datesRaw is List) { for (final d in datesRaw) { if (d is String) _markedDates.add(d); } } final dateEvents = res['date_events']; if (dateEvents is List) { for (final item in dateEvents) { if (item is Map) { final k = item['date_of_event']?.toString(); final cnt = item['events_of_date']; if (k != null) { _dateCounts[k] = (cnt is int) ? cnt : int.tryParse(cnt?.toString() ?? '0') ?? 0; } } } } if (_markedDates.isNotEmpty) { await _fetchThumbnailsForDates(_markedDates.toList()); } } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); } finally { if (mounted) setState(() => _loadingMonth = false); } } Future _fetchThumbnailsForDates(List dates) async { for (final date in dates) { try { final events = await _service.getEventsForDate(date); final thumbs = []; for (final e in events) { String? url; if (e.thumbImg != null && e.thumbImg!.trim().isNotEmpty) { url = e.thumbImg!.trim(); } else if (e.images.isNotEmpty && e.images.first.image.trim().isNotEmpty) { url = e.images.first.image.trim(); } if (url != null && url.isNotEmpty) thumbs.add(url); if (thumbs.length >= 3) break; } if (thumbs.isNotEmpty) { if (mounted) { setState(() => _dateThumbnails[date] = thumbs); } else { _dateThumbnails[date] = thumbs; } } } catch (_) { // ignore per-date errors } } } Future _onSelectDate(String yyyyMMdd) async { setState(() { _loadingDay = true; _eventsOfDay = []; final parts = yyyyMMdd.split('-'); if (parts.length == 3) { final y = int.tryParse(parts[0]) ?? DateTime.now().year; final m = int.tryParse(parts[1]) ?? DateTime.now().month; final d = int.tryParse(parts[2]) ?? DateTime.now().day; selectedDate = DateTime(y, m, d); } }); try { final events = await _service.getEventsForDate(yyyyMMdd); if (mounted) setState(() => _eventsOfDay = events); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); } finally { if (mounted) setState(() => _loadingDay = false); } } void _prevMonth() { setState(() => visibleMonth = DateTime(visibleMonth.year, visibleMonth.month - 1, 1)); _loadMonth(visibleMonth); } void _nextMonth() { setState(() => visibleMonth = DateTime(visibleMonth.year, visibleMonth.month + 1, 1)); _loadMonth(visibleMonth); } int _daysInMonth(DateTime d) { final next = (d.month == 12) ? DateTime(d.year + 1, 1, 1) : DateTime(d.year, d.month + 1, 1); return next.subtract(const Duration(days: 1)).day; } int _firstWeekdayOfMonth(DateTime d) { final first = DateTime(d.year, d.month, 1); return first.weekday; // 1=Mon ... 7=Sun } String _ymKey(DateTime d) => '${d.year.toString().padLeft(4, "0")}-${d.month.toString().padLeft(2, "0")}-${d.day.toString().padLeft(2, "0")}'; // show a premium modal sheet with years 2020..2050 in a 3-column grid, scrollable & draggable Future _showYearPicker(BuildContext context) async { final startYear = 2020; final endYear = 2050; final years = List.generate(endYear - startYear + 1, (i) => startYear + i); await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) { final theme = Theme.of(ctx); return DraggableScrollableSheet( initialChildSize: 0.55, minChildSize: 0.32, maxChildSize: 0.95, expand: false, builder: (context, scrollController) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 20, offset: Offset(0, 8))], ), child: Column( children: [ // header Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 8.0), child: Row( children: [ const SizedBox(width: 4), Expanded(child: Text('Select Year', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700))), IconButton( icon: Icon(Icons.close, color: theme.hintColor), onPressed: () => Navigator.of(ctx).pop(), ) ], ), ), const SizedBox(height: 8), // grid (scrollable using the passed scrollController) Expanded( child: GridView.builder( controller: scrollController, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, // 3 columns -> premium style mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 2.4, ), itemCount: years.length, itemBuilder: (context, index) { final y = years[index]; final isSelected = visibleMonth.year == y; return InkWell( onTap: () { setState(() { visibleMonth = DateTime(y, visibleMonth.month, 1); }); _loadMonth(visibleMonth); Navigator.of(ctx).pop(); }, borderRadius: BorderRadius.circular(10), child: Container( alignment: Alignment.center, decoration: BoxDecoration( color: isSelected ? theme.colorScheme.primary.withOpacity(0.12) : null, borderRadius: BorderRadius.circular(10), border: isSelected ? Border.all(color: theme.colorScheme.primary, width: 1.6) : Border.all(color: Colors.transparent), boxShadow: isSelected ? [BoxShadow(color: theme.colorScheme.primary.withOpacity(0.06), blurRadius: 10, offset: Offset(0, 4))] : null, ), child: Text( '$y', style: theme.textTheme.bodyLarge?.copyWith( fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600, color: isSelected ? theme.colorScheme.primary : theme.textTheme.bodyLarge?.color, fontSize: 16, ), ), ), ); }, ), ), ], ), ); }, ); }, ); } Widget _buildMonthYearHeader(BuildContext context, Color primaryColor) { final theme = Theme.of(context); return Row( children: [ IconButton(icon: Icon(Icons.chevron_left, color: primaryColor), onPressed: _prevMonth, splashRadius: 20), Expanded( child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ // plain month label (no dropdown) Text( monthNames[visibleMonth.month], style: const TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(width: 10), // year "dropdown" replaced with premium modal trigger InkWell( onTap: () => _showYearPicker(context), borderRadius: BorderRadius.circular(6), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), child: Row( children: [ Text( '${visibleMonth.year}', style: const TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(width: 6), Icon(Icons.arrow_drop_down, size: 22, color: theme.textTheme.bodyLarge?.color), ], ), ), ), ], ), ), ), IconButton(icon: Icon(Icons.chevron_right, color: primaryColor), onPressed: _nextMonth, splashRadius: 20), ], ); } /// Calendar card that shows full rows (including overflow prev/next month dates). Widget _calendarCard(BuildContext context) { final theme = Theme.of(context); final primaryColor = theme.colorScheme.primary; final weekdayShorts = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; // compute rows to guarantee rows * 7 cells final daysInMonth = _daysInMonth(visibleMonth); final firstWeekday = _firstWeekdayOfMonth(visibleMonth); final leadingEmpty = firstWeekday - 1; final totalCells = leadingEmpty + daysInMonth; final rows = (totalCells / 7).ceil(); final totalItems = rows * 7; // first date shown (may be prev month's date) final firstCellDate = DateTime(visibleMonth.year, visibleMonth.month, 1).subtract(Duration(days: leadingEmpty)); return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 8, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0), child: Column( mainAxisSize: MainAxisSize.min, // tightly wrap grid children: [ // month/year header SizedBox(height: 48, child: _buildMonthYearHeader(context, primaryColor)), const SizedBox(height: 6), // weekday labels row SizedBox( height: 22, child: Row( children: List.generate(7, (i) { final isWeekend = (i == 5 || i == 6); return Expanded( child: Center( child: Text( weekdayShorts[i], style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: isWeekend ? Colors.redAccent.withOpacity(0.9) : (Theme.of(context).brightness == Brightness.dark ? Colors.white70 : Colors.black54), ), ), ), ); }), ), ), const SizedBox(height: 8), // GRID: shrinkWrap true ensures all rows are rendered GridView.builder( shrinkWrap: true, controller: _calendarGridController, physics: const NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: totalItems, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 7, mainAxisSpacing: 8, crossAxisSpacing: 8, childAspectRatio: 1, ), itemBuilder: (context, index) { final cellDate = firstCellDate.add(Duration(days: index)); final inCurrentMonth = cellDate.month == visibleMonth.month && cellDate.year == visibleMonth.year; final dayIndex = cellDate.day; final key = _ymKey(cellDate); final hasEvents = _markedDates.contains(key); final thumbnails = _dateThumbnails[key] ?? []; final isSelected = selectedDate.year == cellDate.year && selectedDate.month == cellDate.month && selectedDate.day == cellDate.day; final dayTextColor = inCurrentMonth ? (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black87) : (Theme.of(context).brightness == Brightness.dark ? Colors.white38 : Colors.grey.shade400); return GestureDetector( onTap: () { if (!inCurrentMonth) { setState(() => visibleMonth = DateTime(cellDate.year, cellDate.month, 1)); _loadMonth(visibleMonth); } _onSelectDate(key); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // rounded date cell Container( width: 36, height: 36, decoration: BoxDecoration( color: isSelected ? primaryColor.withOpacity(0.14) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: Text( '$dayIndex', style: TextStyle( fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600, color: isSelected ? primaryColor : dayTextColor, fontSize: 14, ), ), ), const SizedBox(height: 6), // small event indicators (thumbnail overlap or dot) if (hasEvents && thumbnails.isNotEmpty) SizedBox( height: 14, child: Row( mainAxisSize: MainAxisSize.min, children: thumbnails.take(2).toList().asMap().entries.map((entry) { final i = entry.key; final url = entry.value; return Transform.translate( offset: Offset(i * -6.0, 0), child: Container( margin: const EdgeInsets.only(left: 4), width: 14, height: 14, decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: Theme.of(context).cardColor, width: 1.0)), child: ClipOval(child: Image.network(url, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(color: Theme.of(context).dividerColor))), ), ); }).toList(), ), ) else if (hasEvents) Container(width: 18, height: 6, decoration: BoxDecoration(color: primaryColor, borderRadius: BorderRadius.circular(6))) else const SizedBox.shrink(), ], ), ); }, ), ], ), ), ), ); } // Selected-date summary (now guaranteed outside and below the calendar card) Widget _selectedDateSummary(BuildContext context) { final theme = Theme.of(context); final shortWeekday = DateFormat('EEEE').format(selectedDate); final shortDate = DateFormat('d MMMM, yyyy').format(selectedDate); final dayNumber = selectedDate.day; final monthLabel = DateFormat('MMM').format(selectedDate).toUpperCase(); final eventsCount = _eventsOfDay.length; final primaryColor = theme.colorScheme.primary; return Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 8), child: Card( elevation: 6, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 8.0), child: Row( children: [ Container( width: 56, height: 56, decoration: BoxDecoration(color: primaryColor.withOpacity(0.12), borderRadius: BorderRadius.circular(10)), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('$dayNumber', style: theme.textTheme.headlineSmall?.copyWith(color: primaryColor, fontWeight: FontWeight.bold, fontSize: 18)), const SizedBox(height: 2), Text(monthLabel, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 11, fontWeight: FontWeight.w700)), ], ), ), const SizedBox(width: 10), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(shortWeekday, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, fontSize: 14)), const SizedBox(height: 2), Text(shortDate, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 12)), ]), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('$eventsCount', style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold, color: primaryColor, fontSize: 16)), const SizedBox(height: 2), Text(eventsCount == 1 ? 'Event' : 'Events', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 12)), ], ), ], ), ), ), ); } // Mobile event card (kept unchanged) Widget _eventCardMobile(EventModel e) { final theme = Theme.of(context); final imgUrl = (e.thumbImg != null && e.thumbImg!.isNotEmpty) ? e.thumbImg! : (e.images.isNotEmpty ? e.images.first.image : null); final dateLabel = (e.startDate != null && e.endDate != null && e.startDate == e.endDate) ? '${e.startDate}' : (e.startDate != null && e.endDate != null ? '${e.startDate} - ${e.endDate}' : (e.startDate ?? '')); return GestureDetector( onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))), child: Card( elevation: 6, margin: const EdgeInsets.fromLTRB(20, 10, 20, 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(14)), child: imgUrl != null ? Image.network(imgUrl, height: 150, width: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(height: 150, color: theme.dividerColor)) : Container(height: 150, color: theme.dividerColor, child: Icon(Icons.event, size: 44, color: theme.hintColor)), ), Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 14), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(e.title ?? e.name ?? '', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: 10), Row(children: [ Container(width: 26, height: 26, decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(6)), child: Icon(Icons.calendar_today, size: 14, color: theme.colorScheme.primary)), const SizedBox(width: 8), Expanded(child: Text(dateLabel, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))), ]), const SizedBox(height: 8), Row(children: [ Container(width: 26, height: 26, decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(6)), child: Icon(Icons.location_on, size: 14, color: theme.colorScheme.primary)), const SizedBox(width: 8), Expanded(child: Text(e.place ?? '', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))), ]), ]), ), ], ), ), ); } @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; final isMobile = width < 700; final theme = Theme.of(context); // For non-mobile, keep original split layout if (!isMobile) { return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, body: SafeArea( child: Row( children: [ Expanded(flex: 3, child: Padding(padding: const EdgeInsets.all(24.0), child: _calendarCard(context))), Expanded(flex: 1, child: _detailsPanel()), ], ), ), ); } // MOBILE layout // Stack: extended gradient panel (below appbar) that visually extends behind the calendar. return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, body: Stack( children: [ // Extended blue gradient panel behind calendar (matches reference) Positioned( top: 0, left: 0, right: 0, child: Container( height: 260, // controls how much gradient shows behind calendar decoration: AppDecoration.blueGradient.copyWith( borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30)), boxShadow: [const BoxShadow(color: Colors.black12, blurRadius: 12, offset: Offset(0, 6))], ), // leave child empty — title and bell are placed above child: const SizedBox.shrink(), ), ), // TOP APP BAR (title centered + notification at top-right) - unchanged placement Positioned( top: 0, left: 0, right: 0, child: SafeArea( bottom: false, child: SizedBox( height: 56, // app bar height child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Stack( alignment: Alignment.center, children: [ // centered title Text( "Event's Calendar", style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, ), ), // notification icon at absolute top-right Positioned( right: 0, child: InkWell( onTap: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Notifications (demo)'))), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.white24, borderRadius: BorderRadius.circular(10), ), child: const Icon(Icons.notifications_none, color: Colors.white), ), ), ), ], ), ), ), ), ), // CONTENT: calendar card overlapped on gradient, then summary and list Column( children: [ const SizedBox(height: 110), // leave space for appbar + some gradient top _calendarCard(context), // calendar card sits visually on top of the gradient _selectedDateSummary(context), Expanded( child: _loadingDay ? Center(child: CircularProgressIndicator(color: theme.colorScheme.primary)) : _eventsOfDay.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.event_available, size: 48, color: theme.hintColor), const SizedBox(height: 10), Text('No events scheduled for this date', style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)), ], ), ) : ListView.builder( padding: const EdgeInsets.only(top: 6, bottom: 32), itemCount: _eventsOfDay.length, itemBuilder: (context, idx) => _eventCardMobile(_eventsOfDay[idx]), ), ), ], ), ], ), ); } Widget _detailsPanel() { final theme = Theme.of(context); final shortDate = DateFormat('EEE, d MMM').format(selectedDate); final eventsCount = _eventsOfDay.length; Widget _buildHeaderCompact() { return Padding( padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0), child: Row( children: [ Container( width: 48, height: 48, decoration: AppDecoration.blueGradientRounded(10), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Text('${selectedDate.day}', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 2), Text(monthNames[selectedDate.month].substring(0, 3), style: const TextStyle(color: Colors.white70, fontSize: 10))]), ), const SizedBox(width: 12), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(shortDate, style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 4), Text('$eventsCount ${eventsCount == 1 ? "Event" : "Events"}', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).hintColor))]), const Spacer(), IconButton(onPressed: () {}, icon: Icon(Icons.more_horiz, color: Theme.of(context).iconTheme.color)), ], ), ); } return Container( color: Theme.of(context).scaffoldBackgroundColor, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeaderCompact(), Divider(height: 1, color: theme.dividerColor), Expanded( child: _loadingDay ? Center(child: CircularProgressIndicator(color: theme.colorScheme.primary)) : _eventsOfDay.isEmpty ? const SizedBox.shrink() : ListView.builder(padding: const EdgeInsets.only(top: 6, bottom: 24), itemCount: _eventsOfDay.length, itemBuilder: (context, idx) => _eventCardMobile(_eventsOfDay[idx])), ) ]), ); } }