2026-01-31 15:23:18 +05:30
|
|
|
|
// lib/screens/calendar_screen.dart
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:intl/intl.dart';
|
2026-03-18 16:28:32 +05:30
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
2026-01-31 15:23:18 +05:30
|
|
|
|
import '../features/events/services/events_service.dart';
|
|
|
|
|
|
import '../features/events/models/event_models.dart';
|
|
|
|
|
|
import 'learn_more_screen.dart';
|
|
|
|
|
|
import '../core/app_decoration.dart';
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
// landscape_section_header no longer needed for this screen
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
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,
|
2026-03-14 08:55:21 +05:30
|
|
|
|
mainAxisSpacing: 4,
|
|
|
|
|
|
crossAxisSpacing: 4,
|
|
|
|
|
|
childAspectRatio: 0.78,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
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);
|
2026-03-14 08:55:21 +05:30
|
|
|
|
final eventCount = _dateCounts[key] ?? 0;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
final isSelected = selectedDate.year == cellDate.year && selectedDate.month == cellDate.month && selectedDate.day == cellDate.day;
|
2026-03-14 08:55:21 +05:30
|
|
|
|
final isToday = cellDate.year == DateTime.now().year && cellDate.month == DateTime.now().month && cellDate.day == DateTime.now().day;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
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,
|
2026-03-14 08:55:21 +05:30
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
children: [
|
|
|
|
|
|
// rounded date cell
|
|
|
|
|
|
Container(
|
2026-03-14 08:55:21 +05:30
|
|
|
|
width: 32,
|
|
|
|
|
|
height: 32,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
decoration: BoxDecoration(
|
2026-03-14 08:55:21 +05:30
|
|
|
|
color: isSelected
|
|
|
|
|
|
? primaryColor
|
|
|
|
|
|
: isToday
|
|
|
|
|
|
? primaryColor.withOpacity(0.12)
|
|
|
|
|
|
: Colors.transparent,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
|
),
|
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
'$dayIndex',
|
|
|
|
|
|
style: TextStyle(
|
2026-03-14 08:55:21 +05:30
|
|
|
|
fontWeight: (isSelected || isToday) ? FontWeight.w700 : FontWeight.w500,
|
|
|
|
|
|
color: isSelected
|
|
|
|
|
|
? Colors.white
|
|
|
|
|
|
: isToday
|
|
|
|
|
|
? primaryColor
|
|
|
|
|
|
: dayTextColor,
|
|
|
|
|
|
fontSize: 13,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
else
|
2026-03-14 08:55:21 +05:30
|
|
|
|
const SizedBox(height: 5),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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)),
|
2026-03-18 16:28:32 +05:30
|
|
|
|
child: imgUrl != null
|
|
|
|
|
|
? CachedNetworkImage(
|
|
|
|
|
|
imageUrl: imgUrl,
|
perf: add memCacheWidth/memCacheHeight to all thumbnail images
All CachedNetworkImage instances in list/card contexts now decode at
2x rendered size instead of full resolution. A 3000x2000 event photo
previously decoded to ~24MB in GPU memory even when shown at 96px —
now decodes to <1MB.
Affected screens (16 CachedNetworkImage instances total):
- home_screen.dart: hero (800w), top card (300w), stacked (192w),
horizontal (440x360), full-width (800x400), search (112x112),
filter sheet (160x160), type icons (112x112)
- home_desktop_screen.dart: mini (128x128), grid (600x280), horiz (600x296)
- calendar_screen.dart: event card (400x300)
- profile_screen.dart: avatar (size*2), event tile (120x120)
learn_more_screen.dart intentionally unchanged — full-res for detail view.
Estimated memory reduction: ~500MB → ~30MB for a typical home screen.
2026-03-20 22:26:52 +05:30
|
|
|
|
memCacheWidth: 400,
|
|
|
|
|
|
memCacheHeight: 300,
|
2026-03-18 16:28:32 +05:30
|
|
|
|
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)),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
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))),
|
|
|
|
|
|
]),
|
|
|
|
|
|
]),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
// ── 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))),
|
|
|
|
|
|
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]),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final width = MediaQuery.of(context).size.width;
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
final isLandscape = width >= 820;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
// ── LANDSCAPE layout ──────────────────────────────────────────────────
|
|
|
|
|
|
if (isLandscape) {
|
2026-01-31 15:23:18 +05:30
|
|
|
|
return Scaffold(
|
|
|
|
|
|
backgroundColor: theme.scaffoldBackgroundColor,
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
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),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
// ── MOBILE layout ─────────────────────────────────────────────────────
|
|
|
|
|
|
// (unchanged from original)
|
2026-01-31 15:23:18 +05:30
|
|
|
|
return Scaffold(
|
|
|
|
|
|
backgroundColor: theme.scaffoldBackgroundColor,
|
|
|
|
|
|
body: Stack(
|
|
|
|
|
|
children: [
|
2026-03-18 17:16:38 +05:30
|
|
|
|
// TOP APP BAR stays fixed (title + bell icon)
|
2026-01-31 15:23:18 +05:30
|
|
|
|
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),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
2026-03-18 17:16:38 +05:30
|
|
|
|
// CONTENT: gradient + calendar card scroll together as one unit
|
2026-03-18 16:39:48 +05:30
|
|
|
|
CustomScrollView(
|
|
|
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
|
|
|
slivers: [
|
2026-03-18 17:16:38 +05:30
|
|
|
|
// 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),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-18 16:39:48 +05:30
|
|
|
|
|
|
|
|
|
|
// 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)),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|