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>
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../core/auth/auth_guard.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../features/events/models/event_models.dart';
|
||||
import '../features/events/services/events_service.dart';
|
||||
@@ -61,15 +62,15 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startAutoScroll() {
|
||||
void _startAutoScroll({Duration delay = const Duration(seconds: 5)}) {
|
||||
_autoScrollTimer?.cancel();
|
||||
_autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
|
||||
_autoScrollTimer = Timer.periodic(delay, (timer) {
|
||||
if (_heroEvents.isEmpty) return;
|
||||
final nextPage = (_heroPageNotifier.value + 1) % _heroEvents.length;
|
||||
if (_heroPageController.hasClients) {
|
||||
_heroPageController.animateToPage(
|
||||
nextPage,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
@@ -80,7 +81,14 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
setState(() => _loading = true);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_username = prefs.getString('display_name') ?? prefs.getString('username') ?? '';
|
||||
_location = prefs.getString('location') ?? 'Whitefield, Bengaluru';
|
||||
final storedLocation = prefs.getString('location') ?? 'Whitefield, Bengaluru';
|
||||
// Fix legacy lat,lng strings saved before the reverse-geocoding fix
|
||||
if (RegExp(r'^-?\d+\.\d+,-?\d+\.\d+$').hasMatch(storedLocation)) {
|
||||
_location = 'Current Location';
|
||||
prefs.setString('location', _location);
|
||||
} else {
|
||||
_location = storedLocation;
|
||||
}
|
||||
_pincode = prefs.getString('pincode') ?? 'all';
|
||||
|
||||
try {
|
||||
@@ -482,7 +490,26 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
|
||||
|
||||
// Get hero events (first 4 events for the carousel)
|
||||
List<EventModel> get _heroEvents => _events.take(4).toList();
|
||||
List<EventModel> get _heroEvents => _events.take(6).toList();
|
||||
|
||||
String _formatDate(String dateStr) {
|
||||
try {
|
||||
final dt = DateTime.parse(dateStr);
|
||||
return DateFormat('d MMM yyyy').format(dt);
|
||||
} catch (_) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
String _getEventTypeName(EventModel event) {
|
||||
if (event.eventTypeId != null && event.eventTypeId! > 0) {
|
||||
final match = _types.where((t) => t.id == event.eventTypeId);
|
||||
if (match.isNotEmpty && match.first.name.isNotEmpty) {
|
||||
return match.first.name.toUpperCase();
|
||||
}
|
||||
}
|
||||
return 'EVENT';
|
||||
}
|
||||
|
||||
// Date filter state
|
||||
String _selectedDateFilter = '';
|
||||
@@ -1131,42 +1158,51 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
|
||||
// Featured carousel
|
||||
_heroEvents.isEmpty
|
||||
? SizedBox(
|
||||
height: 280,
|
||||
child: Center(
|
||||
child: _loading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text('No events available', style: TextStyle(color: Colors.white70)),
|
||||
),
|
||||
)
|
||||
? _loading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
height: 320,
|
||||
child: _HeroShimmer(),
|
||||
),
|
||||
)
|
||||
: const SizedBox(
|
||||
height: 280,
|
||||
child: Center(
|
||||
child: Text('No events available',
|
||||
style: TextStyle(color: Colors.white70)),
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 320,
|
||||
child: PageView.builder(
|
||||
controller: _heroPageController,
|
||||
onPageChanged: (page) {
|
||||
_heroPageNotifier.value = page;
|
||||
// Reset 3-second countdown so user always gets full read time
|
||||
_startAutoScroll();
|
||||
},
|
||||
itemCount: _heroEvents.length,
|
||||
itemBuilder: (context, index) {
|
||||
// Scale animation: active card = 1.0, adjacent = 0.94
|
||||
return AnimatedBuilder(
|
||||
animation: _heroPageController,
|
||||
builder: (context, child) {
|
||||
double scale = index == _heroPageNotifier.value ? 1.0 : 0.94;
|
||||
if (_heroPageController.position.haveDimensions) {
|
||||
scale = (1.0 -
|
||||
(_heroPageController.page! - index).abs() * 0.06)
|
||||
.clamp(0.94, 1.0);
|
||||
}
|
||||
return Transform.scale(scale: scale, child: child);
|
||||
},
|
||||
child: _buildHeroEventImage(_heroEvents[index]),
|
||||
);
|
||||
},
|
||||
RepaintBoundary(
|
||||
child: SizedBox(
|
||||
height: 320,
|
||||
child: PageView.builder(
|
||||
controller: _heroPageController,
|
||||
onPageChanged: (page) {
|
||||
_heroPageNotifier.value = page;
|
||||
// 8s delay after manual swipe for full read time
|
||||
_startAutoScroll(delay: const Duration(seconds: 8));
|
||||
},
|
||||
itemCount: _heroEvents.length,
|
||||
itemBuilder: (context, index) {
|
||||
// Scale animation: active card = 1.0, adjacent = 0.94
|
||||
return AnimatedBuilder(
|
||||
animation: _heroPageController,
|
||||
builder: (context, child) {
|
||||
double scale = index == _heroPageNotifier.value ? 1.0 : 0.94;
|
||||
if (_heroPageController.position.haveDimensions) {
|
||||
scale = (1.0 -
|
||||
(_heroPageController.page! - index).abs() * 0.06)
|
||||
.clamp(0.94, 1.0);
|
||||
}
|
||||
return Transform.scale(scale: scale, child: child);
|
||||
},
|
||||
child: _buildHeroEventImage(_heroEvents[index]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -1185,7 +1221,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
valueListenable: _heroPageNotifier,
|
||||
builder: (context, currentPage, _) {
|
||||
return SizedBox(
|
||||
height: 12,
|
||||
height: 44,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
@@ -1199,13 +1235,21 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: isActive ? 24 : 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.white : Colors.white.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: SizedBox(
|
||||
width: 44,
|
||||
height: 44,
|
||||
child: Center(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: isActive ? 24 : 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? Colors.white
|
||||
: Colors.white.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1247,7 +1291,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
img != null && img.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: img,
|
||||
memCacheWidth: 800,
|
||||
memCacheWidth: 700,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, __) => const _HeroShimmer(radius: radius),
|
||||
errorWidget: (_, __, ___) =>
|
||||
@@ -1272,7 +1316,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
),
|
||||
),
|
||||
|
||||
// ── Layer 2: FEATURED glassmorphism badge (top-left) ──
|
||||
// ── Layer 2: Event type glassmorphism badge (top-left) ──
|
||||
Positioned(
|
||||
top: 14,
|
||||
left: 14,
|
||||
@@ -1283,18 +1327,18 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.18),
|
||||
color: Colors.white.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.28)),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.28)),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.star_rounded, color: Colors.amber, size: 13),
|
||||
SizedBox(width: 4),
|
||||
const Icon(Icons.star_rounded, color: Colors.amber, size: 13),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'FEATURED',
|
||||
style: TextStyle(
|
||||
_getEventTypeName(event),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w800,
|
||||
@@ -1339,7 +1383,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
color: Colors.white70, size: 12),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
event.startDate!,
|
||||
_formatDate(event.startDate!),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
@@ -2198,7 +2242,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
/// Renders a blue-toned scan-line effect matching the app's colour palette.
|
||||
class _HeroShimmer extends StatefulWidget {
|
||||
final double radius;
|
||||
const _HeroShimmer({required this.radius});
|
||||
const _HeroShimmer({this.radius = 24.0});
|
||||
|
||||
@override
|
||||
State<_HeroShimmer> createState() => _HeroShimmerState();
|
||||
|
||||
Reference in New Issue
Block a user