- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten - Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home - Desktop calendar: 60/40 two-column layout with white background - Desktop profile: full-width banner + 3-column event grids - Desktop learn more: hero image + about/venue columns + gallery strip - Desktop settings/contribute: polished to match design system - Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates - Guest access: requiresAuth false on read endpoints - Location fix: show place names instead of lat/lng coordinates - Version 1.6.1+17 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
890 lines
31 KiB
Dart
890 lines
31 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../features/events/models/event_models.dart';
|
|
import '../features/events/services/events_service.dart';
|
|
import '../features/gamification/providers/gamification_provider.dart';
|
|
import '../widgets/responsive_shell.dart';
|
|
import 'calendar_screen.dart';
|
|
import 'profile_screen.dart';
|
|
import 'booking_screen.dart';
|
|
import 'settings_screen.dart';
|
|
import 'learn_more_screen.dart';
|
|
import 'contribute_screen.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> {
|
|
int _selectedMenu = 0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ResponsiveShell(
|
|
currentIndex: _selectedMenu,
|
|
onIndexChanged: (idx) => setState(() => _selectedMenu = idx),
|
|
showTopBar: true,
|
|
child: _getCurrentPage(),
|
|
);
|
|
}
|
|
|
|
Widget _getCurrentPage() {
|
|
switch (_selectedMenu) {
|
|
case 1:
|
|
return const CalendarScreen();
|
|
case 2:
|
|
return const ProfileScreen();
|
|
case 3:
|
|
return BookingScreen(onBook: () {}, image: '');
|
|
case 4:
|
|
return ChangeNotifierProvider(
|
|
create: (_) => GamificationProvider(),
|
|
child: const ContributeScreen(),
|
|
);
|
|
case 5:
|
|
return const SettingsScreen();
|
|
default:
|
|
return _HomeContent(
|
|
onEventTap: (eventId) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: eventId)),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Home content — hero, categories, event grid
|
|
// ---------------------------------------------------------------------------
|
|
class _HomeContent extends StatefulWidget {
|
|
final void Function(int eventId) onEventTap;
|
|
const _HomeContent({required this.onEventTap});
|
|
|
|
@override
|
|
State<_HomeContent> createState() => _HomeContentState();
|
|
}
|
|
|
|
class _HomeContentState extends State<_HomeContent>
|
|
with SingleTickerProviderStateMixin {
|
|
final EventsService _eventsService = EventsService();
|
|
|
|
List<EventModel> _events = [];
|
|
List<EventTypeModel> _types = [];
|
|
bool _loading = true;
|
|
bool _loadingTypes = true;
|
|
|
|
int _selectedTypeId = -1;
|
|
String? _selectedTypeLabel;
|
|
|
|
String _username = 'Guest';
|
|
String _pincode = 'all';
|
|
|
|
// Ken Burns hero animation
|
|
late AnimationController _heroAnimController;
|
|
late Animation<double> _heroScaleAnim;
|
|
int _heroImageIndex = 0;
|
|
Timer? _heroRotateTimer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_heroAnimController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(seconds: 12),
|
|
);
|
|
_heroScaleAnim = Tween<double>(begin: 1.0, end: 1.08).animate(
|
|
CurvedAnimation(parent: _heroAnimController, curve: Curves.easeInOut),
|
|
);
|
|
_heroAnimController.repeat(reverse: true);
|
|
_loadData();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_heroAnimController.dispose();
|
|
_heroRotateTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _startHeroRotation() {
|
|
_heroRotateTimer?.cancel();
|
|
if (_events.length <= 1) return;
|
|
_heroRotateTimer = Timer.periodic(const Duration(seconds: 6), (_) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_heroImageIndex = (_heroImageIndex + 1) % _events.length.clamp(1, 5);
|
|
});
|
|
});
|
|
}
|
|
|
|
Future<void> _loadData() async {
|
|
setState(() {
|
|
_loading = true;
|
|
_loadingTypes = true;
|
|
});
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
_username = prefs.getString('display_name') ??
|
|
prefs.getString('username') ??
|
|
'Guest';
|
|
_pincode = prefs.getString('pincode') ?? 'all';
|
|
|
|
await Future.wait([
|
|
_fetchEventTypes(),
|
|
_fetchEvents(),
|
|
]);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_loading = false;
|
|
_loadingTypes = false;
|
|
});
|
|
_startHeroRotation();
|
|
}
|
|
}
|
|
|
|
Future<void> _fetchEventTypes() async {
|
|
try {
|
|
final types = await _eventsService.getEventTypes();
|
|
if (mounted) setState(() => _types = types);
|
|
} catch (_) {}
|
|
}
|
|
|
|
Future<void> _fetchEvents() async {
|
|
try {
|
|
final events = await _eventsService.getEventsByPincode(_pincode);
|
|
if (mounted) setState(() => _events = events);
|
|
} catch (_) {}
|
|
}
|
|
|
|
List<EventModel> get _filteredEvents {
|
|
if (_selectedTypeLabel != null &&
|
|
_selectedTypeLabel!.toLowerCase() != 'all events') {
|
|
final label = _selectedTypeLabel!.toLowerCase();
|
|
return _events.where((e) {
|
|
final typeName = (e.eventTypeId != null && e.eventTypeId! > 0)
|
|
? (_types
|
|
.firstWhere((t) => t.id == e.eventTypeId,
|
|
orElse: () =>
|
|
EventTypeModel(id: -1, name: '', iconUrl: null))
|
|
.name)
|
|
: '';
|
|
return [e.venueName, e.title, e.name, typeName]
|
|
.any((c) => c != null && c.toLowerCase().contains(label));
|
|
}).toList();
|
|
}
|
|
if (_selectedTypeId != -1) {
|
|
return _events.where((e) => e.eventTypeId == _selectedTypeId).toList();
|
|
}
|
|
return _events;
|
|
}
|
|
|
|
String? _chooseImage(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;
|
|
}
|
|
|
|
String _formatDateBadge(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\n$month';
|
|
}
|
|
} catch (_) {}
|
|
return dateStr;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Container(
|
|
color: const Color(0xFFFAFBFC),
|
|
child: RefreshIndicator(
|
|
onRefresh: _loadData,
|
|
color: theme.colorScheme.primary,
|
|
child: CustomScrollView(
|
|
physics: const BouncingScrollPhysics(
|
|
parent: AlwaysScrollableScrollPhysics(),
|
|
),
|
|
slivers: [
|
|
// Hero section
|
|
SliverToBoxAdapter(child: _buildHeroSection(theme)),
|
|
const SliverToBoxAdapter(child: SizedBox(height: 24)),
|
|
|
|
// Type chips
|
|
if (!_loadingTypes)
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(height: 48, child: _buildTypeChips(theme)),
|
|
),
|
|
|
|
const SliverToBoxAdapter(child: SizedBox(height: 20)),
|
|
|
|
// Section header
|
|
SliverToBoxAdapter(
|
|
child: 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,
|
|
color: const Color(0xFF111827),
|
|
),
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: () {},
|
|
child: Text(
|
|
'View All',
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
color: const Color(0xFF2563EB),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
|
|
const SliverToBoxAdapter(child: SizedBox(height: 4)),
|
|
|
|
// Event grid
|
|
_buildEventGrid(theme),
|
|
|
|
const SliverToBoxAdapter(child: SizedBox(height: 40)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ──── Hero Section (website-style: large image + Ken Burns + overlay) ────
|
|
Widget _buildHeroSection(ThemeData theme) {
|
|
final heroEvent = _events.isNotEmpty
|
|
? _events[_heroImageIndex.clamp(0, _events.length - 1)]
|
|
: null;
|
|
final heroImg = heroEvent != null ? _chooseImage(heroEvent) : null;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 8, 24, 0),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: SizedBox(
|
|
height: 400,
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
// Background image with Ken Burns
|
|
AnimatedBuilder(
|
|
animation: _heroScaleAnim,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _heroScaleAnim.value,
|
|
child: child,
|
|
);
|
|
},
|
|
child: heroImg != null
|
|
? AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 800),
|
|
child: CachedNetworkImage(
|
|
key: ValueKey(heroImg),
|
|
imageUrl: heroImg,
|
|
fit: BoxFit.cover,
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
memCacheWidth: 1400,
|
|
placeholder: (_, __) => Container(
|
|
color: const Color(0xFF0A0E1A),
|
|
),
|
|
errorWidget: (_, __, ___) => Container(
|
|
color: const Color(0xFF0A0E1A),
|
|
),
|
|
),
|
|
)
|
|
: Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [Color(0xFF0F45CF), Color(0xFF082369)],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Dark gradient overlay
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Color(0x4D000000), // 0.3 alpha
|
|
Color(0x99000000), // 0.6 alpha
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Content overlay
|
|
Padding(
|
|
padding: const EdgeInsets.all(40),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (heroEvent != null) ...[
|
|
Text(
|
|
heroEvent.title ?? heroEvent.name ?? 'Discover Amazing Events',
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 36,
|
|
fontWeight: FontWeight.w800,
|
|
height: 1.2,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (heroEvent.place != null || heroEvent.startDate != null)
|
|
Row(
|
|
children: [
|
|
if (heroEvent.startDate != null) ...[
|
|
const Icon(Icons.calendar_today,
|
|
size: 14, color: Colors.white70),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
heroEvent.startDate!,
|
|
style: const TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
if (heroEvent.startDate != null &&
|
|
heroEvent.place != null)
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 12),
|
|
child: Text('·',
|
|
style: TextStyle(
|
|
color: Colors.white54, fontSize: 14)),
|
|
),
|
|
if (heroEvent.place != null) ...[
|
|
const Icon(Icons.location_on,
|
|
size: 14, color: Colors.white70),
|
|
const SizedBox(width: 6),
|
|
Flexible(
|
|
child: Text(
|
|
heroEvent.place!,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
] else ...[
|
|
const Text(
|
|
'Discover Amazing\nEvents Near You',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 36,
|
|
fontWeight: FontWeight.w800,
|
|
height: 1.2,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Find events, book tickets, and explore',
|
|
style: TextStyle(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 24),
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
if (heroEvent != null) {
|
|
_showFeaturedModal(theme, heroEvent);
|
|
}
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 32, vertical: 14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF2563EB),
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF2563EB)
|
|
.withValues(alpha: 0.4),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Text(
|
|
heroEvent != null ? 'Learn More' : 'Explore Events',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Hero image indicator dots
|
|
if (_events.length > 1)
|
|
Positioned(
|
|
bottom: 16,
|
|
right: 24,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: List.generate(
|
|
_events.length.clamp(0, 5),
|
|
(i) => Container(
|
|
width: i == _heroImageIndex ? 24 : 8,
|
|
height: 8,
|
|
margin: const EdgeInsets.only(left: 6),
|
|
decoration: BoxDecoration(
|
|
color: i == _heroImageIndex
|
|
? Colors.white
|
|
: Colors.white.withValues(alpha: 0.4),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ──── Featured Event Modal ────
|
|
void _showFeaturedModal(ThemeData theme, EventModel event) {
|
|
final img = _chooseImage(event);
|
|
showDialog(
|
|
context: context,
|
|
barrierColor: Colors.black87,
|
|
builder: (ctx) => Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 700, maxHeight: 500),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: Stack(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: SizedBox(
|
|
width: 700,
|
|
height: 500,
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
if (img != null)
|
|
CachedNetworkImage(
|
|
imageUrl: img,
|
|
fit: BoxFit.cover,
|
|
memCacheWidth: 1400,
|
|
)
|
|
else
|
|
Container(color: const Color(0xFF0A0E1A)),
|
|
// Gradient
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.transparent, Color(0xCC000000)],
|
|
),
|
|
),
|
|
),
|
|
// Content
|
|
Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
event.title ?? event.name ?? '',
|
|
maxLines: 3,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w800,
|
|
height: 1.2,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (event.startDate != null)
|
|
Text(
|
|
'${event.startDate}${event.place != null ? ' · ${event.place}' : ''}',
|
|
style: const TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
Navigator.of(ctx).pop();
|
|
widget.onEventTap(event.id);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 32, vertical: 14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF2563EB),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Text(
|
|
'Learn More',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Close button
|
|
Positioned(
|
|
top: 12,
|
|
right: 12,
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: () => Navigator.of(ctx).pop(),
|
|
child: Container(
|
|
width: 36,
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.5),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.close,
|
|
color: Colors.white, size: 20),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ──── Type Chips (pill style matching website) ────
|
|
Widget _buildTypeChips(ThemeData theme) {
|
|
final chips = <Widget>[];
|
|
|
|
final allSelected =
|
|
_selectedTypeLabel == null && _selectedTypeId == -1;
|
|
chips.add(_chip(theme, 'All Events', allSelected, () {
|
|
setState(() {
|
|
_selectedTypeId = -1;
|
|
_selectedTypeLabel = null;
|
|
});
|
|
}));
|
|
|
|
for (final t in _types) {
|
|
final selected = _selectedTypeLabel == null && _selectedTypeId == t.id;
|
|
chips.add(_chip(theme, t.name, selected, () {
|
|
setState(() {
|
|
_selectedTypeId = t.id;
|
|
_selectedTypeLabel = null;
|
|
});
|
|
}));
|
|
}
|
|
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
scrollDirection: Axis.horizontal,
|
|
itemBuilder: (_, idx) => chips[idx],
|
|
separatorBuilder: (_, __) => const SizedBox(width: 10),
|
|
itemCount: chips.length,
|
|
);
|
|
}
|
|
|
|
Widget _chip(ThemeData theme, String label, bool selected, VoidCallback onTap) {
|
|
return MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: onTap,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
height: 36,
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
decoration: BoxDecoration(
|
|
color: selected
|
|
? const Color(0xFF0F45CF)
|
|
: const Color(0xFFF3F4F6),
|
|
borderRadius: BorderRadius.circular(999),
|
|
boxShadow: selected
|
|
? [
|
|
BoxShadow(
|
|
color: const Color(0xFF0F45CF).withValues(alpha: 0.3),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: selected ? Colors.white : const Color(0xFF111827),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ──── Event Grid ────
|
|
Widget _buildEventGrid(ThemeData theme) {
|
|
final list = _filteredEvents;
|
|
|
|
if (_loading) {
|
|
return SliverFillRemaining(
|
|
child: Center(
|
|
child: CircularProgressIndicator(color: theme.colorScheme.primary),
|
|
),
|
|
);
|
|
}
|
|
if (list.isEmpty) {
|
|
return SliverFillRemaining(
|
|
child: Center(
|
|
child: Text('No events available', style: theme.textTheme.bodyMedium),
|
|
),
|
|
);
|
|
}
|
|
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
sliver: SliverGrid(
|
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
maxCrossAxisExtent: 400,
|
|
mainAxisExtent: 260,
|
|
crossAxisSpacing: 18,
|
|
mainAxisSpacing: 18,
|
|
),
|
|
delegate: SliverChildBuilderDelegate(
|
|
(ctx, idx) {
|
|
final e = list[idx];
|
|
final img = _chooseImage(e);
|
|
return _eventCard(theme, e, img);
|
|
},
|
|
childCount: list.length,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _eventCard(ThemeData theme, EventModel e, String? img) {
|
|
const double cardRadius = 16;
|
|
const double imageHeight = 160;
|
|
|
|
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 MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: () => widget.onEventTap(e.id),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(cardRadius),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.06),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Image with date badge
|
|
Stack(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: const BorderRadius.vertical(
|
|
top: Radius.circular(cardRadius)),
|
|
child: img != null
|
|
? CachedNetworkImage(
|
|
imageUrl: img,
|
|
memCacheWidth: 600,
|
|
memCacheHeight: 320,
|
|
width: double.infinity,
|
|
height: imageHeight,
|
|
fit: BoxFit.cover,
|
|
placeholder: (_, __) => Container(
|
|
height: imageHeight,
|
|
color: const Color(0xFFE5E7EB)),
|
|
errorWidget: (_, __, ___) => Container(
|
|
height: imageHeight,
|
|
color: const Color(0xFFE5E7EB)),
|
|
)
|
|
: Container(
|
|
height: imageHeight,
|
|
width: double.infinity,
|
|
color: const Color(0xFFE5E7EB),
|
|
),
|
|
),
|
|
// Date badge
|
|
if (e.startDate != null)
|
|
Positioned(
|
|
top: 10,
|
|
right: 10,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.92),
|
|
borderRadius: BorderRadius.circular(8),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.1),
|
|
blurRadius: 4,
|
|
),
|
|
],
|
|
),
|
|
child: Text(
|
|
_formatDateBadge(e.startDate!),
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF111827),
|
|
height: 1.3,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
e.title ?? e.name ?? '',
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 15,
|
|
color: Color(0xFF111827),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Row(children: [
|
|
const Icon(Icons.calendar_today,
|
|
size: 13, color: Color(0xFF3B82F6)),
|
|
const SizedBox(width: 6),
|
|
Flexible(
|
|
child: Text(
|
|
dateLabel,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 12, color: Color(0xFF6B7280)),
|
|
),
|
|
),
|
|
]),
|
|
if (e.place != null) ...[
|
|
const SizedBox(height: 4),
|
|
Row(children: [
|
|
const Icon(Icons.location_on,
|
|
size: 13, color: Color(0xFF22C55E)),
|
|
const SizedBox(width: 6),
|
|
Flexible(
|
|
child: Text(
|
|
e.place!,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 12, color: Color(0xFF6B7280)),
|
|
),
|
|
),
|
|
]),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|