diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 6847983..d19ced2 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -40,7 +40,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM bool _loading = true; // Hero carousel - final PageController _heroPageController = PageController(); + final PageController _heroPageController = PageController(viewportFraction: 0.88); late final ValueNotifier _heroPageNotifier; Timer? _autoScrollTimer; @@ -1131,12 +1131,31 @@ class _HomeScreenState extends State with SingleTickerProviderStateM : Column( children: [ SizedBox( - height: 300, + height: 320, child: PageView.builder( controller: _heroPageController, - onPageChanged: (page) => _heroPageNotifier.value = page, + onPageChanged: (page) { + _heroPageNotifier.value = page; + // Reset 3-second countdown so user always gets full read time + _startAutoScroll(); + }, itemCount: _heroEvents.length, - itemBuilder: (context, index) => _buildHeroEventImage(_heroEvents[index]), + 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), @@ -1197,58 +1216,147 @@ class _HomeScreenState extends State with SingleTickerProviderStateM img = event.images.first.image; } - final radius = 24.0; + const double radius = 24.0; return GestureDetector( onTap: () { if (event.id != null) { - Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id))); + Navigator.push(context, + MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id))); } }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - children: [ - // Image only (no text overlay) - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(radius), - child: SizedBox( - width: double.infinity, - child: img != null && img.isNotEmpty - ? CachedNetworkImage( - imageUrl: img, - fit: BoxFit.cover, - placeholder: (_, __) => Container( - decoration: const BoxDecoration(color: Color(0xFF1A2A4A))), - errorWidget: (_, __, ___) => - Container(decoration: AppDecoration.blueGradientRounded(radius)), - ) - : Container( - decoration: AppDecoration.blueGradientRounded(radius), - ), + padding: const EdgeInsets.symmetric(horizontal: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: Stack( + fit: StackFit.expand, + children: [ + // ── Layer 0: Event image (full-bleed) ── + img != null && img.isNotEmpty + ? CachedNetworkImage( + imageUrl: img, + fit: BoxFit.cover, + placeholder: (_, __) => const _HeroShimmer(radius: radius), + errorWidget: (_, __, ___) => + Container(decoration: AppDecoration.blueGradientRounded(radius)), + ) + : Container(decoration: AppDecoration.blueGradientRounded(radius)), + + // ── Layer 1: Bottom gradient overlay (text readability) ── + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.35, 1.0], + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.78), + ], + ), + ), ), ), - ), - // Title text outside the image - const SizedBox(height: 12), - Text( - event.title ?? event.name ?? '', - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 22, - fontWeight: FontWeight.bold, - height: 1.2, - shadows: [ - Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)), - ], + // ── Layer 2: FEATURED glassmorphism badge (top-left) ── + Positioned( + top: 14, + left: 14, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.18), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withOpacity(0.28)), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.star_rounded, color: Colors.amber, size: 13), + SizedBox(width: 4), + Text( + 'FEATURED', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w800, + letterSpacing: 0.6, + ), + ), + ], + ), + ), + ), + ), ), - ), - ], + + // ── Layer 3: Title + metadata (bottom overlay) ── + Positioned( + bottom: 18, + left: 16, + right: 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + event.title ?? event.name ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w800, + height: 1.25, + shadows: [ + Shadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)), + ], + ), + ), + const SizedBox(height: 8), + Row( + children: [ + if (event.startDate != null) ...[ + const Icon(Icons.calendar_today_rounded, + color: Colors.white70, size: 12), + const SizedBox(width: 4), + Text( + event.startDate!, + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w500), + ), + const SizedBox(width: 10), + ], + if (event.place != null && event.place!.isNotEmpty) ...[ + const Icon(Icons.location_on_rounded, + color: Colors.white70, size: 12), + const SizedBox(width: 4), + Flexible( + child: Text( + event.place!, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w500), + ), + ), + ], + ], + ), + ], + ), + ), + ], + ), ), ), ); @@ -2067,3 +2175,57 @@ class _HomeScreenState extends State with SingleTickerProviderStateM return 'You'; } } + +/// Animated shimmer placeholder shown while a hero card image is loading. +/// 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}); + + @override + State<_HeroShimmer> createState() => _HeroShimmerState(); +} + +class _HeroShimmerState extends State<_HeroShimmer> + with SingleTickerProviderStateMixin { + late final AnimationController _ctrl; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1400), + )..repeat(); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _ctrl, + builder: (_, __) { + final x = -1.5 + _ctrl.value * 3.0; + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.radius), + gradient: LinearGradient( + begin: Alignment(x - 1.0, 0), + end: Alignment(x, 0), + colors: const [ + Color(0xFF1A2A4A), + Color(0xFF2D4580), + Color(0xFF1A2A4A), + ], + ), + ), + ); + }, + ); + } +}