Files
Eventify-frontend/lib/screens/calendar_screen.dart
Sicherhaven 2c109f692c fix: replace Column+Expanded with CustomScrollView on calendar screen
The mobile calendar layout had a split-height bug where the event list
at the bottom was squeezed into whatever pixel crumbs remained after the
calendar card and summary bar consumed their fixed space. On small phones
or 6-row months (~390px calendar), the events area could shrink to under
100px — barely one card, with no way to scroll.

Fix: replace Column + Expanded(ListView) with a CustomScrollView using
slivers so the full page — calendar card, summary bar, and event cards —
scrolls as one unified surface. SliverFillRemaining handles loading and
empty states so they always fill the visible viewport naturally.
2026-03-18 16:39:48 +05:30

736 lines
30 KiB
Dart

// lib/screens/calendar_screen.dart
import 'package:flutter/material.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';
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(e.toString())));
} 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(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<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))),
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,
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))),
]),
]),
),
],
),
),
);
}
@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: whole page scrolls as one — calendar + summary + events
CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// Space for app bar + gradient top
const SliverToBoxAdapter(child: SizedBox(height: 110)),
// Calendar card
SliverToBoxAdapter(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)),
],
),
],
),
);
}
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])),
)
]),
);
}
}