Files
Eventify-frontend/lib/screens/calendar_screen.dart
Sicherhaven bc12fe70aa security: sanitize all error messages shown to users
Created centralized userFriendlyError() utility that converts raw
exceptions into clean, user-friendly messages. Strips hostnames,
ports, OS error codes, HTTP status codes, stack traces, and Django
field names. Maps network/timeout/auth/server errors to plain
English messages.

Fixed 16 locations across 10 files:
- home_screen, calendar_screen, learn_more_screen (SnackBar/Text)
- login_screen, desktop_login_screen (SnackBar)
- profile_screen, contribute_screen, search_screen (SnackBar)
- review_form, review_section (inline error text)
- gamification_provider (error field)

Also removed double-wrapped exceptions in ReviewService (rethrow
instead of throw Exception('Failed to...: $e')).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:15:02 +05:30

935 lines
38 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// lib/screens/calendar_screen.dart
import 'package:flutter/material.dart';
import '../core/utils/error_utils.dart';
import 'package:intl/intl.dart';
import 'package:cached_network_image/cached_network_image.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';
// landscape_section_header no longer needed for this screen
class CalendarScreen extends StatefulWidget {
const CalendarScreen({Key? key}) : super(key: key);
@override
State<CalendarScreen> createState() => _CalendarScreenState();
}
class _CalendarScreenState extends State<CalendarScreen> {
DateTime visibleMonth = DateTime.now();
DateTime selectedDate = DateTime.now();
final EventsService _service = EventsService();
bool _loadingMonth = true;
bool _loadingDay = false;
final Set<String> _markedDates = {};
final Map<String, int> _dateCounts = {};
List<EventModel> _eventsOfDay = [];
// Scroll controller for the calendar grid
final ScrollController _calendarGridController = ScrollController();
static const List<String> 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<void> _loadMonth(DateTime dt) async {
setState(() {
_loadingMonth = true;
_markedDates.clear();
_dateCounts.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;
}
}
}
}
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
} finally {
if (mounted) setState(() => _loadingMonth = false);
}
}
Future<void> _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(userFriendlyError(e))));
} 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<void> _showYearPicker(BuildContext context) async {
final startYear = 2020;
final endYear = 2050;
final years = List<int>.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: 4,
crossAxisSpacing: 4,
childAspectRatio: 0.78,
),
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 eventCount = _dateCounts[key] ?? 0;
final isSelected = selectedDate.year == cellDate.year && selectedDate.month == cellDate.month && selectedDate.day == cellDate.day;
final isToday = cellDate.year == DateTime.now().year && cellDate.month == DateTime.now().month && cellDate.day == DateTime.now().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,
mainAxisSize: MainAxisSize.min,
children: [
// rounded date cell
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isSelected
? primaryColor
: isToday
? primaryColor.withOpacity(0.12)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
'$dayIndex',
style: TextStyle(
fontWeight: (isSelected || isToday) ? FontWeight.w700 : FontWeight.w500,
color: isSelected
? Colors.white
: isToday
? primaryColor
: dayTextColor,
fontSize: 13,
),
),
),
const SizedBox(height: 3),
// event indicator dots
if (hasEvents && inCurrentMonth)
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
eventCount.clamp(1, 3),
(i) => Container(
width: 5,
height: 5,
margin: EdgeInsets.only(left: i > 0 ? 2 : 0),
decoration: BoxDecoration(
color: isSelected
? primaryColor
: const Color(0xFFEF4444),
shape: BoxShape.circle,
),
),
),
)
else
const SizedBox(height: 5),
],
),
);
},
),
],
),
),
),
);
}
// 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, initialEvent: e))),
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
? CachedNetworkImage(
imageUrl: imgUrl,
memCacheWidth: 400,
memCacheHeight: 300,
height: 150,
width: double.infinity,
fit: BoxFit.cover,
placeholder: (_, __) => Container(height: 150, color: theme.dividerColor),
errorWidget: (_, __, ___) => 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))),
]),
]),
),
],
),
),
);
}
// ── Landscape: event card for the right panel ───────────────────────────
Widget _eventCardLandscape(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, initialEvent: e))),
child: Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 14),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(14),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 10, offset: const Offset(0, 3))],
),
child: Row(
children: [
// Image
ClipRRect(
borderRadius: const BorderRadius.horizontal(left: Radius.circular(14)),
child: imgUrl != null
? CachedNetworkImage(
imageUrl: imgUrl,
memCacheWidth: 300,
memCacheHeight: 300,
width: 100,
height: 100,
fit: BoxFit.cover,
placeholder: (_, __) => Container(width: 100, height: 100, color: theme.dividerColor),
errorWidget: (_, __, ___) => Container(
width: 100,
height: 100,
color: theme.dividerColor,
child: Icon(Icons.event, size: 32, color: theme.hintColor),
),
)
: Container(
width: 100,
height: 100,
color: theme.dividerColor,
child: Icon(Icons.event, size: 32, color: theme.hintColor),
),
),
// Content
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
e.title ?? e.name ?? '',
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
// Date row with blue dot
Row(children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF3B82F6),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(child: Text(dateLabel, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor), maxLines: 1, overflow: TextOverflow.ellipsis)),
]),
const SizedBox(height: 6),
// Venue row with green dot
Row(children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF22C55E),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(child: Text(e.place ?? '', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor), maxLines: 1, overflow: TextOverflow.ellipsis)),
]),
],
),
),
),
],
),
),
);
}
// ── Landscape: left panel content (calendar on white bg) ─────────────────
Widget _landscapeLeftPanel(BuildContext context) {
final theme = Theme.of(context);
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 20),
// Title
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
"Event's Calendar",
style: theme.textTheme.titleLarge?.copyWith(
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
),
),
),
const SizedBox(height: 12),
// Calendar card — reuses the mobile _calendarCard widget
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
children: [
_calendarCard(context),
if (_loadingMonth)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: LinearProgressIndicator(
color: theme.colorScheme.primary,
backgroundColor: theme.colorScheme.primary.withOpacity(0.12),
),
),
],
),
),
),
],
),
);
}
// ── Landscape: right panel (event list for selected day) ────────────────
Widget _landscapeRightPanel(BuildContext context) {
final theme = Theme.of(context);
final dayName = DateFormat('EEEE').format(selectedDate);
final dateFormatted = DateFormat('d MMMM, yyyy').format(selectedDate);
final count = _eventsOfDay.length;
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Date header matching Figma: "Monday, 16 June, 2025 — 2 Events"
Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$dayName, $dateFormatted',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
'$count ${count == 1 ? "Event" : "Events"}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.hintColor,
fontWeight: FontWeight.w500,
),
),
],
),
),
// Divider
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Divider(height: 1, color: theme.dividerColor),
),
const SizedBox(height: 12),
// Scrollable event list
Expanded(
child: _loadingDay
? Center(child: CircularProgressIndicator(color: theme.colorScheme.primary))
: _eventsOfDay.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.event_available, size: 56, color: theme.hintColor),
const SizedBox(height: 12),
Text(
'No events on this date',
style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor),
),
],
),
)
: ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(top: 4, bottom: 32),
itemCount: _eventsOfDay.length,
itemBuilder: (ctx, i) => _eventCardLandscape(_eventsOfDay[i]),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final isLandscape = width >= 820;
final theme = Theme.of(context);
// ── LANDSCAPE layout ──────────────────────────────────────────────────
if (isLandscape) {
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: Row(
children: [
// Left: Calendar panel with WHITE background (~60%)
Flexible(
flex: 3,
child: RepaintBoundary(
child: Container(
color: theme.cardColor,
child: _landscapeLeftPanel(context),
),
),
),
// Vertical divider between panels
VerticalDivider(width: 1, thickness: 1, color: theme.dividerColor),
// Right: Events panel (~40%)
Flexible(
flex: 2,
child: RepaintBoundary(
child: _landscapeRightPanel(context),
),
),
],
),
);
}
// ── MOBILE layout ─────────────────────────────────────────────────────
// (unchanged from original)
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: Stack(
children: [
// TOP APP BAR stays fixed (title + bell icon)
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: gradient + calendar card scroll together as one unit
CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// Gradient + calendar card in one scrollable Stack
// Gradient scrolls away with content; app bar remains fixed above
SliverToBoxAdapter(
child: Stack(
children: [
// Blue gradient banner — scrolls with content
Container(
height: 260,
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))],
),
),
// Calendar card starts at y=110 (after app bar), overlapping gradient
Padding(
padding: const EdgeInsets.only(top: 110),
child: _calendarCard(context),
),
],
),
),
// Selected date summary
SliverToBoxAdapter(child: _selectedDateSummary(context)),
// Events area — loading / empty / list
if (_loadingDay)
SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: CircularProgressIndicator(color: theme.colorScheme.primary),
),
)
else if (_eventsOfDay.isEmpty)
SliverFillRemaining(
hasScrollBody: false,
child: 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),
),
],
),
),
)
else
SliverList(
delegate: SliverChildBuilderDelegate(
(context, idx) => _eventCardMobile(_eventsOfDay[idx]),
childCount: _eventsOfDay.length,
),
),
// Bottom padding
const SliverToBoxAdapter(child: SizedBox(height: 32)),
],
),
],
),
);
}
}