2026-01-31 15:23:18 +05:30
|
|
|
// lib/screens/home_screen.dart
|
|
|
|
|
import 'dart:async';
|
|
|
|
|
import 'dart:ui';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
|
|
|
|
|
|
import '../features/events/models/event_models.dart';
|
|
|
|
|
import '../features/events/services/events_service.dart';
|
|
|
|
|
import 'calendar_screen.dart';
|
|
|
|
|
import 'profile_screen.dart';
|
|
|
|
|
import 'contribute_screen.dart';
|
|
|
|
|
import 'learn_more_screen.dart';
|
|
|
|
|
import 'search_screen.dart';
|
|
|
|
|
import '../core/app_decoration.dart';
|
2026-03-11 20:13:13 +05:30
|
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
|
|
|
const HomeScreen({Key? key}) : super(key: key);
|
|
|
|
|
@override
|
|
|
|
|
_HomeScreenState createState() => _HomeScreenState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Main screen that hosts 4 tabs in an IndexedStack (Home, Calendar, Contribute, Profile).
|
|
|
|
|
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
|
|
|
|
|
int _selectedIndex = 0;
|
|
|
|
|
String _username = '';
|
|
|
|
|
String _location = '';
|
|
|
|
|
String _pincode = 'all';
|
|
|
|
|
|
|
|
|
|
final EventsService _eventsService = EventsService();
|
|
|
|
|
|
|
|
|
|
// backend-driven
|
|
|
|
|
List<EventModel> _events = [];
|
|
|
|
|
List<EventTypeModel> _types = [];
|
|
|
|
|
int _selectedTypeId = -1; // -1 == All
|
|
|
|
|
|
|
|
|
|
bool _loading = true;
|
|
|
|
|
|
|
|
|
|
// Hero carousel
|
|
|
|
|
final PageController _heroPageController = PageController();
|
|
|
|
|
int _heroCurrentPage = 0;
|
|
|
|
|
Timer? _autoScrollTimer;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_loadUserDataAndEvents();
|
|
|
|
|
_startAutoScroll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_autoScrollTimer?.cancel();
|
|
|
|
|
_heroPageController.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _startAutoScroll() {
|
|
|
|
|
_autoScrollTimer?.cancel();
|
|
|
|
|
_autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
|
|
|
|
|
if (_heroEvents.isEmpty) return;
|
|
|
|
|
final nextPage = (_heroCurrentPage + 1) % _heroEvents.length;
|
|
|
|
|
if (_heroPageController.hasClients) {
|
|
|
|
|
_heroPageController.animateToPage(
|
|
|
|
|
nextPage,
|
|
|
|
|
duration: const Duration(milliseconds: 500),
|
|
|
|
|
curve: Curves.easeInOut,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _loadUserDataAndEvents() async {
|
|
|
|
|
setState(() => _loading = true);
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
|
_username = prefs.getString('display_name') ?? prefs.getString('username') ?? '';
|
|
|
|
|
_location = prefs.getString('location') ?? 'Whitefield, Bengaluru';
|
|
|
|
|
_pincode = prefs.getString('pincode') ?? 'all';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final types = await _events_service_getEventTypesSafe();
|
|
|
|
|
final events = await _events_service_getEventsSafe(_pincode);
|
|
|
|
|
if (mounted) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_types = types;
|
|
|
|
|
_events = events;
|
|
|
|
|
_selectedTypeId = -1;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
if (mounted) setState(() => _loading = false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<List<EventTypeModel>> _events_service_getEventTypesSafe() async {
|
|
|
|
|
try {
|
|
|
|
|
return await _eventsService.getEventTypes();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return <EventTypeModel>[];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<List<EventModel>> _events_service_getEventsSafe(String pincode) async {
|
|
|
|
|
try {
|
|
|
|
|
return await _eventsService.getEventsByPincode(pincode);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return <EventModel>[];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _refresh() async {
|
|
|
|
|
await _loadUserDataAndEvents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _bookEventAtIndex(int index) {
|
|
|
|
|
if (index >= 0 && index < _events.length) {
|
|
|
|
|
setState(() => _events.removeAt(index));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _categoryChip({
|
|
|
|
|
required String label,
|
|
|
|
|
required bool selected,
|
|
|
|
|
required VoidCallback onTap,
|
2026-03-11 20:13:13 +05:30
|
|
|
String? imageUrl,
|
2026-01-31 15:23:18 +05:30
|
|
|
IconData? icon,
|
|
|
|
|
}) {
|
|
|
|
|
final theme = Theme.of(context);
|
2026-03-11 20:13:13 +05:30
|
|
|
return GestureDetector(
|
2026-01-31 15:23:18 +05:30
|
|
|
onTap: onTap,
|
|
|
|
|
child: Container(
|
2026-03-11 20:13:13 +05:30
|
|
|
width: 110,
|
2026-01-31 15:23:18 +05:30
|
|
|
decoration: BoxDecoration(
|
2026-03-11 20:13:13 +05:30
|
|
|
color: selected ? theme.colorScheme.primary : Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(18),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: selected
|
|
|
|
|
? theme.colorScheme.primary.withOpacity(0.35)
|
|
|
|
|
: Colors.black.withOpacity(0.06),
|
|
|
|
|
blurRadius: selected ? 12 : 8,
|
|
|
|
|
offset: const Offset(0, 4),
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
child: Column(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
2026-01-31 15:23:18 +05:30
|
|
|
children: [
|
2026-03-11 20:13:13 +05:30
|
|
|
// Image / Icon area
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 56,
|
|
|
|
|
width: 56,
|
|
|
|
|
child: imageUrl != null && imageUrl.isNotEmpty
|
|
|
|
|
? ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
child: Image.network(
|
|
|
|
|
imageUrl,
|
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
|
errorBuilder: (_, __, ___) => Icon(
|
|
|
|
|
icon ?? Icons.category,
|
|
|
|
|
size: 36,
|
|
|
|
|
color: selected ? Colors.white : theme.colorScheme.primary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: Icon(
|
|
|
|
|
icon ?? Icons.category,
|
|
|
|
|
size: 36,
|
|
|
|
|
color: selected ? Colors.white : theme.colorScheme.primary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
// Label
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
|
|
|
|
child: Text(
|
|
|
|
|
label,
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
color: selected
|
|
|
|
|
? Colors.white
|
|
|
|
|
: theme.textTheme.bodyLarge?.color ?? Colors.black87,
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _openLocationSearch() async {
|
|
|
|
|
final selected = await Navigator.of(context).push(PageRouteBuilder(
|
|
|
|
|
opaque: false,
|
|
|
|
|
pageBuilder: (context, animation, secondaryAnimation) => const SearchScreen(),
|
|
|
|
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
|
|
|
return FadeTransition(opacity: animation, child: child);
|
|
|
|
|
},
|
|
|
|
|
transitionDuration: const Duration(milliseconds: 220),
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
if (selected != null && selected is String) {
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
|
await prefs.setString('location', selected);
|
|
|
|
|
setState(() {
|
|
|
|
|
_location = selected;
|
|
|
|
|
});
|
|
|
|
|
await _refresh();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _openEventSearch() {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
context: context,
|
|
|
|
|
isScrollControlled: true,
|
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
|
builder: (ctx) {
|
|
|
|
|
return DraggableScrollableSheet(
|
|
|
|
|
expand: false,
|
|
|
|
|
initialChildSize: 0.6,
|
|
|
|
|
minChildSize: 0.3,
|
|
|
|
|
maxChildSize: 0.95,
|
|
|
|
|
builder: (context, scrollController) {
|
|
|
|
|
String query = '';
|
|
|
|
|
List<EventModel> results = List.from(_events);
|
|
|
|
|
return StatefulBuilder(builder: (context, setModalState) {
|
|
|
|
|
void _onQueryChanged(String v) {
|
|
|
|
|
query = v.trim().toLowerCase();
|
|
|
|
|
final r = _events.where((e) {
|
|
|
|
|
final title = (e.title ?? e.name ?? '').toLowerCase();
|
|
|
|
|
return title.contains(query);
|
|
|
|
|
}).toList();
|
|
|
|
|
setModalState(() {
|
|
|
|
|
results = r;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.cardColor,
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
),
|
|
|
|
|
child: SafeArea(
|
|
|
|
|
top: false,
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
controller: scrollController,
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Center(
|
|
|
|
|
child: Container(
|
|
|
|
|
width: 48,
|
|
|
|
|
height: 6,
|
|
|
|
|
decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(6)),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Container(
|
|
|
|
|
height: 48,
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
|
|
|
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: theme.dividerColor)),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.search, color: theme.hintColor),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: TextField(
|
|
|
|
|
style: theme.textTheme.bodyLarge,
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
hintText: 'Search events by name',
|
|
|
|
|
hintStyle: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor),
|
|
|
|
|
border: InputBorder.none,
|
|
|
|
|
),
|
|
|
|
|
autofocus: true,
|
|
|
|
|
onChanged: _onQueryChanged,
|
|
|
|
|
textInputAction: TextInputAction.search,
|
|
|
|
|
onSubmitted: (v) => _onQueryChanged(v),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: Icon(Icons.close, color: theme.iconTheme.color),
|
|
|
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
if (_loading)
|
|
|
|
|
Center(child: CircularProgressIndicator(color: theme.colorScheme.primary))
|
|
|
|
|
else if (results.isEmpty)
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
|
|
|
|
child: Center(child: Text(query.isEmpty ? 'Type to search events' : 'No events found', style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor))),
|
|
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
ListView.separated(
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
|
|
|
itemBuilder: (ctx, idx) {
|
|
|
|
|
final ev = results[idx];
|
|
|
|
|
final img = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty) ? ev.thumbImg! : (ev.images.isNotEmpty ? ev.images.first.image : null);
|
|
|
|
|
final title = ev.title ?? ev.name ?? '';
|
|
|
|
|
final subtitle = ev.startDate ?? '';
|
|
|
|
|
return ListTile(
|
|
|
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),
|
|
|
|
|
leading: img != null && img.isNotEmpty
|
|
|
|
|
? ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.network(img, width: 56, height: 56, fit: BoxFit.cover))
|
|
|
|
|
: Container(width: 56, height: 56, decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(8)), child: Icon(Icons.event, color: theme.hintColor)),
|
|
|
|
|
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge),
|
|
|
|
|
subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)),
|
|
|
|
|
onTap: () {
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
if (ev.id != null) {
|
|
|
|
|
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id)));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
separatorBuilder: (_, __) => Divider(color: theme.dividerColor),
|
|
|
|
|
itemCount: results.length,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
backgroundColor: theme.scaffoldBackgroundColor,
|
|
|
|
|
body: Stack(
|
|
|
|
|
children: [
|
|
|
|
|
// IndexedStack keeps each tab alive and preserves state.
|
|
|
|
|
IndexedStack(
|
|
|
|
|
index: _selectedIndex,
|
|
|
|
|
children: [
|
|
|
|
|
_buildHomeContent(), // index 0
|
|
|
|
|
const CalendarScreen(), // index 1
|
|
|
|
|
const ContributeScreen(), // index 2 (full page, scrollable)
|
|
|
|
|
const ProfileScreen(), // index 3
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Floating bottom navigation (always visible)
|
|
|
|
|
Positioned(
|
|
|
|
|
left: 16,
|
|
|
|
|
right: 16,
|
|
|
|
|
bottom: 16,
|
|
|
|
|
child: _buildFloatingBottomNav(),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildFloatingBottomNav() {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
return Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.cardColor,
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
boxShadow: [BoxShadow(color: theme.shadowColor.withOpacity(0.06), blurRadius: 12)],
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
|
|
|
children: [
|
2026-03-11 20:13:13 +05:30
|
|
|
_bottomNavItem(
|
|
|
|
|
0,
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.home,
|
|
|
|
|
color: _selectedIndex == 0
|
|
|
|
|
? theme.colorScheme.primary
|
|
|
|
|
: theme.iconTheme.color,
|
|
|
|
|
),
|
|
|
|
|
'Home',
|
|
|
|
|
),
|
|
|
|
|
_bottomNavItem(
|
|
|
|
|
1,
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.calendar_today,
|
|
|
|
|
color: _selectedIndex == 1
|
|
|
|
|
? theme.colorScheme.primary
|
|
|
|
|
: theme.iconTheme.color,
|
|
|
|
|
),
|
|
|
|
|
'Calendar',
|
|
|
|
|
),
|
|
|
|
|
_bottomNavItem(
|
|
|
|
|
2,
|
|
|
|
|
SvgPicture.asset(
|
|
|
|
|
'assets/icon/hand_stop.svg',
|
|
|
|
|
height: 24,
|
|
|
|
|
width: 24,
|
|
|
|
|
colorFilter: ColorFilter.mode(
|
|
|
|
|
_selectedIndex == 2
|
|
|
|
|
? theme.colorScheme.primary
|
|
|
|
|
: theme.iconTheme.color!,
|
|
|
|
|
BlendMode.srcIn,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
'Contribute',
|
|
|
|
|
),
|
|
|
|
|
_bottomNavItem(
|
|
|
|
|
3,
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.person,
|
|
|
|
|
color: _selectedIndex == 3
|
|
|
|
|
? theme.colorScheme.primary
|
|
|
|
|
: theme.iconTheme.color,
|
|
|
|
|
),
|
|
|
|
|
'Profile',
|
|
|
|
|
),
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
Widget _bottomNavItem(int index, Widget icon, String label) {
|
2026-01-31 15:23:18 +05:30
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
bool active = _selectedIndex == index;
|
2026-03-11 20:13:13 +05:30
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
_selectedIndex = index;
|
|
|
|
|
});
|
|
|
|
|
},
|
2026-03-11 20:13:13 +05:30
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: active
|
|
|
|
|
? theme.colorScheme.primary.withOpacity(0.08)
|
|
|
|
|
: Colors.transparent,
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
),
|
|
|
|
|
child: icon,
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
Text(
|
|
|
|
|
label,
|
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
color: active
|
|
|
|
|
? theme.colorScheme.primary
|
|
|
|
|
: theme.iconTheme.color,
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
// Get hero events (first 4 events for the carousel)
|
|
|
|
|
List<EventModel> get _heroEvents => _events.take(4).toList();
|
|
|
|
|
|
|
|
|
|
Widget _buildHomeContent() {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
|
|
|
|
// Get current hero event image for full-screen blurred background
|
|
|
|
|
String? currentBgImage;
|
|
|
|
|
if (_heroEvents.isNotEmpty && _heroCurrentPage < _heroEvents.length) {
|
|
|
|
|
final event = _heroEvents[_heroCurrentPage];
|
|
|
|
|
if (event.thumbImg != null && event.thumbImg!.isNotEmpty) {
|
|
|
|
|
currentBgImage = event.thumbImg;
|
|
|
|
|
} else if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) {
|
|
|
|
|
currentBgImage = event.images.first.image;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Stack(
|
|
|
|
|
children: [
|
|
|
|
|
// Full-screen blurred background of current event image OR the AppDecoration blue gradient if no image
|
|
|
|
|
Positioned.fill(
|
|
|
|
|
child: currentBgImage != null
|
|
|
|
|
? Image.network(
|
|
|
|
|
currentBgImage,
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
errorBuilder: (context, error, stackTrace) => Container(decoration: AppDecoration.blueGradient),
|
|
|
|
|
)
|
|
|
|
|
: Container(
|
|
|
|
|
decoration: AppDecoration.blueGradient,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Blur overlay on background (applies both when an image is present and when using the blue gradient)
|
|
|
|
|
Positioned.fill(
|
|
|
|
|
child: BackdropFilter(
|
|
|
|
|
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
|
|
|
|
|
child: Container(
|
|
|
|
|
color: Colors.black.withOpacity(0.15),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Hero section with cards
|
|
|
|
|
_buildHeroSection(),
|
|
|
|
|
|
|
|
|
|
// Draggable bottom sheet
|
|
|
|
|
DraggableScrollableSheet(
|
|
|
|
|
initialChildSize: 0.28,
|
|
|
|
|
minChildSize: 0.22,
|
|
|
|
|
maxChildSize: 0.92,
|
|
|
|
|
builder: (context, scrollController) {
|
|
|
|
|
return Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.scaffoldBackgroundColor,
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: Colors.black.withOpacity(0.15),
|
|
|
|
|
blurRadius: 20,
|
|
|
|
|
offset: const Offset(0, -5),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: _buildSheetContent(scrollController),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildHeroSection() {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
|
|
|
|
// 0.5 cm gap approximation in logical pixels (approx. 32)
|
|
|
|
|
const double gapBetweenLocationAndHero = 32.0;
|
|
|
|
|
|
|
|
|
|
return SafeArea(
|
|
|
|
|
bottom: false,
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
// Top bar with location and search
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
// Location pill
|
|
|
|
|
GestureDetector(
|
|
|
|
|
onTap: _openLocationSearch,
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white.withOpacity(0.2),
|
|
|
|
|
borderRadius: BorderRadius.circular(25),
|
|
|
|
|
border: Border.all(color: Colors.white.withOpacity(0.3)),
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
const Icon(Icons.location_on_outlined, color: Colors.white, size: 18),
|
|
|
|
|
const SizedBox(width: 6),
|
|
|
|
|
Text(
|
|
|
|
|
_location.length > 20 ? '${_location.substring(0, 20)}...' : _location,
|
|
|
|
|
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
const Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 18),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// Search button
|
|
|
|
|
GestureDetector(
|
|
|
|
|
onTap: _openEventSearch,
|
|
|
|
|
child: Container(
|
|
|
|
|
width: 48,
|
|
|
|
|
height: 48,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white.withOpacity(0.2),
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
border: Border.all(color: Colors.white.withOpacity(0.3)),
|
|
|
|
|
),
|
|
|
|
|
child: const Icon(Icons.search, color: Colors.white, size: 24),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// 0.5 cm gap (approx. 32 logical pixels)
|
|
|
|
|
const SizedBox(height: gapBetweenLocationAndHero),
|
|
|
|
|
|
|
|
|
|
// Hero image carousel (PageView) and fixed indicators under it.
|
|
|
|
|
_heroEvents.isEmpty
|
|
|
|
|
? SizedBox(
|
2026-03-11 20:13:13 +05:30
|
|
|
height: 240,
|
2026-01-31 15:23:18 +05:30
|
|
|
child: Center(
|
|
|
|
|
child: _loading ? const CircularProgressIndicator(color: Colors.white) : const Text('No events available', style: TextStyle(color: Colors.white70)),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: Column(
|
|
|
|
|
children: [
|
|
|
|
|
// PageView with only the images/titles
|
|
|
|
|
SizedBox(
|
2026-03-11 20:13:13 +05:30
|
|
|
height: 300,
|
2026-01-31 15:23:18 +05:30
|
|
|
child: PageView.builder(
|
|
|
|
|
controller: _heroPageController,
|
|
|
|
|
onPageChanged: (page) {
|
|
|
|
|
setState(() => _heroCurrentPage = page);
|
|
|
|
|
},
|
|
|
|
|
itemCount: _heroEvents.length,
|
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
|
return _buildHeroEventImage(_heroEvents[index]);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// fixed indicators (outside PageView)
|
2026-03-11 20:13:13 +05:30
|
|
|
const SizedBox(height: 20),
|
2026-01-31 15:23:18 +05:30
|
|
|
SizedBox(
|
2026-03-11 20:13:13 +05:30
|
|
|
height: 20,
|
2026-01-31 15:23:18 +05:30
|
|
|
child: Center(
|
|
|
|
|
child: AnimatedBuilder(
|
|
|
|
|
animation: _heroPageController,
|
|
|
|
|
builder: (context, child) {
|
|
|
|
|
double page = _heroCurrentPage.toDouble();
|
|
|
|
|
if (_heroPageController.hasClients) {
|
|
|
|
|
page = _heroPageController.page ?? page;
|
|
|
|
|
}
|
|
|
|
|
return Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: List.generate(_heroEvents.length, (i) {
|
|
|
|
|
final dx = (i - page).abs();
|
2026-03-11 20:13:13 +05:30
|
|
|
final t = 1.0 - dx.clamp(0.0, 1.0);
|
|
|
|
|
final width = 7 + (24 - 7) * t;
|
2026-01-31 15:23:18 +05:30
|
|
|
final opacity = 0.35 + (0.65 * t);
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () {
|
|
|
|
|
if (_heroPageController.hasClients) {
|
|
|
|
|
_heroPageController.animateToPage(
|
|
|
|
|
i,
|
|
|
|
|
duration: const Duration(milliseconds: 300),
|
|
|
|
|
curve: Curves.easeInOut,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
child: Container(
|
2026-03-11 20:13:13 +05:30
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 5),
|
2026-01-31 15:23:18 +05:30
|
|
|
width: width,
|
2026-03-11 20:13:13 +05:30
|
|
|
height: 7,
|
2026-01-31 15:23:18 +05:30
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white.withOpacity(opacity),
|
2026-03-11 20:13:13 +05:30
|
|
|
borderRadius: BorderRadius.circular(4),
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// spacing so the sheet handle doesn't overlap the indicator
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
/// Build a hero image card with the image only (rounded),
|
|
|
|
|
/// and the title text placed below the image.
|
2026-01-31 15:23:18 +05:30
|
|
|
Widget _buildHeroEventImage(EventModel event) {
|
|
|
|
|
String? img;
|
|
|
|
|
if (event.thumbImg != null && event.thumbImg!.isNotEmpty) {
|
|
|
|
|
img = event.thumbImg;
|
|
|
|
|
} else if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) {
|
|
|
|
|
img = event.images.first.image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final radius = 24.0;
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () {
|
|
|
|
|
if (event.id != null) {
|
|
|
|
|
Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id)));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
2026-03-11 20:13:13 +05:30
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
// Image only (no text overlay)
|
|
|
|
|
Expanded(
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(radius),
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
child: img != null && img.isNotEmpty
|
|
|
|
|
? Image.network(
|
|
|
|
|
img,
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
|
|
|
Container(decoration: AppDecoration.blueGradientRounded(radius)),
|
|
|
|
|
)
|
|
|
|
|
: Container(
|
|
|
|
|
decoration: AppDecoration.blueGradientRounded(radius),
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
// Title text outside the image
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
Text(
|
|
|
|
|
event.title ?? event.name ?? '',
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
fontSize: 22,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
height: 1.2,
|
|
|
|
|
shadows: [
|
|
|
|
|
Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)),
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
),
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildSheetContent(ScrollController scrollController) {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
|
|
|
|
return ListView(
|
|
|
|
|
controller: scrollController,
|
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
children: [
|
|
|
|
|
// Drag handle and "see more" text
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
// Arrow up icon
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.keyboard_arrow_up,
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
size: 28,
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
'see more',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// "Events Around You" header
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 16),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Events Around You',
|
|
|
|
|
style: theme.textTheme.titleLarge?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
fontSize: 22,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
TextButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
// View all action
|
|
|
|
|
},
|
|
|
|
|
child: Text(
|
|
|
|
|
'View All',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
// Category chips (card-style)
|
2026-01-31 15:23:18 +05:30
|
|
|
SizedBox(
|
2026-03-11 20:13:13 +05:30
|
|
|
height: 140,
|
2026-01-31 15:23:18 +05:30
|
|
|
child: ListView(
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
|
|
|
children: [
|
|
|
|
|
_categoryChip(
|
|
|
|
|
label: 'All Events',
|
|
|
|
|
icon: Icons.grid_view_rounded,
|
|
|
|
|
selected: _selectedTypeId == -1,
|
|
|
|
|
onTap: () => _onSelectType(-1),
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
const SizedBox(width: 12),
|
2026-01-31 15:23:18 +05:30
|
|
|
for (final t in _types) ...[
|
|
|
|
|
_categoryChip(
|
|
|
|
|
label: t.name,
|
2026-03-11 20:13:13 +05:30
|
|
|
imageUrl: t.iconUrl,
|
2026-01-31 15:23:18 +05:30
|
|
|
icon: _getIconForType(t.name),
|
|
|
|
|
selected: _selectedTypeId == t.id,
|
|
|
|
|
onTap: () => _onSelectType(t.id),
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
const SizedBox(width: 12),
|
2026-01-31 15:23:18 +05:30
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
// --- NEW: when All Events is active, show only "types that have events"
|
|
|
|
|
if (_selectedTypeId == -1) ...[
|
|
|
|
|
if (_loading)
|
|
|
|
|
const Padding(
|
|
|
|
|
padding: EdgeInsets.all(40),
|
|
|
|
|
child: Center(child: CircularProgressIndicator()),
|
|
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
for (final t in _types)
|
|
|
|
|
if (_events.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[
|
|
|
|
|
_buildTypeSection(t),
|
|
|
|
|
const SizedBox(height: 18),
|
|
|
|
|
],
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
] else ...[
|
|
|
|
|
// Selected a specific type -> show filtered events in vertical list (full cards)
|
|
|
|
|
if (_loading)
|
|
|
|
|
const Padding(
|
|
|
|
|
padding: EdgeInsets.all(40),
|
|
|
|
|
child: Center(child: CircularProgressIndicator()),
|
|
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: _events.map((e) => _buildFullWidthCard(e)).toList(),
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
// Bottom padding for nav bar
|
|
|
|
|
const SizedBox(height: 100),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
/// Build a type section that follows your requested layout rules:
|
|
|
|
|
/// - If type has <= 5 events => single horizontal row of compact cards.
|
|
|
|
|
/// - If type has >= 6 events => arrange events into column groups of 3 (so visually there are 3 rows across horizontally scrollable columns).
|
|
|
|
|
Widget _buildTypeSection(EventTypeModel type) {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
final eventsForType = _events.where((e) => e.eventTypeId == type.id).toList();
|
|
|
|
|
final n = eventsForType.length;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
// Header row
|
|
|
|
|
Widget header = Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
Text(type.name, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
|
|
|
|
|
TextButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
_onSelectType(type.id);
|
|
|
|
|
},
|
|
|
|
|
child: Text('View All', style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600)),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
// If <= 5 events: show one horizontal row using _buildHorizontalEventCard
|
|
|
|
|
if (n <= 5) {
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
header,
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 290, // card height: image 180 + text ~110
|
|
|
|
|
child: ListView.separated(
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
|
|
|
itemBuilder: (ctx, idx) => _buildHorizontalEventCard(eventsForType[idx]),
|
|
|
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
|
|
|
itemCount: eventsForType.length,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
2026-01-31 15:23:18 +05:30
|
|
|
}
|
2026-03-11 20:13:13 +05:30
|
|
|
|
|
|
|
|
// For 6+ events: arrange into columns where each column has up to 3 stacked cards.
|
|
|
|
|
final columnsCount = (n / 3).ceil();
|
|
|
|
|
final columnWidth = 260.0; // narrower so second column peeks in
|
|
|
|
|
final verticalCardHeight = 120.0; // each stacked card height matches sample
|
|
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
header,
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
|
|
|
|
// Container height must accommodate 3 stacked cards + small gaps
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: (verticalCardHeight * 3) + 16, // 3 cards + spacing
|
|
|
|
|
child: ListView.separated(
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
|
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
|
|
|
itemBuilder: (ctx, colIndex) {
|
|
|
|
|
// Build one column: contains up to 3 items: indices colIndex*3 + 0/1/2
|
|
|
|
|
return Container(
|
|
|
|
|
width: columnWidth,
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
// top card
|
|
|
|
|
if ((colIndex * 3 + 0) < n)
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: verticalCardHeight,
|
|
|
|
|
child: _buildStackedCard(eventsForType[colIndex * 3 + 0]),
|
|
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
const SizedBox(height: 0),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
// middle card
|
|
|
|
|
if ((colIndex * 3 + 1) < n)
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: verticalCardHeight,
|
|
|
|
|
child: _buildStackedCard(eventsForType[colIndex * 3 + 1]),
|
|
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
const SizedBox(height: 0),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
// bottom card
|
|
|
|
|
if ((colIndex * 3 + 2) < n)
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: verticalCardHeight,
|
|
|
|
|
child: _buildStackedCard(eventsForType[colIndex * 3 + 2]),
|
|
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
const SizedBox(height: 0),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
itemCount: columnsCount,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
2026-01-31 15:23:18 +05:30
|
|
|
}
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
/// A stacked card styled to match your sample (left square thumbnail, bold title).
|
|
|
|
|
/// REMOVED: price/rating row (per your request).
|
|
|
|
|
Widget _buildStackedCard(EventModel e) {
|
2026-01-31 15:23:18 +05:30
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
String? img;
|
|
|
|
|
if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
|
|
|
|
|
img = e.thumbImg;
|
|
|
|
|
} else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
|
|
|
|
|
img = e.images.first.image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () {
|
2026-03-11 20:13:13 +05:30
|
|
|
if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
|
2026-01-31 15:23:18 +05:30
|
|
|
},
|
|
|
|
|
child: Container(
|
2026-03-11 20:13:13 +05:30
|
|
|
margin: const EdgeInsets.symmetric(vertical: 0),
|
2026-01-31 15:23:18 +05:30
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.cardColor,
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
2026-03-11 20:13:13 +05:30
|
|
|
boxShadow: [BoxShadow(color: theme.shadowColor.withOpacity(0.06), blurRadius: 12, offset: const Offset(0, 8))],
|
|
|
|
|
),
|
|
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
// thumbnail square (rounded)
|
|
|
|
|
ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
child: img != null && img.isNotEmpty
|
|
|
|
|
? Image.network(img, width: 96, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(width: 96, color: theme.dividerColor))
|
|
|
|
|
: Container(width: 96, height: double.infinity, color: theme.dividerColor),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 14),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [
|
|
|
|
|
Text(e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 18)),
|
|
|
|
|
// removed price/rating row here per request
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
// optional heart icon aligned top-right
|
|
|
|
|
Icon(Icons.favorite_border, color: theme.hintColor),
|
2026-01-31 15:23:18 +05:30
|
|
|
],
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Compact card used inside the one-row layout for small counts (<=5).
|
|
|
|
|
/// Matches Figma: vertical card with image, date badge, title, location, "Free".
|
|
|
|
|
Widget _buildHorizontalEventCard(EventModel e) {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
String? img;
|
|
|
|
|
if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
|
|
|
|
|
img = e.thumbImg;
|
|
|
|
|
} else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
|
|
|
|
|
img = e.images.first.image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse day & month for the date badge
|
|
|
|
|
String day = '';
|
|
|
|
|
String month = '';
|
|
|
|
|
try {
|
|
|
|
|
final parts = e.startDate.split('-');
|
|
|
|
|
if (parts.length == 3) {
|
|
|
|
|
day = int.parse(parts[2]).toString();
|
|
|
|
|
const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
|
|
|
|
|
month = months[int.parse(parts[1]) - 1];
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
final venue = e.venueName ?? e.place ?? '';
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () {
|
|
|
|
|
if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
|
|
|
|
|
},
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
width: 220,
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// Image with date badge
|
|
|
|
|
Stack(
|
|
|
|
|
children: [
|
|
|
|
|
ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(18),
|
|
|
|
|
child: img != null && img.isNotEmpty
|
|
|
|
|
? Image.network(
|
|
|
|
|
img,
|
|
|
|
|
width: 220,
|
|
|
|
|
height: 180,
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
errorBuilder: (_, __, ___) => Container(
|
|
|
|
|
width: 220,
|
|
|
|
|
height: 180,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
borderRadius: BorderRadius.circular(18),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(Icons.image, size: 40, color: theme.hintColor),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: Container(
|
|
|
|
|
width: 220,
|
|
|
|
|
height: 180,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
borderRadius: BorderRadius.circular(18),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(Icons.image, size: 40, color: theme.hintColor),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// Date badge
|
|
|
|
|
if (day.isNotEmpty)
|
|
|
|
|
Positioned(
|
|
|
|
|
top: 10,
|
|
|
|
|
right: 10,
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: Colors.black.withOpacity(0.08),
|
|
|
|
|
blurRadius: 6,
|
|
|
|
|
offset: const Offset(0, 2),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
day,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
color: Colors.black87,
|
|
|
|
|
height: 1.1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
month,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
color: Colors.black54,
|
|
|
|
|
height: 1.2,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
// Title
|
|
|
|
|
Text(
|
|
|
|
|
e.title ?? e.name ?? '',
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
if (venue.isNotEmpty) ...[
|
|
|
|
|
const SizedBox(height: 4),
|
2026-01-31 15:23:18 +05:30
|
|
|
Text(
|
2026-03-11 20:13:13 +05:30
|
|
|
venue,
|
|
|
|
|
maxLines: 1,
|
2026-01-31 15:23:18 +05:30
|
|
|
overflow: TextOverflow.ellipsis,
|
2026-03-11 20:13:13 +05:30
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
],
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
Text(
|
|
|
|
|
'Free',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Format a date string (YYYY-MM-DD) to short display like "4 Mar".
|
|
|
|
|
String _formatDateShort(String dateStr) {
|
|
|
|
|
try {
|
|
|
|
|
final parts = dateStr.split('-');
|
|
|
|
|
if (parts.length == 3) {
|
|
|
|
|
final day = int.parse(parts[2]);
|
|
|
|
|
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
|
|
|
final month = months[int.parse(parts[1]) - 1];
|
|
|
|
|
return '$day $month';
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
return dateStr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Full width card used when a single type is selected (vertical list).
|
|
|
|
|
/// Matches Figma: large image, badge, title, date + venue.
|
|
|
|
|
Widget _buildFullWidthCard(EventModel e) {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
String? img;
|
|
|
|
|
if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
|
|
|
|
|
img = e.thumbImg;
|
|
|
|
|
} else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
|
|
|
|
|
img = e.images.first.image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build date range string
|
|
|
|
|
final startShort = _formatDateShort(e.startDate);
|
|
|
|
|
final endShort = _formatDateShort(e.endDate);
|
|
|
|
|
final dateRange = startShort == endShort ? startShort : '$startShort - $endShort';
|
|
|
|
|
|
|
|
|
|
final venue = e.venueName ?? e.place ?? '';
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () {
|
|
|
|
|
if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
|
|
|
|
|
},
|
|
|
|
|
child: Container(
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 18),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.cardColor,
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(color: theme.shadowColor.withOpacity(0.10), blurRadius: 16, offset: const Offset(0, 6)),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// Image with badge
|
|
|
|
|
Stack(
|
|
|
|
|
children: [
|
|
|
|
|
ClipRRect(
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
child: img != null && img.isNotEmpty
|
|
|
|
|
? Image.network(
|
|
|
|
|
img,
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
height: 200,
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
errorBuilder: (_, __, ___) => Container(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
height: 200,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(Icons.image, size: 48, color: theme.hintColor),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: Container(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
height: 200,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(Icons.image, size: 48, color: theme.hintColor),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// "ADDED BY EVENTIFY" badge
|
|
|
|
|
Positioned(
|
|
|
|
|
top: 14,
|
|
|
|
|
left: 14,
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: const [
|
|
|
|
|
Icon(Icons.star, color: Colors.white, size: 14),
|
|
|
|
|
SizedBox(width: 4),
|
|
|
|
|
Text(
|
|
|
|
|
'ADDED BY EVENTIFY',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
letterSpacing: 0.5,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
// Title + date/venue
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
e.title ?? e.name ?? '',
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
fontSize: 17,
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
maxLines: 2,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.calendar_today_outlined, size: 14, color: theme.hintColor),
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
Text(
|
|
|
|
|
dateRange,
|
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
if (venue.isNotEmpty) ...[
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Icon(Icons.location_on_outlined, size: 14, color: theme.hintColor),
|
|
|
|
|
const SizedBox(width: 3),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Text(
|
|
|
|
|
venue,
|
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
),
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
IconData _getIconForType(String typeName) {
|
|
|
|
|
final name = typeName.toLowerCase();
|
|
|
|
|
if (name.contains('music')) return Icons.music_note;
|
|
|
|
|
if (name.contains('art') || name.contains('comedy')) return Icons.palette;
|
|
|
|
|
if (name.contains('festival')) return Icons.celebration;
|
|
|
|
|
if (name.contains('heritage') || name.contains('history')) return Icons.account_balance;
|
|
|
|
|
if (name.contains('sport')) return Icons.sports;
|
|
|
|
|
if (name.contains('food')) return Icons.restaurant;
|
|
|
|
|
return Icons.event;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onSelectType(int id) async {
|
|
|
|
|
setState(() {
|
|
|
|
|
_selectedTypeId = id;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final all = await _eventsService.getEventsByPincode(_pincode);
|
|
|
|
|
final filtered = id == -1 ? all : all.where((e) => e.eventTypeId == id).toList();
|
|
|
|
|
if (mounted) setState(() => _events = filtered);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
String _getShortEmailLabel() {
|
|
|
|
|
try {
|
|
|
|
|
final parts = _username.split('@');
|
|
|
|
|
if (parts.isNotEmpty && parts[0].isNotEmpty) return parts[0];
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
return 'You';
|
|
|
|
|
}
|
|
|
|
|
}
|