Files
Eventify-frontend/lib/screens/home_screen.dart

947 lines
35 KiB
Dart
Raw Permalink Normal View History

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';
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,
IconData? icon,
}) {
final theme = Theme.of(context);
return InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onTap,
child: Container(
height: 40,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: selected ? theme.colorScheme.primary : theme.cardColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: selected ? theme.colorScheme.primary : theme.dividerColor,
width: 1,
),
boxShadow: selected
? [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
)
]
: [],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 16, color: selected ? Colors.white : theme.colorScheme.primary),
const SizedBox(width: 6),
],
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: selected ? Colors.white : theme.textTheme.bodyLarge?.color,
),
),
],
),
),
);
}
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: [
_bottomNavItem(0, Icons.home, 'Home'),
_bottomNavItem(1, Icons.calendar_today, 'Calendar'),
_bottomNavItem(2, Icons.volunteer_activism, 'Contribute'),
_bottomNavItem(3, Icons.person, 'Profile'),
],
),
);
}
Widget _bottomNavItem(int index, IconData icon, String label) {
final theme = Theme.of(context);
bool active = _selectedIndex == index;
return GestureDetector(
onTap: () {
setState(() {
_selectedIndex = index;
});
},
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(icon, color: active ? theme.colorScheme.primary : theme.iconTheme.color),
),
const SizedBox(height: 4),
Text(label, style: theme.textTheme.bodySmall?.copyWith(color: active ? theme.colorScheme.primary : theme.iconTheme.color, fontSize: 12)),
]),
);
}
// 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(
height: 360,
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(
height: 360,
child: PageView.builder(
controller: _heroPageController,
onPageChanged: (page) {
setState(() => _heroCurrentPage = page);
},
itemCount: _heroEvents.length,
itemBuilder: (context, index) {
return _buildHeroEventImage(_heroEvents[index]);
},
),
),
// fixed indicators (outside PageView)
const SizedBox(height: 12),
SizedBox(
height: 28,
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();
final t = 1.0 - dx.clamp(0.0, 1.0); // 1 when focused, 0 when far
final width = 10 + (36 - 10) * t; // interpolate between 10 and 36
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(
margin: const EdgeInsets.symmetric(horizontal: 8),
width: width,
height: 10,
decoration: BoxDecoration(
color: Colors.white.withOpacity(opacity),
borderRadius: BorderRadius.circular(6),
),
),
);
}),
);
},
),
),
),
// spacing so the sheet handle doesn't overlap the indicator
const SizedBox(height: 8),
],
),
],
),
);
}
/// Build a hero image card (image + gradient + title).
/// If there's no image, show the AppDecoration blue gradient rounded background
/// and a black overlay gradient for contrast.
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;
final startDate = event.startDate ?? '';
return GestureDetector(
onTap: () {
if (event.id != null) {
Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id)));
}
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Stack(
fit: StackFit.expand,
children: [
// If image available show it; otherwise use AppDecoration blue gradient.
if (img != null && img.isNotEmpty)
Image.network(
img,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(decoration: AppDecoration.blueGradientRounded(radius)),
)
else
Container(
decoration: AppDecoration.blueGradientRounded(radius),
),
// BLACK gradient overlay to darken bottom area for text (stronger to match your reference)
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.72), // strong black near bottom for contrast
Colors.black.withOpacity(0.38),
Colors.black.withOpacity(0.08), // subtle near top
],
stops: const [0.0, 0.45, 1.0],
),
),
),
// Title and date positioned bottom-left
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 18),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (startDate.isNotEmpty)
Text(
startDate,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
if (startDate.isNotEmpty) const SizedBox(height: 8),
Text(
event.title ?? event.name ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
height: 1.1,
shadows: [
Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)),
],
),
),
],
),
),
],
),
),
),
);
}
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,
),
),
),
],
),
),
// Category chips
SizedBox(
height: 48,
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),
),
const SizedBox(width: 10),
for (final t in _types) ...[
_categoryChip(
label: t.name,
icon: _getIconForType(t.name),
selected: _selectedTypeId == t.id,
onTap: () => _onSelectType(t.id),
),
const SizedBox(width: 10),
],
],
),
),
const SizedBox(height: 16),
// Event cards
if (_loading)
const Padding(
padding: EdgeInsets.all(40),
child: Center(child: CircularProgressIndicator()),
)
else if (_events.isEmpty)
Padding(
padding: const EdgeInsets.all(40),
child: Center(
child: Text(
'No events found',
style: theme.textTheme.bodyLarge?.copyWith(color: theme.hintColor),
),
),
)
else
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
for (int i = 0; i < _events.length; i++) ...[
_buildEventCard(_events[i], i),
],
],
),
),
// Bottom padding for nav bar
const SizedBox(height: 100),
],
);
}
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())));
}
}
Widget _buildEventCard(EventModel e, int index) {
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: () {
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(16),
boxShadow: [
BoxShadow(color: theme.shadowColor.withOpacity(0.12), blurRadius: 18, offset: const Offset(0, 8)),
BoxShadow(color: theme.shadowColor.withOpacity(0.04), blurRadius: 6, offset: const Offset(0, 2)),
],
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
child: img != null && img.isNotEmpty
? Image.network(img, fit: BoxFit.cover, width: double.infinity, height: 160)
: Image.asset('assets/images/event1.jpg', fit: BoxFit.cover, width: double.infinity, height: 160),
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(
e.title ?? e.name ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.calendar_today, size: 14, color: theme.colorScheme.primary),
const SizedBox(width: 6),
Flexible(
flex: 2,
child: Text(
'${e.startDate ?? ''}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withOpacity(0.9), fontSize: 13),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text('', style: TextStyle(color: theme.textTheme.bodySmall?.color?.withOpacity(0.4))),
),
Icon(Icons.location_on, size: 14, color: theme.colorScheme.primary),
const SizedBox(width: 6),
Flexible(
flex: 3,
child: Text(
e.place ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withOpacity(0.9), fontSize: 13),
),
),
],
),
]),
)
]),
),
);
}
String _getShortEmailLabel() {
try {
final parts = _username.split('@');
if (parts.isNotEmpty && parts[0].isNotEmpty) return parts[0];
} catch (_) {}
return 'You';
}
}