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:
@@ -6,6 +6,7 @@ import '../features/events/services/events_service.dart';
|
||||
import '../features/events/models/event_models.dart';
|
||||
import 'learn_more_screen.dart';
|
||||
import '../core/app_decoration.dart';
|
||||
// landscape_section_header no longer needed for this screen
|
||||
|
||||
class CalendarScreen extends StatefulWidget {
|
||||
const CalendarScreen({Key? key}) : super(key: key);
|
||||
@@ -549,28 +550,261 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Landscape: event card for the right panel ───────────────────────────
|
||||
Widget _eventCardLandscape(EventModel e) {
|
||||
final theme = Theme.of(context);
|
||||
final imgUrl = (e.thumbImg != null && e.thumbImg!.isNotEmpty)
|
||||
? e.thumbImg!
|
||||
: (e.images.isNotEmpty ? e.images.first.image : null);
|
||||
final dateLabel = (e.startDate != null && e.endDate != null && e.startDate == e.endDate)
|
||||
? '${e.startDate}'
|
||||
: (e.startDate != null && e.endDate != null
|
||||
? '${e.startDate} – ${e.endDate}'
|
||||
: (e.startDate ?? ''));
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 10, offset: const Offset(0, 3))],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Image
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.horizontal(left: Radius.circular(14)),
|
||||
child: imgUrl != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: imgUrl,
|
||||
memCacheWidth: 300,
|
||||
memCacheHeight: 300,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, __) => Container(width: 100, height: 100, color: theme.dividerColor),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: theme.dividerColor,
|
||||
child: Icon(Icons.event, size: 32, color: theme.hintColor),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: theme.dividerColor,
|
||||
child: Icon(Icons.event, size: 32, color: theme.hintColor),
|
||||
),
|
||||
),
|
||||
// Content
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
e.title ?? e.name ?? '',
|
||||
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Date row with blue dot
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF3B82F6),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(dateLabel, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
// Venue row with green dot
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF22C55E),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(e.place ?? '', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Landscape: left panel content (calendar on white bg) ─────────────────
|
||||
Widget _landscapeLeftPanel(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
// Title
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
"Event's Calendar",
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Calendar card — reuses the mobile _calendarCard widget
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
_calendarCard(context),
|
||||
if (_loadingMonth)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
child: LinearProgressIndicator(
|
||||
color: theme.colorScheme.primary,
|
||||
backgroundColor: theme.colorScheme.primary.withOpacity(0.12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Landscape: right panel (event list for selected day) ────────────────
|
||||
Widget _landscapeRightPanel(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final dayName = DateFormat('EEEE').format(selectedDate);
|
||||
final dateFormatted = DateFormat('d MMMM, yyyy').format(selectedDate);
|
||||
final count = _eventsOfDay.length;
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Date header matching Figma: "Monday, 16 June, 2025 — 2 Events"
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$dayName, $dateFormatted',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$count ${count == 1 ? "Event" : "Events"}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Divider
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Divider(height: 1, color: theme.dividerColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Scrollable event list
|
||||
Expanded(
|
||||
child: _loadingDay
|
||||
? Center(child: CircularProgressIndicator(color: theme.colorScheme.primary))
|
||||
: _eventsOfDay.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.event_available, size: 56, color: theme.hintColor),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'No events on this date',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 32),
|
||||
itemCount: _eventsOfDay.length,
|
||||
itemBuilder: (ctx, i) => _eventCardLandscape(_eventsOfDay[i]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final isMobile = width < 700;
|
||||
final isLandscape = width >= 820;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// For non-mobile, keep original split layout
|
||||
if (!isMobile) {
|
||||
// ── LANDSCAPE layout ──────────────────────────────────────────────────
|
||||
if (isLandscape) {
|
||||
return Scaffold(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(flex: 3, child: Padding(padding: const EdgeInsets.all(24.0), child: _calendarCard(context))),
|
||||
Expanded(flex: 1, child: _detailsPanel()),
|
||||
],
|
||||
),
|
||||
body: Row(
|
||||
children: [
|
||||
// Left: Calendar panel with WHITE background (~60%)
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: RepaintBoundary(
|
||||
child: Container(
|
||||
color: theme.cardColor,
|
||||
child: _landscapeLeftPanel(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Vertical divider between panels
|
||||
VerticalDivider(width: 1, thickness: 1, color: theme.dividerColor),
|
||||
// Right: Events panel (~40%)
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: RepaintBoundary(
|
||||
child: _landscapeRightPanel(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// MOBILE layout
|
||||
// ── MOBILE layout ─────────────────────────────────────────────────────
|
||||
// (unchanged from original)
|
||||
return Scaffold(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
body: Stack(
|
||||
@@ -696,44 +930,4 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _detailsPanel() {
|
||||
final theme = Theme.of(context);
|
||||
final shortDate = DateFormat('EEE, d MMM').format(selectedDate);
|
||||
final eventsCount = _eventsOfDay.length;
|
||||
|
||||
Widget _buildHeaderCompact() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: AppDecoration.blueGradientRounded(10),
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Text('${selectedDate.day}', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 2), Text(monthNames[selectedDate.month].substring(0, 3), style: const TextStyle(color: Colors.white70, fontSize: 10))]),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(shortDate, style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 4), Text('$eventsCount ${eventsCount == 1 ? "Event" : "Events"}', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).hintColor))]),
|
||||
const Spacer(),
|
||||
IconButton(onPressed: () {}, icon: Icon(Icons.more_horiz, color: Theme.of(context).iconTheme.color)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
_buildHeaderCompact(),
|
||||
Divider(height: 1, color: theme.dividerColor),
|
||||
Expanded(
|
||||
child: _loadingDay
|
||||
? Center(child: CircularProgressIndicator(color: theme.colorScheme.primary))
|
||||
: _eventsOfDay.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: ListView.builder(padding: const EdgeInsets.only(top: 6, bottom: 24), itemCount: _eventsOfDay.length, itemBuilder: (context, idx) => _eventCardMobile(_eventsOfDay[idx])),
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user