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:
2026-03-21 13:28:19 +05:30
parent 04af387945
commit dd7268cd98
21 changed files with 2938 additions and 1285 deletions

View File

@@ -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();