Files
Eventify-frontend/lib/screens/home_desktop_screen.dart
Sicherhaven bc6fde1b90 feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30

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)),
),
),
]),
],
],
),
),
),
],
),
),
),
);
}
}