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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user