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

@@ -11,6 +11,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import '../features/events/models/event_models.dart';
import '../features/events/services/events_service.dart';
import '../core/auth/auth_guard.dart';
import '../core/constants.dart';
class LearnMoreScreen extends StatefulWidget {
final int eventId;
@@ -227,10 +228,279 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
}
final mediaQuery = MediaQuery.of(context);
final screenWidth = mediaQuery.size.width;
final screenHeight = mediaQuery.size.height;
final imageHeight = screenHeight * 0.45;
final topPadding = mediaQuery.padding.top;
// ── DESKTOP layout ──────────────────────────────────────────────────
if (screenWidth >= AppConstants.desktopBreakpoint) {
final images = _imageUrls;
final heroImage = images.isNotEmpty ? images[0] : null;
final venueLabel = _event!.locationName ?? _event!.venueName ?? _event!.place ?? '';
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Hero image with gradient overlay ──
SizedBox(
width: double.infinity,
height: 300,
child: Stack(
fit: StackFit.expand,
children: [
// Background image
if (heroImage != null)
CachedNetworkImage(
imageUrl: heroImage,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
),
),
),
errorWidget: (_, __, ___) => Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
),
),
),
)
else
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
),
),
),
// Gradient overlay
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.3),
Colors.black.withOpacity(0.65),
],
),
),
),
// Top bar: back + share + wishlist
Positioned(
top: topPadding + 10,
left: 16,
right: 16,
child: Row(
children: [
_squareIconButton(icon: Icons.arrow_back, onTap: () => Navigator.pop(context)),
const SizedBox(width: 8),
_squareIconButton(icon: Icons.ios_share_outlined, onTap: _shareEvent),
const SizedBox(width: 8),
_squareIconButton(
icon: _wishlisted ? Icons.favorite : Icons.favorite_border,
iconColor: _wishlisted ? Colors.redAccent : Colors.white,
onTap: () {
if (!AuthGuard.requireLogin(context, reason: 'Sign in to save events to your wishlist.')) return;
setState(() => _wishlisted = !_wishlisted);
},
),
],
),
),
// Title + date + venue overlaid at bottom-left
Positioned(
left: 32,
bottom: 28,
right: 200,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_event!.title ?? _event!.name,
style: theme.textTheme.headlineMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 28,
height: 1.2,
),
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.calendar_today_outlined, size: 16, color: Colors.white70),
const SizedBox(width: 6),
Text(
_formattedDateRange(),
style: const TextStyle(color: Colors.white70, fontSize: 15),
),
if (venueLabel.isNotEmpty) ...[
const SizedBox(width: 16),
const Icon(Icons.location_on_outlined, size: 16, color: Colors.white70),
const SizedBox(width: 4),
Flexible(
child: Text(
venueLabel,
style: const TextStyle(color: Colors.white70, fontSize: 15),
overflow: TextOverflow.ellipsis,
),
),
],
],
),
],
),
),
// "Book Your Spot" CTA on the right
Positioned(
right: 32,
bottom: 36,
child: ElevatedButton(
onPressed: () {
// TODO: implement booking action
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A56DB),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 4,
),
child: const Text(
'Book Your Spot',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
),
),
],
),
),
const SizedBox(height: 28),
// ── Two-column: About (left 60%) + Venue/Map (right 40%) ──
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left column — About the Event
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAboutSection(theme),
if (_event!.importantInfo.isNotEmpty)
_buildImportantInfoSection(theme),
if (_event!.importantInfo.isEmpty &&
(_event!.importantInformation ?? '').isNotEmpty)
_buildImportantInfoFallback(theme),
],
),
),
const SizedBox(width: 32),
// Right column — Venue / map
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_event!.latitude != null && _event!.longitude != null) ...[
_buildVenueSection(theme),
const SizedBox(height: 12),
_buildGetDirectionsButton(theme),
],
],
),
),
],
),
),
// ── Gallery: horizontal scrollable image strip ──
if (images.length > 1) ...[
const SizedBox(height: 32),
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
'Gallery',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
fontSize: 20,
),
),
),
const SizedBox(height: 14),
SizedBox(
height: 160,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 32),
itemCount: images.length > 6 ? 6 : images.length,
itemBuilder: (context, i) {
// Show overflow count badge on last visible item
final isLast = i == 5 && images.length > 6;
return Padding(
padding: const EdgeInsets.only(right: 12),
child: ClipRRect(
borderRadius: BorderRadius.circular(14),
child: SizedBox(
width: 220,
child: Stack(
fit: StackFit.expand,
children: [
CachedNetworkImage(
imageUrl: images[i],
fit: BoxFit.cover,
placeholder: (_, __) => Container(color: theme.dividerColor),
errorWidget: (_, __, ___) => Container(
color: theme.dividerColor,
child: Icon(Icons.broken_image, color: theme.hintColor),
),
),
if (isLast)
Container(
color: Colors.black.withOpacity(0.55),
alignment: Alignment.center,
child: Text(
'+${images.length - 6}',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
);
},
),
),
],
const SizedBox(height: 80),
],
),
),
);
}
// ── MOBILE layout ─────────────────────────────────────────────────────
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: Stack(