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

1043 lines
39 KiB
Dart
Raw Normal View History

2026-01-31 15:23:18 +05:30
import 'dart:async';
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 'booking_screen.dart';
import 'settings_screen.dart';
import 'learn_more_screen.dart';
import '../core/app_decoration.dart';
class HomeDesktopScreen extends StatefulWidget {
final bool skipSidebarEntranceAnimation;
const HomeDesktopScreen({Key? key, this.skipSidebarEntranceAnimation = false}) : super(key: key);
@override
State<HomeDesktopScreen> createState() => _HomeDesktopScreenState();
}
class _HomeDesktopScreenState extends State<HomeDesktopScreen> with SingleTickerProviderStateMixin {
final EventsService _eventsService = EventsService();
// Navigation state
int selectedMenu = 0; // 0 = Home, 1 = Calendar, 2 = Profile, 3 = Bookings, 4 = Contribute, 5 = Settings
// Backend-driven data for Home
List<EventModel> _events = [];
List<EventTypeModel> _types = [];
bool _loading = true;
bool _loadingTypes = true;
// Selection: either a backend id (event type) or a fixed label filter.
int _SelectedTypeId = -1;
String? _selectedTypeLabel;
// fixed categories required by product
final List<String> _fixedCategories = [
'All Events',
];
// User prefs
String _username = 'Guest';
String _location = 'Unknown';
String _pincode = 'all';
String? _profileImage;
// Sidebar text animation
late final AnimationController _sidebarTextController;
late final Animation<double> _sidebarTextOpacity;
// Sidebar width constant (used when computing main content width)
static const double _sidebarWidth = 220;
// Topbar compact breakpoint (content width)
static const double _compactTopBarWidth = 720;
// --- marquee (featured events) fields ---
late final ScrollController _marqueeController;
Timer? _marqueeTimer;
// Speed in pixels per second
double _marqueeSpeed = 40.0;
final Duration _marqueeTick = const Duration(milliseconds: 16);
@override
void initState() {
super.initState();
_sidebarTextController = AnimationController(vsync: this, duration: const Duration(milliseconds: 420));
_sidebarTextOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _sidebarTextController, curve: Curves.easeOut));
_marqueeController = ScrollController();
if (widget.skipSidebarEntranceAnimation) {
_sidebarTextController.value = 1.0;
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _sidebarTextController.forward();
});
}
// load initial data for home only
_loadPreferencesAndData();
}
@override
void dispose() {
_sidebarTextController.dispose();
_marqueeTimer?.cancel();
_marqueeController.dispose();
super.dispose();
}
// ------------------------ Data loaders ------------------------
Future<void> _loadPreferencesAndData() async {
setState(() {
_loading = true;
_loadingTypes = true;
});
final prefs = await SharedPreferences.getInstance();
// UI display name should come from display_name (profile). Backend identity is separate.
_username = prefs.getString('display_name') ?? prefs.getString('username') ?? 'Jane Doe';
_location = prefs.getString('location') ?? 'Whitefield, Bengaluru';
_pincode = prefs.getString('pincode') ?? 'all';
_profileImage = prefs.getString('profileImage');
await Future.wait([
_fetchEventTypes(),
_fetchEventsByPincode(_pincode),
]);
if (mounted) setState(() {
_loading = false;
_loadingTypes = false;
});
// start marquee when we have events
_restartMarquee();
}
Future<void> _fetchEventTypes() async {
try {
final types = await _eventsService.getEventTypes();
if (mounted) setState(() => _types = types);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to load categories: ${e.toString()}')));
}
}
Future<void> _fetchEventsByPincode(String pincode) async {
try {
final events = await _eventsService.getEventsByPincode(pincode);
if (mounted) setState(() => _events = events);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to load events: ${e.toString()}')));
}
// ensure marquee restarts when event list changes
_restartMarquee();
}
/// Public refresh entry used by UI (pull-to-refresh).
Future<void> _refreshHome() async {
// Prevent duplicate refresh triggers
if (_loading) return;
setState(() => _loading = true);
try {
await _loadPreferencesAndData();
} finally {
// _loadPreferencesAndData normally clears _loading, but ensure we reset if needed.
if (mounted && _loading) setState(() => _loading = false);
}
}
// ------------------------ Helpers ------------------------
String? _chooseEventImage(EventModel e) {
if (e.thumbImg != null && e.thumbImg!.trim().isNotEmpty) return e.thumbImg!.trim();
if (e.images.isNotEmpty && e.images.first.image.trim().isNotEmpty) return e.images.first.image.trim();
return null;
}
Widget _profileAvatar() {
// If profile image exists and is a network URL -> show it
if (_profileImage != null && _profileImage!.trim().isNotEmpty) {
final url = _profileImage!.trim();
if (url.startsWith('http')) {
return CircleAvatar(
radius: 20,
backgroundColor: Colors.grey.shade200,
backgroundImage: NetworkImage(url),
onBackgroundImageError: (_, __) {},
child: const Icon(Icons.person, color: Colors.transparent),
);
}
}
// Fallback → initials (clean & readable)
final name = _username.trim();
String initials = 'U';
if (name.isNotEmpty) {
if (name.contains('@')) {
// Email → first letter only
initials = name[0].toUpperCase();
} else {
final parts = name.split(' ').where((p) => p.isNotEmpty).toList();
initials = parts.isEmpty ? 'U' : parts.take(2).map((p) => p[0].toUpperCase()).join();
}
}
return CircleAvatar(
radius: 20,
backgroundColor: Colors.blue.shade600,
child: Text(
initials,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
}
// ------------------------ Marquee control ------------------------
void _restartMarquee() {
// stop previous timer
_marqueeTimer?.cancel();
if (_events.isEmpty) {
// reset position
if (_marqueeController.hasClients) _marqueeController.jumpTo(0);
return;
}
// Wait for next frame so scroll metrics are available (after the duplicated row is built)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (!_marqueeController.hasClients) {
// Try again next frame
WidgetsBinding.instance.addPostFrameCallback((_) => _restartMarquee());
return;
}
final maxScroll = _marqueeController.position.maxScrollExtent;
if (maxScroll <= 0) {
// Nothing to scroll
return;
}
// Start a periodic timer that increments offset smoothly
final tickSeconds = _marqueeTick.inMilliseconds / 1000.0;
final delta = _marqueeSpeed * tickSeconds;
_marqueeTimer?.cancel();
_marqueeTimer = Timer.periodic(_marqueeTick, (_) {
if (!mounted || !_marqueeController.hasClients) return;
final cur = _marqueeController.offset;
final max = _marqueeController.position.maxScrollExtent;
final half = max / 2.0; // because we duplicate the list twice
double next = cur + delta;
if (next >= max) {
// wrap back by half (seamless loop)
final wrapped = next - half;
_marqueeController.jumpTo(wrapped.clamp(0.0, max));
} else {
// small incremental jump gives smooth appearance
_marqueeController.jumpTo(next);
}
});
});
}
// ------------------------ Filtering logic ------------------------
List<EventModel> get _filteredEvents {
if (_selectedTypeLabel != null && _selectedTypeLabel!.toLowerCase() != 'all events') {
final label = _selectedTypeLabel!.toLowerCase();
return _events.where((e) {
final nameFromBackend = (e.eventTypeId != null && e.eventTypeId! > 0)
? (_types.firstWhere((t) => t.id == e.eventTypeId, orElse: () => EventTypeModel(id: -1, name: '', iconUrl: null)).name)
: '';
final candidateNames = <String?>[
e.venueName,
e.title,
e.name,
nameFromBackend,
];
return candidateNames.any((c) => c != null && c.toLowerCase().contains(label));
}).toList();
}
if (_SelectedTypeId != -1) {
return _events.where((e) => e.eventTypeId == _SelectedTypeId).toList();
}
return _events;
}
void _selectBackendType(int id) {
setState(() {
_SelectedTypeId = id;
_selectedTypeLabel = null;
});
}
void _selectFixedLabel(String label) {
setState(() {
if (label.toLowerCase() == 'all events') {
_SelectedTypeId = -1;
_selectedTypeLabel = null;
} else {
_SelectedTypeId = -1;
_selectedTypeLabel = label;
}
});
}
// ------------------------ Left panel (nav) ------------------------
Widget _buildLeftPanel() {
final theme = Theme.of(context);
// Layout: top area (logo + nav list) scrolls if necessary, bottom area (host panel + settings) remains pinned.
return Container(
width: _sidebarWidth,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/gradient_dark_blue.png'),
fit: BoxFit.cover,
),
),
child: SafeArea(
child: Column(
children: [
const SizedBox(height: 18),
FadeTransition(
opacity: _sidebarTextOpacity,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Text('EVENTIFY', style: theme.textTheme.titleMedium?.copyWith(color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold, fontSize: 18)),
),
),
),
const SizedBox(height: 16),
// Expandable nav list
Expanded(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_navItem(icon: Icons.home, label: 'Home', idx: 0),
_navItem(icon: Icons.location_on, label: 'Events near you', idx: 0),
_navItem(icon: Icons.calendar_today, label: 'Upcoming events', idx: 0),
_navItem(icon: Icons.calendar_view_month, label: 'Calendar', idx: 1),
// <-- Profile between Calendar and Contribute
_navItem(icon: Icons.person, label: 'Profile', idx: 2),
_navItem(icon: Icons.add, label: 'Contribute', idx: 4),
const SizedBox(height: 12),
// optionally more items...
],
),
),
),
// bottom pinned area
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
children: [
_buildHostPanel(),
const SizedBox(height: 12),
_navItem(icon: Icons.settings, label: 'Settings', idx: 5),
const SizedBox(height: 12),
],
),
),
],
),
),
);
}
Widget _buildHostPanel() {
final theme = Theme.of(context);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/gradient_dark_blue.png'),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Hosting a private or ticketed event?', style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
Text('Schedule a call back for setting up event.', style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onPrimary.withOpacity(0.85), fontSize: 12)),
const SizedBox(height: 10),
ElevatedButton(onPressed: () {}, style: ElevatedButton.styleFrom(backgroundColor: theme.colorScheme.primary), child: Text('Schedule Call', style: theme.textTheme.labelLarge?.copyWith(color: theme.colorScheme.onPrimary))),
],
),
);
}
Widget _navItem({required IconData icon, required String label, required int idx}) {
final theme = Theme.of(context);
final selected = selectedMenu == idx;
final color = selected ? theme.colorScheme.onPrimary : theme.colorScheme.onPrimary.withOpacity(0.9);
return ListTile(
leading: Icon(icon, color: color),
title: Text(label, style: TextStyle(color: color)),
dense: true,
onTap: () {
setState(() {
selectedMenu = idx;
});
if (idx == 0) _refreshHome();
},
);
}
// ------------------------ Top bar (common, responsive) ------------------------
Widget _buildTopBar() {
// The top bar sits inside the white content area (to the right of the sidebar).
// The search box is centered inside the content area (slightly toward the right because of fixed width).
final theme = Theme.of(context);
return LayoutBuilder(builder: (context, constraints) {
final isCompact = constraints.maxWidth < _compactTopBarWidth;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 18),
child: Row(
children: [
// Left: location or icon when compact
if (!isCompact)
_fullLocationWidget()
else
IconButton(
tooltip: 'Location',
onPressed: () {},
icon: Icon(Icons.location_on, color: theme.iconTheme.color),
),
const SizedBox(width: 12),
// Center: place the search bar centered in content area using Center + fixed width box.
if (!isCompact)
Expanded(
child: Align(
alignment: Alignment.center,
child: Padding(
// ⬇️ Increase LEFT padding to move search bar more to the right
padding: const EdgeInsets.only(left: 120),
child: SizedBox(
width: 520,
height: 44,
child: TextField(
decoration: InputDecoration(
filled: true,
fillColor: theme.cardColor,
prefixIcon: Icon(Icons.search, color: theme.hintColor),
hintText: 'Search events, pages, features...',
hintStyle: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
style: theme.textTheme.bodyLarge,
),
),
),
),
)
else
IconButton(
tooltip: 'Search',
onPressed: () {},
icon: Icon(Icons.search, color: theme.iconTheme.color),
),
// Spacer ensures right side stays right-aligned
const Spacer(),
// Right: notifications + (optionally username) + avatar
Row(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
children: [
IconButton(onPressed: () {}, icon: Icon(Icons.notifications_none, color: theme.iconTheme.color)),
Positioned(right: 6, top: 6, child: CircleAvatar(radius: 8, backgroundColor: Colors.red, child: Text('2', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white, fontSize: 10)))),
],
),
const SizedBox(width: 8),
if (!isCompact) ...[
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(_username, style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)),
Text('Explorer', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 12)),
]),
const SizedBox(width: 8),
],
GestureDetector(onTap: () => _openProfile(), child: _profileAvatar()),
],
),
],
),
);
});
}
Widget _fullLocationWidget() {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Location', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 12)),
const SizedBox(height: 4),
Row(children: [
Icon(Icons.location_on, size: 18, color: theme.hintColor),
const SizedBox(width: 6),
Text(_location, style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(width: 6),
Icon(Icons.arrow_drop_down, size: 18, color: theme.hintColor),
]),
],
);
}
void _openProfile() {
setState(() => selectedMenu = 2);
}
// ------------------------ Home content (index 0) ------------------------
Widget _homeContent() {
final theme = Theme.of(context);
// Entire home content is a vertical scrollable wrapped in RefreshIndicator for pull-to-refresh behaviour.
return RefreshIndicator(
onRefresh: _refreshHome,
color: theme.colorScheme.primary,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
// HERO / welcome panel
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Container(
padding: const EdgeInsets.all(20),
decoration: AppDecoration.blueGradientRounded(16),
child: Row(
children: [
// left welcome
Expanded(
flex: 6,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('Welcome Back,', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white.withOpacity(0.9), fontSize: 16)),
const SizedBox(height: 6),
Text(_username, style: const TextStyle(color: Colors.white, fontSize: 30, fontWeight: FontWeight.bold)),
]),
),
const SizedBox(width: 16),
// right: horizontal featured events (backend-driven)
Expanded(
flex: 4,
child: SizedBox(
height: 90,
child: _events.isEmpty
? const SizedBox()
: _buildMarqueeFeaturedEvents(),
),
),
],
),
),
),
const SizedBox(height: 18),
// Events header
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 8),
child: Row(children: [
Expanded(child: Text('Events Around You', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold))),
TextButton(onPressed: () {}, child: Text('View All', style: theme.textTheme.bodyMedium)),
]),
),
// type chips: fixed categories first, then backend categories
if (!_loadingTypes) SizedBox(height: 56, child: _buildTypeChips()),
const SizedBox(height: 14),
// events area: use LayoutBuilder to decide between grid and horizontal scroll
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: LayoutBuilder(
builder: (context, constraints) {
// constraints.maxWidth is the available width inside the white content area (after padding)
return _buildEventsArea(constraints.maxWidth);
},
),
),
const SizedBox(height: 40),
],
),
),
);
}
/// Build marquee (continuous leftward scroll) by duplicating the events list and using a ScrollController.
Widget _buildMarqueeFeaturedEvents() {
// Duplicate events for seamless loop
final display = <EventModel>[];
display.addAll(_events);
display.addAll(_events);
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SingleChildScrollView(
controller: _marqueeController,
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
child: Row(
children: List.generate(display.length, (i) {
final e = display[i];
final img = _chooseEventImage(e);
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: SizedBox(width: 220, child: _miniEventCard(e, img)),
);
}),
),
),
);
}
Widget _buildTypeChips() {
final chips = <Widget>[];
// fixed categories first
for (final name in _fixedCategories) {
final isSelected = _selectedTypeLabel != null ? _selectedTypeLabel == name : (name == 'All Events' && _SelectedTypeId == -1 && _selectedTypeLabel == null);
chips.add(_chipWidget(
label: name,
onTap: () => _selectFixedLabel(name),
selected: isSelected,
));
}
// then backend categories (if any)
for (final t in _types) {
final isSelected = (_selectedTypeLabel == null && _SelectedTypeId == t.id);
chips.add(_chipWidget(label: t.name, onTap: () => _selectBackendType(t.id), selected: isSelected));
}
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 24),
scrollDirection: Axis.horizontal,
itemBuilder: (_, idx) => chips[idx],
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemCount: chips.length,
);
}
Widget _chipWidget({
required String label,
required VoidCallback onTap,
required bool selected,
}) {
final theme = Theme.of(context);
return InkWell(
borderRadius: BorderRadius.circular(10),
onTap: onTap,
child: Container(
height: 40, // 👈 fixed height for all chips
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: selected ? theme.colorScheme.primary.withOpacity(0.08) : theme.cardColor,
borderRadius: BorderRadius.circular(10), // 👈 box shape (not pill)
border: Border.all(
color: selected ? theme.colorScheme.primary : theme.dividerColor,
width: 1,
),
),
child: Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: selected ? theme.colorScheme.primary : theme.textTheme.bodyLarge?.color,
),
),
),
);
}
// ------------------------
// EVENTS AREA: fixed 3-columns grid on wide widths; fallback horizontal on narrow widths.
// ------------------------
Widget _buildEventsArea(double contentWidth) {
// Preferred card width you'd like to keep (matches your reference)
const double preferredCardWidth = 360.0;
const double cardHeight = 220.0; // matches reference proportions
const double spacing = 18.0;
final list = _filteredEvents;
final theme = Theme.of(context);
if (_loading) {
return Padding(padding: const EdgeInsets.only(top: 32), child: Center(child: CircularProgressIndicator(color: theme.colorScheme.primary)));
}
if (list.isEmpty) {
return Padding(padding: const EdgeInsets.symmetric(vertical: 40), child: Center(child: Text('No events available', style: theme.textTheme.bodyMedium)));
}
// Determine if we have space to show exactly 3 columns.
// Required width for 3 columns = 3 * preferredCardWidth + 2 * spacing
final double requiredWidthForThree = preferredCardWidth * 3 + spacing * 2;
if (contentWidth >= requiredWidthForThree) {
// Enough space: force a 3-column grid (this ensures exactly 3 cards per row)
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: list.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // always 3 when space allows
mainAxisExtent: cardHeight,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
),
itemBuilder: (ctx, idx) {
final e = list[idx];
final img = _chooseEventImage(e);
return _eventCardForGrid(e, img);
},
);
} else {
// Not enough space for 3 columns — use horizontal scroll preserving card width.
// This keeps behaviour sensible on narrower desktop windows.
final double preferredWidth = preferredCardWidth;
return SizedBox(
height: cardHeight,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: list.length,
separatorBuilder: (_, __) => const SizedBox(width: spacing),
itemBuilder: (ctx, idx) {
final e = list[idx];
final img = _chooseEventImage(e);
return SizedBox(width: preferredWidth, child: _eventCardForFixedSize(e, img, preferredWidth));
},
),
);
}
}
// small horizontal card used inside HERO right area
Widget _miniEventCard(EventModel e, String? img) {
final theme = Theme.of(context);
return Container(
width: 220,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(12)),
child: Row(children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: img != null ? Image.network(img, width: 64, height: 64, fit: BoxFit.cover) : Container(width: 64, height: 64, color: Theme.of(context).dividerColor),
),
const SizedBox(width: 8),
Expanded(
child: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(e.title ?? e.name ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
Text(e.startDate ?? '', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).hintColor, fontSize: 12)),
]),
),
]),
);
}
// ---------- Cards ----------
// Card used in grid (we can rely on grid cell having fixed height)
Widget _eventCardForGrid(EventModel e, String? img) {
final theme = Theme.of(context);
// Styling constants to match reference
const double cardRadius = 16.0;
const double imageHeight = 140.0;
const double horizontalPadding = 12.0;
const double verticalPadding = 10.0;
// Friendly formatted date label
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.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))),
child: Container(
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(cardRadius),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 18, offset: const Offset(0, 10)),
BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 6, offset: const Offset(0, 3)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// image flush to top corners
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(cardRadius)),
child: img != null
? Image.network(img, width: double.infinity, height: imageHeight, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor))
: Container(height: imageHeight, width: double.infinity, color: theme.dividerColor),
),
// content area
Padding(
padding: const EdgeInsets.fromLTRB(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title (2 lines)
Text(
e.title ?? e.name ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700, fontSize: 14),
),
const SizedBox(height: 8),
// single row: date • location (icons small, subtle)
Row(
children: [
// calendar small badge
Container(
width: 18,
height: 18,
decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)),
child: Icon(Icons.calendar_today, size: 12, color: theme.colorScheme.primary),
),
const SizedBox(width: 8),
Expanded(
child: Text(
dateLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text('', style: TextStyle(color: Colors.black26)),
),
Container(
width: 18,
height: 18,
decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)),
child: Icon(Icons.location_on, size: 12, color: theme.colorScheme.primary),
),
const SizedBox(width: 8),
Expanded(
child: Text(
e.place ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13),
),
),
],
),
],
),
),
],
),
),
);
}
// Card used in horizontal list (fixed width)
Widget _eventCardForFixedSize(EventModel e, String? img, double width) {
final theme = Theme.of(context);
// Styling constants to match grid card
const double cardRadius = 16.0;
const double imageHeight = 140.0;
const double horizontalPadding = 12.0;
const double verticalPadding = 10.0;
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.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))),
child: Container(
width: width,
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(cardRadius),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 18, offset: const Offset(0, 10)),
BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 6, offset: const Offset(0, 3)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(cardRadius)),
child: img != null
? Image.network(img, width: width, height: imageHeight, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor))
: Container(height: imageHeight, width: width, color: theme.dividerColor),
),
Padding(
padding: const EdgeInsets.fromLTRB(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
e.title ?? e.name ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700, fontSize: 14),
),
const SizedBox(height: 8),
Row(
children: [
Container(
width: 18,
height: 18,
decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)),
child: Icon(Icons.calendar_today, size: 12, color: theme.colorScheme.primary),
),
const SizedBox(width: 8),
Flexible(child: Text(dateLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13))),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text('', style: TextStyle(color: Colors.black26)),
),
Container(
width: 18,
height: 18,
decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)),
child: Icon(Icons.location_on, size: 12, color: theme.colorScheme.primary),
),
const SizedBox(width: 8),
Expanded(child: Text(e.place ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13))),
],
),
],
),
),
],
),
),
);
}
// ------------------------ Page routing ------------------------
Widget _getCurrentPage() {
switch (selectedMenu) {
case 1:
return const CalendarScreen();
case 2:
return const ProfileScreen();
case 3:
return BookingScreen(onBook: () {}, image: '');
case 4:
// Contribute placeholder (kept simple)
return SingleChildScrollView(
padding: const EdgeInsets.all(28),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('Contribute', style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 14),
const Text('Submit events or contact the Eventify team.'),
const SizedBox(height: 24),
Card(
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(children: [
TextField(decoration: InputDecoration(labelText: 'Event title', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))),
const SizedBox(height: 12),
TextField(decoration: InputDecoration(labelText: 'Location', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))),
const SizedBox(height: 12),
TextField(maxLines: 4, decoration: InputDecoration(labelText: 'Description', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))),
const SizedBox(height: 12),
Row(children: [
ElevatedButton(onPressed: () {}, child: const Text('Submit')),
const SizedBox(width: 12),
OutlinedButton(onPressed: () {}, child: const Text('Reset')),
])
]),
),
),
]),
);
case 5:
return const SettingsScreen();
default:
return _homeContent();
}
}
// ------------------------ Build ------------------------
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: Row(
children: [
_buildLeftPanel(),
Expanded(
child: Column(
children: [
// Show top bar ONLY when Home is active
if (selectedMenu == 0) _buildTopBar(),
// Page content under the top bar (or directly if top bar hidden)
Expanded(child: _getCurrentPage()),
],
),
),
],
),
);
}
}