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 9dd78be03e
commit bc6fde1b90
21 changed files with 2938 additions and 1285 deletions

View File

@@ -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])),
)
]),
);
}
}