diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7f93ec5..2d64908 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "com.sicherhaven.eventify" minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion - versionCode = 15 - versionName = "1.5(p)" + versionCode = 17 + versionName = "1.6.1(p)" } // ---------- SIGNING CONFIG ---------- @@ -51,9 +51,9 @@ android { // Use the release signing config created above signingConfig = signingConfigs.getByName("release") - isMinifyEnabled = false - isShrinkResources = false - // proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + isMinifyEnabled = true + isShrinkResources = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..e90819d --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,28 @@ +# Flutter +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } + +# Google Maps +-keep class com.google.android.gms.maps.** { *; } +-keep interface com.google.android.gms.maps.** { *; } + +# Keep annotations +-keepattributes *Annotation* +-keepattributes SourceFile,LineNumberTable + +# Play Core (deferred components) +-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication +-dontwarn com.google.android.play.core.splitinstall.SplitInstallException +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManager +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManagerFactory +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest$Builder +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest +-dontwarn com.google.android.play.core.splitinstall.SplitInstallSessionState +-dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener +-dontwarn com.google.android.play.core.tasks.OnFailureListener +-dontwarn com.google.android.play.core.tasks.OnSuccessListener +-dontwarn com.google.android.play.core.tasks.Task diff --git a/docs/hero_slider_changes.csv b/docs/hero_slider_changes.csv new file mode 100644 index 0000000..971df64 --- /dev/null +++ b/docs/hero_slider_changes.csv @@ -0,0 +1,15 @@ +Component,Old Implementation,New Implementation (matching mvnew.eventifyplus.com/home) +Hero Height,120px blue gradient banner,400px immersive full-width hero with rounded corners (20px) +Background Image,None (solid blue gradient via AppDecoration),Featured event image with CachedNetworkImage and BoxFit.cover +Ken Burns Animation,None,AnimationController 12s loop scaling 1.0 to 1.08 with easeInOut curve +Image Rotation,None,Timer.periodic every 6s cycling through first 5 events with AnimatedSwitcher crossfade +Dark Overlay,None,LinearGradient top-to-bottom rgba(0/0/0/0.3) to rgba(0/0/0/0.6) +Title Text,Welcome Back + username (14px + 22px),Featured event title (36px weight 800) or fallback Discover Amazing Events Near You +Subtitle,None,Event date + venue with calendar and location icons (14px white70) +CTA Button,None,Learn More or Explore Events button (#2563EB bg white text rounded 12px with blue glow shadow) +Indicator Dots,None,Bottom-right pill dots (active=24px white elongated / inactive=8px white40) for up to 5 events +Fallback (no events),Blue gradient with username,Blue gradient (#0F45CF to #082369) with static heading and subtitle +Featured Modal,None,showDialog with dark overlay (black87) plus 700x500 modal with event image gradient title and Learn More CTA +Close Button,None,Circular black50 button with white X icon top-right of modal +Animation Mixin,None,SingleTickerProviderStateMixin added to _HomeContentState +Disposal,None,AnimationController.dispose and Timer.cancel in dispose() diff --git a/docs/landscape_changes.csv b/docs/landscape_changes.csv new file mode 100644 index 0000000..dc9fb63 --- /dev/null +++ b/docs/landscape_changes.csv @@ -0,0 +1,27 @@ +Phase,File,Action,Priority,Description +1,lib/core/constants.dart,MODIFY,Critical,"Add desktop breakpoints: wideDesktopBreakpoint=1200, sidebarExpandedWidth=240, sidebarCollapsedWidth=72, topBarHeight=64" +1,lib/widgets/desktop_sidebar.dart,CREATE,Critical,"Collapsible sidebar with icon-only (820-1200px) and full (1200px+) modes, AnimatedContainer transitions" +1,lib/widgets/desktop_topbar.dart,CREATE,Critical,"Responsive top bar with LayoutBuilder: search field, notifications, avatar, location" +1,lib/widgets/responsive_shell.dart,CREATE,Critical,"Master responsive scaffold wrapping all desktop screens: sidebar + topbar + content" +1,lib/screens/responsive_layout.dart,MODIFY,High,"Add three-tier breakpoint support (mobile/tablet/desktop) and ScreenSize enum" +1,lib/main.dart,MODIFY,Critical,"Update StartupScreen to use ResponsiveShell for desktop routing" +2,lib/screens/home_desktop_screen.dart,REWRITE,Critical,"Split 1077-line monolith into thin orchestrator + focused content widget" +2,lib/screens/home_desktop_screen.dart,REWRITE,Critical,"Replace marquee with rich hero carousel (PageView.builder matching mobile)" +2,lib/screens/home_desktop_screen.dart,REWRITE,Critical,"Replace fixed 3-column grid with SliverGrid + SliverGridDelegateWithMaxCrossAxisExtent" +2,lib/screens/home_desktop_screen.dart,REWRITE,High,"Add MouseRegion + SystemMouseCursors.click on all tappable cards" +3,lib/screens/desktop_login_screen.dart,REWRITE,High,"Video background + glassmorphism card matching mobile login style" +3,lib/screens/desktop_login_screen.dart,REWRITE,High,"Continue as Guest button prominent" +4,lib/screens/calendar_screen.dart,MODIFY,Medium,"Remove LandscapeShell; use Row with Card-based calendar left + event list right inside ResponsiveShell" +4,lib/screens/calendar_screen.dart,MODIFY,Medium,"New _eventCardLandscape() with larger images (200px), full title, date, location" +4,lib/screens/profile_screen.dart,MODIFY,Medium,"Remove LandscapeShell; Row with profile card left + tabbed events right" +4,lib/screens/profile_screen.dart,MODIFY,Medium,"DefaultTabController with Ongoing/Upcoming/Past tabs + SliverList per tab" +4,lib/screens/contribute_screen.dart,MODIFY,Medium,"Remove LandscapeShell; Row with contributor info/nav left + tab content right" +4,lib/screens/contribute_screen.dart,MODIFY,Medium,"Vertical nav list for landscape tabs instead of horizontal TabBar" +4,lib/screens/settings_screen.dart,MODIFY,Medium,"Remove LandscapeShell; Row with settings nav left + content right" +4,lib/screens/settings_screen.dart,MODIFY,Medium,"Fix logout navigation to use unified approach" +4,lib/screens/search_screen.dart,MODIFY,Medium,"Add landscape layout: full-width search bar + Row(popular cities, results)" +4,lib/screens/learn_more_screen.dart,MODIFY,Low,"Replace hard-coded 820 with AppConstants; add RepaintBoundary; LayoutBuilder for height" +5,lib/widgets/landscape_shell.dart,DELETE,High,"Replaced by responsive_shell.dart" +5,lib/widgets/landscape_section_header.dart,DELETE or REWRITE,Low,"Generalize into SectionHeader or delete if unused" +5,lib/core/app_decoration.dart,MODIFY,Low,"Add desktop-specific decorations: cardShadow, sidebarDecoration, topBarShadow" +8,lib/features/events/services/events_service.dart,MODIFY,Critical,"Set requiresAuth: false on getEventTypes, getEventsByPincode, getEventDetails, getEventsByMonthYear" diff --git a/docs/landscape_rebuild_changes.csv b/docs/landscape_rebuild_changes.csv new file mode 100644 index 0000000..68ab28e --- /dev/null +++ b/docs/landscape_rebuild_changes.csv @@ -0,0 +1,17 @@ +Phase,File,Change Type,Description +1,lib/core/constants.dart,MODIFY,Updated sidebarExpandedWidth to 262px and removed sidebarCollapsedWidth +1,lib/widgets/desktop_sidebar.dart,REWRITE,Figma match: 262px fixed width with blue gradient and white pill selection and EVENTIFY logo with sparkle icon +1,lib/widgets/desktop_topbar.dart,REWRITE,Figma match: search input field plus notification bell plus user avatar and removed location widget +1,lib/widgets/responsive_shell.dart,REWRITE,Simplified to always show 262px sidebar on desktop and removed collapsed mode +2,lib/screens/home_desktop_screen.dart,REWRITE,Website-matching hero (400px Ken Burns animation plus dark overlay plus CTA) replacing welcome banner +2,lib/screens/home_desktop_screen.dart,MODIFY,Pill-shaped category chips (border-radius 999px) with blue active state matching website +2,lib/screens/home_desktop_screen.dart,MODIFY,Event cards enhanced with date badge overlay on image and blue/green icons for date/venue +2,lib/screens/home_desktop_screen.dart,MODIFY,Background color changed to #FAFBFC (off-white) matching website +2,lib/screens/home_desktop_screen.dart,ADD,Featured event modal dialog with large image plus gradient plus Learn More CTA plus close button +3,lib/screens/calendar_screen.dart,MODIFY,Desktop layout: calendar grid 60% left plus events panel 40% right with white background +3,lib/screens/calendar_screen.dart,MODIFY,Event cards use blue and green dot indicators for date and venue +4,lib/screens/profile_screen.dart,MODIFY,Desktop layout: full-width profile banner plus 3-column event grids for Upcoming and Past +4,lib/screens/profile_screen.dart,MODIFY,Added _buildDesktopLayout and _buildDesktopEventSection and _buildDesktopEventGridCard +5,lib/screens/learn_more_screen.dart,MODIFY,Desktop layout: full-width 300px hero image plus About/Venue two-column plus gallery strip +5,lib/screens/learn_more_screen.dart,MODIFY,Added horizontal gallery with overflow count badge and Book Your Spot CTA +6,lib/features/events/services/events_service.dart,VERIFIED,Guest access confirmed working with requiresAuth false on 4 read endpoints diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 9955151..623c801 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; class AppConstants { - // Layout + // Layout — breakpoints static const double desktopBreakpoint = 820; + static const double wideDesktopBreakpoint = 1200; static const double tabletBreakpoint = 600; + // Desktop sidebar + static const double sidebarExpandedWidth = 262; + static const double topBarHeight = 64; + static const double desktopHorizontalPadding = 24; + // Padding & Radius static const double defaultPadding = 16; static const double cardRadius = 14; diff --git a/lib/features/events/services/events_service.dart b/lib/features/events/services/events_service.dart index bc886c2..d3ccbe6 100644 --- a/lib/features/events/services/events_service.dart +++ b/lib/features/events/services/events_service.dart @@ -9,7 +9,7 @@ class EventsService { /// Get event types (POST to /events/type-list/) Future> getEventTypes() async { - final res = await _api.post(ApiEndpoints.eventTypes); + final res = await _api.post(ApiEndpoints.eventTypes, requiresAuth: false); final list = []; final data = res['event_types'] ?? res['event_types'] ?? res; if (data is List) { @@ -27,7 +27,7 @@ class EventsService { /// Get events filtered by pincode (POST to /events/pincode-events/) /// Use pincode='all' to fetch all events. Future> getEventsByPincode(String pincode) async { - final res = await _api.post(ApiEndpoints.eventsByPincode, body: {'pincode': pincode}); + final res = await _api.post(ApiEndpoints.eventsByPincode, body: {'pincode': pincode}, requiresAuth: false); final list = []; final events = res['events'] ?? res['data'] ?? []; if (events is List) { @@ -40,7 +40,7 @@ class EventsService { /// Event details Future getEventDetails(int eventId) async { - final res = await _api.post(ApiEndpoints.eventDetails, body: {'event_id': eventId}); + final res = await _api.post(ApiEndpoints.eventDetails, body: {'event_id': eventId}, requiresAuth: false); return EventModel.fromJson(Map.from(res)); } @@ -48,7 +48,7 @@ class EventsService { /// Accepts month string and year int. /// Returns Map with 'dates' (list of YYYY-MM-DD) and 'date_events' (list with counts). Future> getEventsByMonthYear(String month, int year) async { - final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}); + final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false); // expected keys: dates, total_number_of_events, date_events return res; } diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 747a537..1c0ee21 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -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 { ); } + // ── 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 { ); } - 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])), - ) - ]), - ); - } } diff --git a/lib/screens/contribute_screen.dart b/lib/screens/contribute_screen.dart index e0c324d..15e9033 100644 --- a/lib/screens/contribute_screen.dart +++ b/lib/screens/contribute_screen.dart @@ -13,6 +13,7 @@ import 'package:share_plus/share_plus.dart'; import '../core/app_decoration.dart'; import '../features/gamification/models/gamification_models.dart'; import '../features/gamification/providers/gamification_provider.dart'; +import '../widgets/landscape_section_header.dart'; // ───────────────────────────────────────────────────────────────────────────── // Tier colour map @@ -138,156 +139,184 @@ class _ContributeScreenState extends State static const _desktopTabIcons = [Icons.edit_note, null, null]; Widget _buildDesktopLayout(BuildContext context, GamificationProvider provider) { + return Row( + children: [ + Flexible( + flex: 2, + child: RepaintBoundary( + child: Container( + decoration: AppDecoration.blueGradient, + child: _buildContributeLeftPanel(context, provider), + ), + ), + ), + Flexible( + flex: 3, + child: RepaintBoundary( + child: _buildContributeRightPanel(context, provider), + ), + ), + ], + ); + } + + // ── Landscape left panel: contributor info + vertical nav ─────────────── + Widget _buildContributeLeftPanel(BuildContext context, GamificationProvider provider) { final profile = provider.profile; final tier = profile?.tier ?? ContributorTier.BRONZE; final lifetimeEp = profile?.lifetimeEp ?? 0; - final currentEp = profile?.currentEp ?? 0; - final currentRp = profile?.currentRp ?? 0; - - // Calculate next tier threshold const thresholds = [0, 100, 500, 1500, 5000]; final tierIdx = tier.index; final nextThresh = tierIdx < 4 ? thresholds[tierIdx + 1] : thresholds[4]; final prevThresh = thresholds[tierIdx]; final progress = tierIdx >= 4 ? 1.0 : (lifetimeEp - prevThresh) / (nextThresh - prevThresh); + final tierColor = _tierColors[tier] ?? Colors.white; - return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24), - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 900), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Title - const Text('Contributor Dashboard', - style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, color: Color(0xFF111827))), - const SizedBox(height: 6), - const Text('Track your impact, earn rewards, and climb the ranks!', - style: TextStyle(fontSize: 14, color: Color(0xFF6B7280))), - const SizedBox(height: 24), - - // ── Desktop Tab bar (3 tabs in blue pill) ── - Container( - decoration: BoxDecoration( - color: _primary, - borderRadius: BorderRadius.circular(16), - ), - padding: const EdgeInsets.all(5), - child: Row( - children: List.generate(_desktopTabs.length, (i) { - final isActive = _activeTab == i; - return Expanded( - child: GestureDetector( - onTap: () => setState(() => _activeTab = i), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(vertical: 14), - decoration: BoxDecoration( - color: isActive ? Colors.white : Colors.transparent, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (i == 0) ...[ - Icon(Icons.edit_note, size: 18, - color: isActive ? _primary : Colors.white70), - const SizedBox(width: 6), - ], - Text( - _desktopTabs[i], - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: isActive ? _primary : Colors.white, - ), - ), - ], - ), - ), - ), - ); - }), - ), + return SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + // Title + const Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Text( + 'Contributor\nDashboard', + style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w800, height: 1.2), ), - const SizedBox(height: 20), + ), + const SizedBox(height: 6), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Text( + 'Track your impact & earn rewards', + style: TextStyle(color: Colors.white70, fontSize: 13), + ), + ), + const SizedBox(height: 24), - // ── Contributor Level card ── - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), + // Contributor Level badge + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF0F45CF), Color(0xFF3B82F6)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(16), + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: tierColor.withOpacity(0.5)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Contributor Level', - style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w700)), - const SizedBox(height: 4), - const Text('Start earning rewards by contributing!', - style: TextStyle(color: Colors.white70, fontSize: 13)), - ], + Row(children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: tierColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: tierColor.withOpacity(0.6)), ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Text(tierLabel(tier), - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13)), - ), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('$lifetimeEp pts', - style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)), - if (tierIdx < 4) - Text('Next: ${tierLabel(ContributorTier.values[tierIdx + 1])} (${thresholds[tierIdx + 1]} pts)', - style: const TextStyle(color: Colors.white70, fontSize: 13)), - ], - ), - const SizedBox(height: 8), + child: Text(tierLabel(tier), style: TextStyle(color: tierColor, fontWeight: FontWeight.w700, fontSize: 12)), + ), + const Spacer(), + Text('$lifetimeEp pts', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14)), + ]), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: progress.clamp(0.0, 1.0), - minHeight: 8, - backgroundColor: Colors.white.withOpacity(0.2), - valueColor: const AlwaysStoppedAnimation(Colors.white), + minHeight: 6, + backgroundColor: Colors.white24, + valueColor: AlwaysStoppedAnimation(tierColor), ), ), + if (tierIdx < 4) ...[ + const SizedBox(height: 6), + Text( + 'Next: ${tierLabel(ContributorTier.values[tierIdx + 1])} at ${thresholds[tierIdx + 1]} pts', + style: const TextStyle(color: Colors.white54, fontSize: 11), + ), + ], ], ), ), - const SizedBox(height: 20), + ), + const SizedBox(height: 24), - // ── Desktop tab body ── - _buildDesktopTabBody(context, provider), - ], - ), + // Vertical tab navigation + ...List.generate(_desktopTabs.length, (i) { + final isActive = _activeTab == i; + final icons = [Icons.edit_note, Icons.leaderboard_outlined, Icons.emoji_events_outlined]; + return GestureDetector( + onTap: () => setState(() => _activeTab = i), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.fromLTRB(16, 0, 16, 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: isActive ? Colors.white : Colors.white.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + ), + child: Row(children: [ + Icon(icons[i], size: 20, color: isActive ? _primary : Colors.white70), + const SizedBox(width: 12), + Text( + _desktopTabs[i], + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: isActive ? _primary : Colors.white, + ), + ), + const Spacer(), + if (isActive) Icon(Icons.chevron_right, size: 18, color: _primary), + ]), + ), + ); + }), + const SizedBox(height: 24), + ], ), ), ); } + // ── Landscape right panel: active tab content ──────────────────────────── + Widget _buildContributeRightPanel(BuildContext context, GamificationProvider provider) { + String title; + String subtitle; + switch (_activeTab) { + case 1: + title = 'Leaderboard'; + subtitle = 'Top contributors this month'; + break; + case 2: + title = 'Achievements'; + subtitle = 'Your earned badges'; + break; + default: + title = 'Submit Event'; + subtitle = 'Share events with the community'; + } + + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + LandscapeSectionHeader(title: title, subtitle: subtitle), + Expanded( + child: RepaintBoundary( + child: _buildDesktopTabBody(context, provider), + ), + ), + ], + ), + ); + } + Widget _buildDesktopTabBody(BuildContext context, GamificationProvider provider) { switch (_activeTab) { case 0: diff --git a/lib/screens/home_desktop_screen.dart b/lib/screens/home_desktop_screen.dart index 4be1638..ef0c834 100644 --- a/lib/screens/home_desktop_screen.dart +++ b/lib/screens/home_desktop_screen.dart @@ -3,1011 +3,43 @@ import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:provider/provider.dart'; import '../features/events/models/event_models.dart'; import '../features/events/services/events_service.dart'; +import '../features/gamification/providers/gamification_provider.dart'; +import '../widgets/responsive_shell.dart'; import 'calendar_screen.dart'; import 'profile_screen.dart'; import 'booking_screen.dart'; import 'settings_screen.dart'; import 'learn_more_screen.dart'; import 'contribute_screen.dart'; -import '../core/app_decoration.dart'; -import '../features/gamification/providers/gamification_provider.dart'; -import 'package:provider/provider.dart'; class HomeDesktopScreen extends StatefulWidget { final bool skipSidebarEntranceAnimation; - const HomeDesktopScreen({Key? key, this.skipSidebarEntranceAnimation = false}) : super(key: key); + const HomeDesktopScreen({Key? key, this.skipSidebarEntranceAnimation = false}) + : super(key: key); @override State createState() => _HomeDesktopScreenState(); } -class _HomeDesktopScreenState extends State with SingleTickerProviderStateMixin { - final EventsService _eventsService = EventsService(); - - // Navigation state - int selectedMenu = 0; // 0 = Home, 1 = Calendar, 2 = Profile, 3 = Bookings, 4 = Contribute, 5 = Settings - - // Backend-driven data for Home - List _events = []; - List _types = []; - bool _loading = true; - bool _loadingTypes = true; - - // Selection: either a backend id (event type) or a fixed label filter. - int _SelectedTypeId = -1; - String? _selectedTypeLabel; - - // fixed categories required by product - final List _fixedCategories = [ - 'All Events', - ]; - - // User prefs - String _username = 'Guest'; - String _location = 'Unknown'; - String _pincode = 'all'; - String? _profileImage; - - // Sidebar text animation - late final AnimationController _sidebarTextController; - late final Animation _sidebarTextOpacity; - - // Sidebar width constant (used when computing main content width) - static const double _sidebarWidth = 220; - - // Topbar compact breakpoint (content width) - static const double _compactTopBarWidth = 720; - - // --- marquee (featured events) fields --- - late final ScrollController _marqueeController; - Timer? _marqueeTimer; - // Speed in pixels per second - double _marqueeSpeed = 40.0; - final Duration _marqueeTick = const Duration(milliseconds: 16); +class _HomeDesktopScreenState extends State { + int _selectedMenu = 0; @override - void initState() { - super.initState(); - - _sidebarTextController = AnimationController(vsync: this, duration: const Duration(milliseconds: 420)); - _sidebarTextOpacity = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _sidebarTextController, curve: Curves.easeOut)); - - _marqueeController = ScrollController(); - - if (widget.skipSidebarEntranceAnimation) { - _sidebarTextController.value = 1.0; - } else { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) _sidebarTextController.forward(); - }); - } - - // load initial data for home only - _loadPreferencesAndData(); - } - - @override - void dispose() { - _sidebarTextController.dispose(); - _marqueeTimer?.cancel(); - _marqueeController.dispose(); - super.dispose(); - } - - // ------------------------ Data loaders ------------------------ - Future _loadPreferencesAndData() async { - setState(() { - _loading = true; - _loadingTypes = true; - }); - - final prefs = await SharedPreferences.getInstance(); - // UI display name should come from display_name (profile). Backend identity is separate. - _username = prefs.getString('display_name') ?? prefs.getString('username') ?? 'Jane Doe'; - _location = prefs.getString('location') ?? 'Whitefield, Bengaluru'; - _pincode = prefs.getString('pincode') ?? 'all'; - _profileImage = prefs.getString('profileImage'); - - await Future.wait([ - _fetchEventTypes(), - _fetchEventsByPincode(_pincode), - ]); - - if (mounted) setState(() { - _loading = false; - _loadingTypes = false; - }); - - // start marquee when we have events - _restartMarquee(); - } - - Future _fetchEventTypes() async { - try { - final types = await _eventsService.getEventTypes(); - if (mounted) setState(() => _types = types); - } catch (e) { - if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to load categories: ${e.toString()}'))); - } - } - - Future _fetchEventsByPincode(String pincode) async { - try { - final events = await _eventsService.getEventsByPincode(pincode); - if (mounted) setState(() => _events = events); - } catch (e) { - if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to load events: ${e.toString()}'))); - } - - // ensure marquee restarts when event list changes - _restartMarquee(); - } - - /// Public refresh entry used by UI (pull-to-refresh). - Future _refreshHome() async { - // Prevent duplicate refresh triggers - if (_loading) return; - - setState(() => _loading = true); - try { - await _loadPreferencesAndData(); - } finally { - // _loadPreferencesAndData normally clears _loading, but ensure we reset if needed. - if (mounted && _loading) setState(() => _loading = false); - } - } - - // ------------------------ Helpers ------------------------ - String? _chooseEventImage(EventModel e) { - if (e.thumbImg != null && e.thumbImg!.trim().isNotEmpty) return e.thumbImg!.trim(); - if (e.images.isNotEmpty && e.images.first.image.trim().isNotEmpty) return e.images.first.image.trim(); - return null; - } - - Widget _profileAvatar() { - // If profile image exists and is a network URL -> show it - if (_profileImage != null && _profileImage!.trim().isNotEmpty) { - final url = _profileImage!.trim(); - - if (url.startsWith('http')) { - return CircleAvatar( - radius: 20, - backgroundColor: Colors.grey.shade200, - backgroundImage: NetworkImage(url), - onBackgroundImageError: (_, __) {}, - child: const Icon(Icons.person, color: Colors.transparent), - ); - } - } - - // Fallback → initials (clean & readable) - final name = _username.trim(); - String initials = 'U'; - - if (name.isNotEmpty) { - if (name.contains('@')) { - // Email → first letter only - initials = name[0].toUpperCase(); - } else { - final parts = name.split(' ').where((p) => p.isNotEmpty).toList(); - initials = parts.isEmpty ? 'U' : parts.take(2).map((p) => p[0].toUpperCase()).join(); - } - } - - return CircleAvatar( - radius: 20, - backgroundColor: Colors.blue.shade600, - child: Text( - initials, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), + Widget build(BuildContext context) { + return ResponsiveShell( + currentIndex: _selectedMenu, + onIndexChanged: (idx) => setState(() => _selectedMenu = idx), + showTopBar: true, + child: _getCurrentPage(), ); } - // ------------------------ Marquee control ------------------------ - void _restartMarquee() { - // stop previous timer - _marqueeTimer?.cancel(); - if (_events.isEmpty) { - // reset position - if (_marqueeController.hasClients) _marqueeController.jumpTo(0); - return; - } - - // Wait for next frame so scroll metrics are available (after the duplicated row is built) - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - if (!_marqueeController.hasClients) { - // Try again next frame - WidgetsBinding.instance.addPostFrameCallback((_) => _restartMarquee()); - return; - } - - final maxScroll = _marqueeController.position.maxScrollExtent; - if (maxScroll <= 0) { - // Nothing to scroll - return; - } - - // Start a periodic timer that increments offset smoothly - final tickSeconds = _marqueeTick.inMilliseconds / 1000.0; - final delta = _marqueeSpeed * tickSeconds; - - _marqueeTimer?.cancel(); - _marqueeTimer = Timer.periodic(_marqueeTick, (_) { - if (!mounted || !_marqueeController.hasClients) return; - - final cur = _marqueeController.offset; - final max = _marqueeController.position.maxScrollExtent; - final half = max / 2.0; // because we duplicate the list twice - - double next = cur + delta; - if (next >= max) { - // wrap back by half (seamless loop — instant jump is intentional) - final wrapped = next - half; - _marqueeController.jumpTo(wrapped.clamp(0.0, max)); - } else { - // smooth incremental scroll synced to ticker duration - _marqueeController.animateTo( - next, - duration: _marqueeTick, - curve: Curves.linear, - ); - } - }); - }); - } - - // ------------------------ Filtering logic ------------------------ - List get _filteredEvents { - if (_selectedTypeLabel != null && _selectedTypeLabel!.toLowerCase() != 'all events') { - final label = _selectedTypeLabel!.toLowerCase(); - return _events.where((e) { - final nameFromBackend = (e.eventTypeId != null && e.eventTypeId! > 0) - ? (_types.firstWhere((t) => t.id == e.eventTypeId, orElse: () => EventTypeModel(id: -1, name: '', iconUrl: null)).name) - : ''; - final candidateNames = [ - e.venueName, - e.title, - e.name, - nameFromBackend, - ]; - return candidateNames.any((c) => c != null && c.toLowerCase().contains(label)); - }).toList(); - } - - if (_SelectedTypeId != -1) { - return _events.where((e) => e.eventTypeId == _SelectedTypeId).toList(); - } - - return _events; - } - - void _selectBackendType(int id) { - setState(() { - _SelectedTypeId = id; - _selectedTypeLabel = null; - }); - } - - void _selectFixedLabel(String label) { - setState(() { - if (label.toLowerCase() == 'all events') { - _SelectedTypeId = -1; - _selectedTypeLabel = null; - } else { - _SelectedTypeId = -1; - _selectedTypeLabel = label; - } - }); - } - - // ------------------------ Left panel (nav) ------------------------ - Widget _buildLeftPanel() { - final theme = Theme.of(context); - // Layout: top area (logo + nav list) scrolls if necessary, bottom area (host panel + settings) remains pinned. - return Container( - width: _sidebarWidth, - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/images/gradient_dark_blue.png'), - fit: BoxFit.cover, - ), - ), - child: SafeArea( - child: Column( - children: [ - const SizedBox(height: 18), - FadeTransition( - opacity: _sidebarTextOpacity, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Text('EVENTIFY', style: theme.textTheme.titleMedium?.copyWith(color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold, fontSize: 18)), - ), - ), - ), - const SizedBox(height: 16), - - // Expandable nav list - Expanded( - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _navItem(icon: Icons.home, label: 'Home', idx: 0), - _navItem(icon: Icons.location_on, label: 'Events near you', idx: 0), - _navItem(icon: Icons.calendar_today, label: 'Upcoming events', idx: 0), - _navItem(icon: Icons.calendar_view_month, label: 'Calendar', idx: 1), - - // <-- Profile between Calendar and Contribute - _navItem(icon: Icons.person, label: 'Profile', idx: 2), - - _navItem(icon: Icons.add, label: 'Contribute', idx: 4), - const SizedBox(height: 12), - // optionally more items... - ], - ), - ), - ), - - // bottom pinned area - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Column( - children: [ - _buildHostPanel(), - const SizedBox(height: 12), - _navItem(icon: Icons.settings, label: 'Settings', idx: 5), - const SizedBox(height: 12), - ], - ), - ), - ], - ), - ), - ); - } - - Widget _buildHostPanel() { - final theme = Theme.of(context); - return Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - image: const DecorationImage( - image: AssetImage('assets/images/gradient_dark_blue.png'), - fit: BoxFit.cover, - ), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Hosting a private or ticketed event?', style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold)), - const SizedBox(height: 6), - Text('Schedule a call back for setting up event.', style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onPrimary.withOpacity(0.85), fontSize: 12)), - const SizedBox(height: 10), - ElevatedButton(onPressed: () {}, style: ElevatedButton.styleFrom(backgroundColor: theme.colorScheme.primary), child: Text('Schedule Call', style: theme.textTheme.labelLarge?.copyWith(color: theme.colorScheme.onPrimary))), - ], - ), - ); - } - - Widget _navItem({required IconData icon, required String label, required int idx}) { - final theme = Theme.of(context); - final selected = selectedMenu == idx; - final color = selected ? theme.colorScheme.onPrimary : theme.colorScheme.onPrimary.withOpacity(0.9); - return ListTile( - leading: Icon(icon, color: color), - title: Text(label, style: TextStyle(color: color)), - dense: true, - onTap: () { - setState(() { - selectedMenu = idx; - }); - if (idx == 0) _refreshHome(); - }, - ); - } - - // ------------------------ Top bar (common, responsive) ------------------------ - Widget _buildTopBar() { - // The top bar sits inside the white content area (to the right of the sidebar). - // The search box is centered inside the content area (slightly toward the right because of fixed width). - final theme = Theme.of(context); - return LayoutBuilder(builder: (context, constraints) { - final isCompact = constraints.maxWidth < _compactTopBarWidth; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 18), - child: Row( - children: [ - // Left: location or icon when compact - if (!isCompact) - _fullLocationWidget() - else - IconButton( - tooltip: 'Location', - onPressed: () {}, - icon: Icon(Icons.location_on, color: theme.iconTheme.color), - ), - - const SizedBox(width: 12), - - // Center: place the search bar centered in content area using Center + fixed width box. - if (!isCompact) - Expanded( - child: Align( - alignment: Alignment.center, - child: Padding( - // ⬇️ Increase LEFT padding to move search bar more to the right - padding: const EdgeInsets.only(left: 120), - child: SizedBox( - width: 520, - height: 44, - child: TextField( - decoration: InputDecoration( - filled: true, - fillColor: theme.cardColor, - prefixIcon: Icon(Icons.search, color: theme.hintColor), - hintText: 'Search events, pages, features...', - hintStyle: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - ), - style: theme.textTheme.bodyLarge, - ), - ), - ), - ), - ) - else - IconButton( - tooltip: 'Search', - onPressed: () {}, - icon: Icon(Icons.search, color: theme.iconTheme.color), - ), - - // Spacer ensures right side stays right-aligned - const Spacer(), - - // Right: notifications + (optionally username) + avatar - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - children: [ - IconButton(onPressed: () {}, icon: Icon(Icons.notifications_none, color: theme.iconTheme.color)), - Positioned(right: 6, top: 6, child: CircleAvatar(radius: 8, backgroundColor: Colors.red, child: Text('2', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white, fontSize: 10)))), - ], - ), - const SizedBox(width: 8), - if (!isCompact) ...[ - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(_username, style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)), - Text('Explorer', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 12)), - ]), - const SizedBox(width: 8), - ], - GestureDetector(onTap: () => _openProfile(), child: _profileAvatar()), - ], - ), - ], - ), - ); - }); - } - - Widget _fullLocationWidget() { - final theme = Theme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Location', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 12)), - const SizedBox(height: 4), - Row(children: [ - Icon(Icons.location_on, size: 18, color: theme.hintColor), - const SizedBox(width: 6), - Text(_location, style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600)), - const SizedBox(width: 6), - Icon(Icons.arrow_drop_down, size: 18, color: theme.hintColor), - ]), - ], - ); - } - - void _openProfile() { - setState(() => selectedMenu = 2); - } - - // ------------------------ Home content (index 0) ------------------------ - Widget _homeContent() { - final theme = Theme.of(context); - - // Entire home content is a vertical scrollable wrapped in RefreshIndicator for pull-to-refresh behaviour. - return RefreshIndicator( - onRefresh: _refreshHome, - color: theme.colorScheme.primary, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - - // HERO / welcome panel - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Container( - padding: const EdgeInsets.all(20), - decoration: AppDecoration.blueGradientRounded(16), - child: Row( - children: [ - // left welcome - Expanded( - flex: 6, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Welcome Back,', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white.withOpacity(0.9), fontSize: 16)), - const SizedBox(height: 6), - Text(_username, style: const TextStyle(color: Colors.white, fontSize: 30, fontWeight: FontWeight.bold)), - ]), - ), - - const SizedBox(width: 16), - - // right: horizontal featured events (backend-driven) - Expanded( - flex: 4, - child: SizedBox( - height: 90, - child: _events.isEmpty - ? const SizedBox() - : _buildMarqueeFeaturedEvents(), - ), - ), - ], - ), - ), - ), - - const SizedBox(height: 18), - - // Events header - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 8), - child: Row(children: [ - Expanded(child: Text('Events Around You', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold))), - TextButton(onPressed: () {}, child: Text('View All', style: theme.textTheme.bodyMedium)), - ]), - ), - - // type chips: fixed categories first, then backend categories - if (!_loadingTypes) SizedBox(height: 56, child: _buildTypeChips()), - - const SizedBox(height: 14), - - // events area: use LayoutBuilder to decide between grid and horizontal scroll - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: LayoutBuilder( - builder: (context, constraints) { - // constraints.maxWidth is the available width inside the white content area (after padding) - return _buildEventsArea(constraints.maxWidth); - }, - ), - ), - - const SizedBox(height: 40), - ], - ), - ), - ); - } - - /// Build marquee (continuous leftward scroll) by duplicating the events list and using a ScrollController. - Widget _buildMarqueeFeaturedEvents() { - // Duplicate events for seamless loop - final display = []; - display.addAll(_events); - display.addAll(_events); - - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: SingleChildScrollView( - controller: _marqueeController, - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - child: Row( - children: List.generate(display.length, (i) { - final e = display[i]; - final img = _chooseEventImage(e); - return Padding( - padding: const EdgeInsets.only(right: 12.0), - child: SizedBox(width: 220, child: _miniEventCard(e, img)), - ); - }), - ), - ), - ); - } - - Widget _buildTypeChips() { - final chips = []; - - // fixed categories first - for (final name in _fixedCategories) { - final isSelected = _selectedTypeLabel != null ? _selectedTypeLabel == name : (name == 'All Events' && _SelectedTypeId == -1 && _selectedTypeLabel == null); - chips.add(_chipWidget( - label: name, - onTap: () => _selectFixedLabel(name), - selected: isSelected, - )); - } - - // then backend categories (if any) - for (final t in _types) { - final isSelected = (_selectedTypeLabel == null && _SelectedTypeId == t.id); - chips.add(_chipWidget(label: t.name, onTap: () => _selectBackendType(t.id), selected: isSelected)); - } - - return ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 24), - scrollDirection: Axis.horizontal, - itemBuilder: (_, idx) => chips[idx], - separatorBuilder: (_, __) => const SizedBox(width: 8), - itemCount: chips.length, - ); - } - - Widget _chipWidget({ - required String label, - required VoidCallback onTap, - required bool selected, - }) { - final theme = Theme.of(context); - return InkWell( - borderRadius: BorderRadius.circular(10), - onTap: onTap, - child: Container( - height: 40, // 👈 fixed height for all chips - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: selected ? theme.colorScheme.primary.withOpacity(0.08) : theme.cardColor, - borderRadius: BorderRadius.circular(10), // 👈 box shape (not pill) - border: Border.all( - color: selected ? theme.colorScheme.primary : theme.dividerColor, - width: 1, - ), - ), - child: Text( - label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: selected ? theme.colorScheme.primary : theme.textTheme.bodyLarge?.color, - ), - ), - ), - ); - } - - // ------------------------ - // EVENTS AREA: fixed 3-columns grid on wide widths; fallback horizontal on narrow widths. - // ------------------------ - Widget _buildEventsArea(double contentWidth) { - // Preferred card width you'd like to keep (matches your reference) - const double preferredCardWidth = 360.0; - const double cardHeight = 220.0; // matches reference proportions - const double spacing = 18.0; - - final list = _filteredEvents; - final theme = Theme.of(context); - - if (_loading) { - return Padding(padding: const EdgeInsets.only(top: 32), child: Center(child: CircularProgressIndicator(color: theme.colorScheme.primary))); - } - if (list.isEmpty) { - return Padding(padding: const EdgeInsets.symmetric(vertical: 40), child: Center(child: Text('No events available', style: theme.textTheme.bodyMedium))); - } - - // Determine if we have space to show exactly 3 columns. - // Required width for 3 columns = 3 * preferredCardWidth + 2 * spacing - final double requiredWidthForThree = preferredCardWidth * 3 + spacing * 2; - - if (contentWidth >= requiredWidthForThree) { - // Enough space: force a 3-column grid (this ensures exactly 3 cards per row) - return GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: list.length, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, // always 3 when space allows - mainAxisExtent: cardHeight, - crossAxisSpacing: spacing, - mainAxisSpacing: spacing, - ), - itemBuilder: (ctx, idx) { - final e = list[idx]; - final img = _chooseEventImage(e); - return _eventCardForGrid(e, img); - }, - ); - } else { - // Not enough space for 3 columns — use horizontal scroll preserving card width. - // This keeps behaviour sensible on narrower desktop windows. - final double preferredWidth = preferredCardWidth; - return SizedBox( - height: cardHeight, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: list.length, - separatorBuilder: (_, __) => const SizedBox(width: spacing), - itemBuilder: (ctx, idx) { - final e = list[idx]; - final img = _chooseEventImage(e); - return SizedBox(width: preferredWidth, child: _eventCardForFixedSize(e, img, preferredWidth)); - }, - ), - ); - } - } - - // small horizontal card used inside HERO right area - Widget _miniEventCard(EventModel e, String? img) { - final theme = Theme.of(context); - return Container( - width: 220, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(12)), - child: Row(children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: img != null - ? CachedNetworkImage( - imageUrl: img, - memCacheWidth: 128, - memCacheHeight: 128, - width: 64, - height: 64, - fit: BoxFit.cover, - placeholder: (_, __) => Container(width: 64, height: 64, color: Theme.of(context).dividerColor), - errorWidget: (_, __, ___) => Container(width: 64, height: 64, color: Theme.of(context).dividerColor, child: const Icon(Icons.event, size: 24, color: Colors.grey)), - ) - : Container(width: 64, height: 64, color: Theme.of(context).dividerColor), - ), - const SizedBox(width: 8), - Expanded( - child: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(e.title ?? e.name ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)), - const SizedBox(height: 6), - Text(e.startDate ?? '', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).hintColor, fontSize: 12)), - ]), - ), - ]), - ); - } - - // ---------- Cards ---------- - // Card used in grid (we can rely on grid cell having fixed height) - Widget _eventCardForGrid(EventModel e, String? img) { - final theme = Theme.of(context); - - // Styling constants to match reference - const double cardRadius = 16.0; - const double imageHeight = 140.0; - const double horizontalPadding = 12.0; - const double verticalPadding = 10.0; - - // Friendly formatted date label - 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.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))), - child: Container( - decoration: BoxDecoration( - color: theme.cardColor, - borderRadius: BorderRadius.circular(cardRadius), - boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 18, offset: const Offset(0, 10)), - BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 6, offset: const Offset(0, 3)), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // image flush to top corners - ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(cardRadius)), - child: img != null - ? CachedNetworkImage( - imageUrl: img, - memCacheWidth: 600, - memCacheHeight: 280, - width: double.infinity, - height: imageHeight, - fit: BoxFit.cover, - placeholder: (_, __) => Container(height: imageHeight, color: theme.dividerColor), - errorWidget: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor), - ) - : Container(height: imageHeight, width: double.infinity, color: theme.dividerColor), - ), - - // content area - Padding( - padding: const EdgeInsets.fromLTRB(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Title (2 lines) - Text( - e.title ?? e.name ?? '', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700, fontSize: 14), - ), - - const SizedBox(height: 8), - - // single row: date • location (icons small, subtle) - Row( - children: [ - // calendar small badge - Container( - width: 18, - height: 18, - decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)), - child: Icon(Icons.calendar_today, size: 12, color: theme.colorScheme.primary), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - dateLabel, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13), - ), - ), - - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Text('•', style: TextStyle(color: Colors.black26)), - ), - - Container( - width: 18, - height: 18, - decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)), - child: Icon(Icons.location_on, size: 12, color: theme.colorScheme.primary), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - e.place ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } - - // Card used in horizontal list (fixed width) - Widget _eventCardForFixedSize(EventModel e, String? img, double width) { - final theme = Theme.of(context); - - // Styling constants to match grid card - const double cardRadius = 16.0; - const double imageHeight = 140.0; - const double horizontalPadding = 12.0; - const double verticalPadding = 10.0; - - 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.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))), - child: Container( - width: width, - decoration: BoxDecoration( - color: theme.cardColor, - borderRadius: BorderRadius.circular(cardRadius), - boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 18, offset: const Offset(0, 10)), - BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 6, offset: const Offset(0, 3)), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(cardRadius)), - child: img != null - ? CachedNetworkImage( - imageUrl: img, - memCacheWidth: 600, - memCacheHeight: 296, - width: width, - height: imageHeight, - fit: BoxFit.cover, - placeholder: (_, __) => Container(height: imageHeight, color: theme.dividerColor), - errorWidget: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor), - ) - : Container(height: imageHeight, width: width, color: theme.dividerColor), - ), - - Padding( - padding: const EdgeInsets.fromLTRB(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - e.title ?? e.name ?? '', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700, fontSize: 14), - ), - const SizedBox(height: 8), - Row( - children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)), - child: Icon(Icons.calendar_today, size: 12, color: theme.colorScheme.primary), - ), - const SizedBox(width: 8), - Flexible(child: Text(dateLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13))), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Text('•', style: TextStyle(color: Colors.black26)), - ), - Container( - width: 18, - height: 18, - decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)), - child: Icon(Icons.location_on, size: 12, color: theme.colorScheme.primary), - ), - const SizedBox(width: 8), - Expanded(child: Text(e.place ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13))), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } - - // ------------------------ Page routing ------------------------ Widget _getCurrentPage() { - switch (selectedMenu) { + switch (_selectedMenu) { case 1: return const CalendarScreen(); case 2: @@ -1022,31 +54,835 @@ class _HomeDesktopScreenState extends State with SingleTicker case 5: return const SettingsScreen(); default: - return _homeContent(); + return _HomeContent( + onEventTap: (eventId) { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: eventId)), + ); + }, + ); + } + } +} + +// --------------------------------------------------------------------------- +// Home content — hero, categories, event grid +// --------------------------------------------------------------------------- +class _HomeContent extends StatefulWidget { + final void Function(int eventId) onEventTap; + const _HomeContent({required this.onEventTap}); + + @override + State<_HomeContent> createState() => _HomeContentState(); +} + +class _HomeContentState extends State<_HomeContent> + with SingleTickerProviderStateMixin { + final EventsService _eventsService = EventsService(); + + List _events = []; + List _types = []; + bool _loading = true; + bool _loadingTypes = true; + + int _selectedTypeId = -1; + String? _selectedTypeLabel; + + String _username = 'Guest'; + String _pincode = 'all'; + + // Ken Burns hero animation + late AnimationController _heroAnimController; + late Animation _heroScaleAnim; + int _heroImageIndex = 0; + Timer? _heroRotateTimer; + + @override + void initState() { + super.initState(); + _heroAnimController = AnimationController( + vsync: this, + duration: const Duration(seconds: 12), + ); + _heroScaleAnim = Tween(begin: 1.0, end: 1.08).animate( + CurvedAnimation(parent: _heroAnimController, curve: Curves.easeInOut), + ); + _heroAnimController.repeat(reverse: true); + _loadData(); + } + + @override + void dispose() { + _heroAnimController.dispose(); + _heroRotateTimer?.cancel(); + super.dispose(); + } + + void _startHeroRotation() { + _heroRotateTimer?.cancel(); + if (_events.length <= 1) return; + _heroRotateTimer = Timer.periodic(const Duration(seconds: 6), (_) { + if (!mounted) return; + setState(() { + _heroImageIndex = (_heroImageIndex + 1) % _events.length.clamp(1, 5); + }); + }); + } + + Future _loadData() async { + setState(() { + _loading = true; + _loadingTypes = true; + }); + + final prefs = await SharedPreferences.getInstance(); + _username = prefs.getString('display_name') ?? + prefs.getString('username') ?? + 'Guest'; + _pincode = prefs.getString('pincode') ?? 'all'; + + await Future.wait([ + _fetchEventTypes(), + _fetchEvents(), + ]); + + if (mounted) { + setState(() { + _loading = false; + _loadingTypes = false; + }); + _startHeroRotation(); } } - // ------------------------ Build ------------------------ + Future _fetchEventTypes() async { + try { + final types = await _eventsService.getEventTypes(); + if (mounted) setState(() => _types = types); + } catch (_) {} + } + + Future _fetchEvents() async { + try { + final events = await _eventsService.getEventsByPincode(_pincode); + if (mounted) setState(() => _events = events); + } catch (_) {} + } + + List get _filteredEvents { + if (_selectedTypeLabel != null && + _selectedTypeLabel!.toLowerCase() != 'all events') { + final label = _selectedTypeLabel!.toLowerCase(); + return _events.where((e) { + final typeName = (e.eventTypeId != null && e.eventTypeId! > 0) + ? (_types + .firstWhere((t) => t.id == e.eventTypeId, + orElse: () => + EventTypeModel(id: -1, name: '', iconUrl: null)) + .name) + : ''; + return [e.venueName, e.title, e.name, typeName] + .any((c) => c != null && c.toLowerCase().contains(label)); + }).toList(); + } + if (_selectedTypeId != -1) { + return _events.where((e) => e.eventTypeId == _selectedTypeId).toList(); + } + return _events; + } + + String? _chooseImage(EventModel e) { + if (e.thumbImg != null && e.thumbImg!.trim().isNotEmpty) { + return e.thumbImg!.trim(); + } + if (e.images.isNotEmpty && e.images.first.image.trim().isNotEmpty) { + return e.images.first.image.trim(); + } + return null; + } + + String _formatDateBadge(String dateStr) { + try { + final parts = dateStr.split('-'); + if (parts.length == 3) { + final day = int.parse(parts[2]); + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + final month = months[int.parse(parts[1]) - 1]; + return '$day\n$month'; + } + } catch (_) {} + return dateStr; + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); - return Scaffold( - backgroundColor: theme.scaffoldBackgroundColor, - body: Row( - children: [ - _buildLeftPanel(), - Expanded( - child: Column( + return Container( + color: const Color(0xFFFAFBFC), + child: RefreshIndicator( + onRefresh: _loadData, + color: theme.colorScheme.primary, + child: CustomScrollView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + slivers: [ + // Hero section + SliverToBoxAdapter(child: _buildHeroSection(theme)), + const SliverToBoxAdapter(child: SizedBox(height: 24)), + + // Type chips + if (!_loadingTypes) + SliverToBoxAdapter( + child: SizedBox(height: 48, child: _buildTypeChips(theme)), + ), + + const SliverToBoxAdapter(child: SizedBox(height: 20)), + + // Section header + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 8), + child: Row(children: [ + Expanded( + child: Text( + 'Events Around You', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: const Color(0xFF111827), + ), + ), + ), + TextButton( + onPressed: () {}, + child: Text( + 'View All', + style: theme.textTheme.bodyMedium?.copyWith( + color: const Color(0xFF2563EB), + fontWeight: FontWeight.w600, + ), + ), + ), + ]), + ), + ), + + const SliverToBoxAdapter(child: SizedBox(height: 4)), + + // Event grid + _buildEventGrid(theme), + + const SliverToBoxAdapter(child: SizedBox(height: 40)), + ], + ), + ), + ); + } + + // ──── Hero Section (website-style: large image + Ken Burns + overlay) ──── + Widget _buildHeroSection(ThemeData theme) { + final heroEvent = _events.isNotEmpty + ? _events[_heroImageIndex.clamp(0, _events.length - 1)] + : null; + final heroImg = heroEvent != null ? _chooseImage(heroEvent) : null; + + return Padding( + padding: const EdgeInsets.fromLTRB(24, 8, 24, 0), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SizedBox( + height: 400, + child: Stack( + fit: StackFit.expand, + children: [ + // Background image with Ken Burns + AnimatedBuilder( + animation: _heroScaleAnim, + builder: (context, child) { + return Transform.scale( + scale: _heroScaleAnim.value, + child: child, + ); + }, + child: heroImg != null + ? AnimatedSwitcher( + duration: const Duration(milliseconds: 800), + child: CachedNetworkImage( + key: ValueKey(heroImg), + imageUrl: heroImg, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + memCacheWidth: 1400, + placeholder: (_, __) => Container( + color: const Color(0xFF0A0E1A), + ), + errorWidget: (_, __, ___) => Container( + color: const Color(0xFF0A0E1A), + ), + ), + ) + : Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF0F45CF), Color(0xFF082369)], + ), + ), + ), + ), + + // Dark gradient overlay + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x4D000000), // 0.3 alpha + Color(0x99000000), // 0.6 alpha + ], + ), + ), + ), + + // Content overlay + Padding( + padding: const EdgeInsets.all(40), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (heroEvent != null) ...[ + Text( + heroEvent.title ?? heroEvent.name ?? 'Discover Amazing Events', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.w800, + height: 1.2, + ), + ), + const SizedBox(height: 12), + if (heroEvent.place != null || heroEvent.startDate != null) + Row( + children: [ + if (heroEvent.startDate != null) ...[ + const Icon(Icons.calendar_today, + size: 14, color: Colors.white70), + const SizedBox(width: 6), + Text( + heroEvent.startDate!, + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + ], + if (heroEvent.startDate != null && + heroEvent.place != null) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Text('·', + style: TextStyle( + color: Colors.white54, fontSize: 14)), + ), + if (heroEvent.place != null) ...[ + const Icon(Icons.location_on, + size: 14, color: Colors.white70), + const SizedBox(width: 6), + Flexible( + child: Text( + heroEvent.place!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + ), + ], + ], + ), + ] else ...[ + const Text( + 'Discover Amazing\nEvents Near You', + style: TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.w800, + height: 1.2, + ), + ), + const SizedBox(height: 8), + Text( + 'Find events, book tickets, and explore', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 16, + ), + ), + ], + const SizedBox(height: 24), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (heroEvent != null) { + _showFeaturedModal(theme, heroEvent); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 32, vertical: 14), + decoration: BoxDecoration( + color: const Color(0xFF2563EB), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: const Color(0xFF2563EB) + .withValues(alpha: 0.4), + blurRadius: 16, + offset: const Offset(0, 4), + ), + ], + ), + child: Text( + heroEvent != null ? 'Learn More' : 'Explore Events', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ), + + // Hero image indicator dots + if (_events.length > 1) + Positioned( + bottom: 16, + right: 24, + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate( + _events.length.clamp(0, 5), + (i) => Container( + width: i == _heroImageIndex ? 24 : 8, + height: 8, + margin: const EdgeInsets.only(left: 6), + decoration: BoxDecoration( + color: i == _heroImageIndex + ? Colors.white + : Colors.white.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + // ──── Featured Event Modal ──── + void _showFeaturedModal(ThemeData theme, EventModel event) { + final img = _chooseImage(event); + showDialog( + context: context, + barrierColor: Colors.black87, + builder: (ctx) => Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 700, maxHeight: 500), + child: Material( + color: Colors.transparent, + child: Stack( children: [ - // Show top bar ONLY when Home is active - if (selectedMenu == 0) _buildTopBar(), - // Page content under the top bar (or directly if top bar hidden) - Expanded(child: _getCurrentPage()), + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SizedBox( + width: 700, + height: 500, + child: Stack( + fit: StackFit.expand, + children: [ + if (img != null) + CachedNetworkImage( + imageUrl: img, + fit: BoxFit.cover, + memCacheWidth: 1400, + ) + else + Container(color: const Color(0xFF0A0E1A)), + // Gradient + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Color(0xCC000000)], + ), + ), + ), + // Content + Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + event.title ?? event.name ?? '', + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.w800, + height: 1.2, + ), + ), + const SizedBox(height: 12), + if (event.startDate != null) + Text( + '${event.startDate}${event.place != null ? ' · ${event.place}' : ''}', + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + const SizedBox(height: 20), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + Navigator.of(ctx).pop(); + widget.onEventTap(event.id); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 32, vertical: 14), + decoration: BoxDecoration( + color: const Color(0xFF2563EB), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + 'Learn More', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + // Close button + Positioned( + top: 12, + right: 12, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => Navigator.of(ctx).pop(), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.5), + shape: BoxShape.circle, + ), + child: const Icon(Icons.close, + color: Colors.white, size: 20), + ), + ), + ), + ), ], ), ), - ], + ), + ), + ); + } + + // ──── Type Chips (pill style matching website) ──── + Widget _buildTypeChips(ThemeData theme) { + final chips = []; + + final allSelected = + _selectedTypeLabel == null && _selectedTypeId == -1; + chips.add(_chip(theme, 'All Events', allSelected, () { + setState(() { + _selectedTypeId = -1; + _selectedTypeLabel = null; + }); + })); + + for (final t in _types) { + final selected = _selectedTypeLabel == null && _selectedTypeId == t.id; + chips.add(_chip(theme, t.name, selected, () { + setState(() { + _selectedTypeId = t.id; + _selectedTypeLabel = null; + }); + })); + } + + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 24), + scrollDirection: Axis.horizontal, + itemBuilder: (_, idx) => chips[idx], + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemCount: chips.length, + ); + } + + Widget _chip(ThemeData theme, String label, bool selected, VoidCallback onTap) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 36, + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 20), + decoration: BoxDecoration( + color: selected + ? const Color(0xFF0F45CF) + : const Color(0xFFF3F4F6), + borderRadius: BorderRadius.circular(999), + boxShadow: selected + ? [ + BoxShadow( + color: const Color(0xFF0F45CF).withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: selected ? Colors.white : const Color(0xFF111827), + ), + ), + ), + ), + ); + } + + // ──── Event Grid ──── + Widget _buildEventGrid(ThemeData theme) { + final list = _filteredEvents; + + if (_loading) { + return SliverFillRemaining( + child: Center( + child: CircularProgressIndicator(color: theme.colorScheme.primary), + ), + ); + } + if (list.isEmpty) { + return SliverFillRemaining( + child: Center( + child: Text('No events available', style: theme.textTheme.bodyMedium), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 24), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + mainAxisExtent: 260, + crossAxisSpacing: 18, + mainAxisSpacing: 18, + ), + delegate: SliverChildBuilderDelegate( + (ctx, idx) { + final e = list[idx]; + final img = _chooseImage(e); + return _eventCard(theme, e, img); + }, + childCount: list.length, + ), + ), + ); + } + + Widget _eventCard(ThemeData theme, EventModel e, String? img) { + const double cardRadius = 16; + const double imageHeight = 160; + + 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 MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => widget.onEventTap(e.id), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(cardRadius), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image with date badge + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(cardRadius)), + child: img != null + ? CachedNetworkImage( + imageUrl: img, + memCacheWidth: 600, + memCacheHeight: 320, + width: double.infinity, + height: imageHeight, + fit: BoxFit.cover, + placeholder: (_, __) => Container( + height: imageHeight, + color: const Color(0xFFE5E7EB)), + errorWidget: (_, __, ___) => Container( + height: imageHeight, + color: const Color(0xFFE5E7EB)), + ) + : Container( + height: imageHeight, + width: double.infinity, + color: const Color(0xFFE5E7EB), + ), + ), + // Date badge + if (e.startDate != null) + Positioned( + top: 10, + right: 10, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.92), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 4, + ), + ], + ), + child: Text( + _formatDateBadge(e.startDate!), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Color(0xFF111827), + height: 1.3, + ), + ), + ), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + e.title ?? e.name ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 15, + color: Color(0xFF111827), + ), + ), + const Spacer(), + Row(children: [ + const Icon(Icons.calendar_today, + size: 13, color: Color(0xFF3B82F6)), + const SizedBox(width: 6), + Flexible( + child: Text( + dateLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, color: Color(0xFF6B7280)), + ), + ), + ]), + if (e.place != null) ...[ + const SizedBox(height: 4), + Row(children: [ + const Icon(Icons.location_on, + size: 13, color: Color(0xFF22C55E)), + const SizedBox(width: 6), + Flexible( + child: Text( + e.place!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, color: Color(0xFF6B7280)), + ), + ), + ]), + ], + ], + ), + ), + ), + ], + ), + ), ), ); } diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index dd16dca..9d7af98 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../core/auth/auth_guard.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:intl/intl.dart'; import '../features/events/models/event_models.dart'; import '../features/events/services/events_service.dart'; @@ -61,15 +62,15 @@ class _HomeScreenState extends State with SingleTickerProviderStateM super.dispose(); } - void _startAutoScroll() { + void _startAutoScroll({Duration delay = const Duration(seconds: 5)}) { _autoScrollTimer?.cancel(); - _autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (timer) { + _autoScrollTimer = Timer.periodic(delay, (timer) { if (_heroEvents.isEmpty) return; final nextPage = (_heroPageNotifier.value + 1) % _heroEvents.length; if (_heroPageController.hasClients) { _heroPageController.animateToPage( nextPage, - duration: const Duration(milliseconds: 500), + duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, ); } @@ -80,7 +81,14 @@ class _HomeScreenState extends State with SingleTickerProviderStateM setState(() => _loading = true); final prefs = await SharedPreferences.getInstance(); _username = prefs.getString('display_name') ?? prefs.getString('username') ?? ''; - _location = prefs.getString('location') ?? 'Whitefield, Bengaluru'; + final storedLocation = prefs.getString('location') ?? 'Whitefield, Bengaluru'; + // Fix legacy lat,lng strings saved before the reverse-geocoding fix + if (RegExp(r'^-?\d+\.\d+,-?\d+\.\d+$').hasMatch(storedLocation)) { + _location = 'Current Location'; + prefs.setString('location', _location); + } else { + _location = storedLocation; + } _pincode = prefs.getString('pincode') ?? 'all'; try { @@ -482,7 +490,26 @@ class _HomeScreenState extends State with SingleTickerProviderStateM // Get hero events (first 4 events for the carousel) - List get _heroEvents => _events.take(4).toList(); + List get _heroEvents => _events.take(6).toList(); + + String _formatDate(String dateStr) { + try { + final dt = DateTime.parse(dateStr); + return DateFormat('d MMM yyyy').format(dt); + } catch (_) { + return dateStr; + } + } + + String _getEventTypeName(EventModel event) { + if (event.eventTypeId != null && event.eventTypeId! > 0) { + final match = _types.where((t) => t.id == event.eventTypeId); + if (match.isNotEmpty && match.first.name.isNotEmpty) { + return match.first.name.toUpperCase(); + } + } + return 'EVENT'; + } // Date filter state String _selectedDateFilter = ''; @@ -1131,42 +1158,51 @@ class _HomeScreenState extends State with SingleTickerProviderStateM // Featured carousel _heroEvents.isEmpty - ? SizedBox( - height: 280, - child: Center( - child: _loading - ? const CircularProgressIndicator(color: Colors.white) - : const Text('No events available', style: TextStyle(color: Colors.white70)), - ), - ) + ? _loading + ? const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: SizedBox( + height: 320, + child: _HeroShimmer(), + ), + ) + : const SizedBox( + height: 280, + child: Center( + child: Text('No events available', + style: TextStyle(color: Colors.white70)), + ), + ) : Column( children: [ - SizedBox( - height: 320, - child: PageView.builder( - controller: _heroPageController, - onPageChanged: (page) { - _heroPageNotifier.value = page; - // Reset 3-second countdown so user always gets full read time - _startAutoScroll(); - }, - itemCount: _heroEvents.length, - 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]), - ); - }, + RepaintBoundary( + child: SizedBox( + height: 320, + child: PageView.builder( + controller: _heroPageController, + onPageChanged: (page) { + _heroPageNotifier.value = page; + // 8s delay after manual swipe for full read time + _startAutoScroll(delay: const Duration(seconds: 8)); + }, + itemCount: _heroEvents.length, + 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), @@ -1185,7 +1221,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM valueListenable: _heroPageNotifier, builder: (context, currentPage, _) { return SizedBox( - height: 12, + height: 44, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( @@ -1199,13 +1235,21 @@ class _HomeScreenState extends State with SingleTickerProviderStateM duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); } }, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 4), - width: isActive ? 24 : 8, - height: 8, - decoration: BoxDecoration( - color: isActive ? Colors.white : Colors.white.withOpacity(0.4), - borderRadius: BorderRadius.circular(4), + child: SizedBox( + width: 44, + height: 44, + child: Center( + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: isActive ? 24 : 8, + height: 8, + decoration: BoxDecoration( + color: isActive + ? Colors.white + : Colors.white.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(4), + ), + ), ), ), ); @@ -1247,7 +1291,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM img != null && img.isNotEmpty ? CachedNetworkImage( imageUrl: img, - memCacheWidth: 800, + memCacheWidth: 700, fit: BoxFit.cover, placeholder: (_, __) => const _HeroShimmer(radius: radius), errorWidget: (_, __, ___) => @@ -1272,7 +1316,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ), ), - // ── Layer 2: FEATURED glassmorphism badge (top-left) ── + // ── Layer 2: Event type glassmorphism badge (top-left) ── Positioned( top: 14, left: 14, @@ -1283,18 +1327,18 @@ class _HomeScreenState extends State with SingleTickerProviderStateM child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.18), + color: Colors.white.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white.withOpacity(0.28)), + border: Border.all(color: Colors.white.withValues(alpha: 0.28)), ), - child: const Row( + child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.star_rounded, color: Colors.amber, size: 13), - SizedBox(width: 4), + const Icon(Icons.star_rounded, color: Colors.amber, size: 13), + const SizedBox(width: 4), Text( - 'FEATURED', - style: TextStyle( + _getEventTypeName(event), + style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.w800, @@ -1339,7 +1383,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM color: Colors.white70, size: 12), const SizedBox(width: 4), Text( - event.startDate!, + _formatDate(event.startDate!), style: const TextStyle( color: Colors.white70, fontSize: 12, @@ -2198,7 +2242,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM /// 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}); + const _HeroShimmer({this.radius = 24.0}); @override State<_HeroShimmer> createState() => _HeroShimmerState(); diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart index cb7afd5..2b0d879 100644 --- a/lib/screens/learn_more_screen.dart +++ b/lib/screens/learn_more_screen.dart @@ -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 { } 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( diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index b5acbea..f7484f8 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -12,6 +12,7 @@ import 'learn_more_screen.dart'; import 'settings_screen.dart'; import '../core/app_decoration.dart'; import '../core/constants.dart'; +import '../widgets/landscape_section_header.dart'; class ProfileScreen extends StatefulWidget { const ProfileScreen({Key? key}) : super(key: key); @@ -1013,6 +1014,534 @@ class _ProfileScreenState extends State ); } + // ═══════════════════════════════════════════════ + // LANDSCAPE LAYOUT + // ═══════════════════════════════════════════════ + + Widget _buildLandscapeLeftPanel(BuildContext context) { + final theme = Theme.of(context); + return SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Top bar row — title + settings + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: Row( + children: [ + const Text( + 'Profile', + style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w700), + ), + const Spacer(), + GestureDetector( + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SettingsScreen())), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(10)), + child: const Icon(Icons.settings, color: Colors.white), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + // Avatar + name section + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 3), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.15), blurRadius: 8, offset: const Offset(0, 2))], + ), + child: _buildProfileAvatar(size: 64), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _username.isNotEmpty ? _username : 'Guest User', + style: const TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.w700), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + _email, + style: const TextStyle(color: Colors.white70, fontSize: 13), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + // EXP Bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _buildExpBar(), + ), + const SizedBox(height: 20), + // Stats row + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(14), + ), + padding: const EdgeInsets.symmetric(vertical: 16), + child: _buildLandscapeStats(context, textColor: Colors.white), + ), + ), + const SizedBox(height: 20), + // Edit profile button + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: OutlinedButton.icon( + onPressed: _openEditDialog, + icon: const Icon(Icons.edit, size: 16, color: Colors.white), + label: const Text('Edit Profile', style: TextStyle(color: Colors.white)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.white38), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildLandscapeStats(BuildContext context, {Color? textColor}) { + final color = textColor ?? Theme.of(context).textTheme.bodyLarge?.color; + final hintColor = textColor?.withOpacity(0.6) ?? Theme.of(context).hintColor; + + String fmt(int v) => v >= 1000 ? '${(v / 1000).toStringAsFixed(1)}K' : '$v'; + + return AnimatedBuilder( + animation: _animController, + builder: (_, __) => Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _landscapeStatItem(fmt(_animatedLikes), 'Likes', color, hintColor), + _landscapeStatDivider(), + _landscapeStatItem(fmt(_animatedPosts), 'Posts', color, hintColor), + _landscapeStatDivider(), + _landscapeStatItem(fmt(_animatedViews), 'Views', color, hintColor), + ], + ), + ); + } + + Widget _landscapeStatItem(String value, String label, Color? valueColor, Color? labelColor) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(value, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: valueColor)), + const SizedBox(height: 2), + Text(label, style: TextStyle(fontSize: 12, color: labelColor, fontWeight: FontWeight.w400)), + ], + ); + } + + Widget _landscapeStatDivider() => Container(width: 1, height: 36, color: Colors.white24); + + Widget _buildLandscapeRightPanel(BuildContext context) { + final theme = Theme.of(context); + + Widget _eventList(List events, {bool faded = false}) { + if (_loadingEvents) { + return const Center(child: CircularProgressIndicator()); + } + if (events.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text('No events', style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)), + ), + ); + } + return ListView.builder( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(18, 8, 18, 32), + itemCount: events.length, + itemBuilder: (ctx, i) => _eventListTileFromModel(events[i], faded: faded), + ); + } + + return SafeArea( + child: DefaultTabController( + length: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const LandscapeSectionHeader(title: 'My Events'), + // Tab bar + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 8), + child: Container( + decoration: BoxDecoration( + color: theme.dividerColor.withOpacity(0.5), + borderRadius: BorderRadius.circular(10), + ), + child: TabBar( + labelColor: Colors.white, + unselectedLabelColor: theme.hintColor, + indicator: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(10), + ), + labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), + tabs: const [ + Tab(text: 'Ongoing'), + Tab(text: 'Upcoming'), + Tab(text: 'Past'), + ], + ), + ), + ), + Expanded( + child: TabBarView( + children: [ + _eventList(_ongoingEvents), + _eventList(_upcomingEvents), + _eventList(_pastEvents, faded: true), + ], + ), + ), + ], + ), + ), + ); + } + + // ═══════════════════════════════════════════════ + // DESKTOP LAYOUT (Figma: full-width banner + 3-col grids) + // ═══════════════════════════════════════════════ + + Widget _buildDesktopLayout(BuildContext context, ThemeData theme) { + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Full-width profile header + card (reuse existing widgets) + Stack( + children: [ + _buildGradientHeader(context, 200), + Padding( + padding: const EdgeInsets.only(top: 130), + child: _buildProfileCard(context), + ), + ], + ), + const SizedBox(height: 24), + + // Ongoing Events (only if non-empty) + if (_ongoingEvents.isNotEmpty) + _buildDesktopEventSection( + context, + title: 'Ongoing Events', + events: _ongoingEvents, + faded: false, + ), + + // Upcoming Events + _buildDesktopEventSection( + context, + title: 'Upcoming Events', + events: _upcomingEvents, + faded: false, + emptyMessage: 'No upcoming events', + ), + + // Past Events + _buildDesktopEventSection( + context, + title: 'Past Events', + events: _pastEvents, + faded: true, + emptyMessage: 'No past events', + ), + + const SizedBox(height: 32), + ], + ), + ), + ); + } + + /// Section heading row ("Title" + "View All >") followed by a 3-column grid. + Widget _buildDesktopEventSection( + BuildContext context, { + required String title, + required List events, + bool faded = false, + String? emptyMessage, + }) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Heading row + Row( + children: [ + Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + fontSize: 20, + ), + ), + const Spacer(), + if (events.isNotEmpty) + TextButton( + onPressed: () { + // View all — no-op for now; could navigate to a full list + }, + child: Text( + 'View All >', + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + + // Content + if (_loadingEvents) + const Center(child: CircularProgressIndicator()) + else if (events.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + emptyMessage ?? 'No events', + style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor), + ), + ) + else + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 0.82, + ), + itemCount: events.length, + itemBuilder: (ctx, i) => + _buildDesktopEventGridCard(events[i], faded: faded), + ), + + const SizedBox(height: 24), + ], + ), + ); + } + + /// A single event card for the desktop grid: image on top, title, date (blue dot), venue (green dot). + Widget _buildDesktopEventGridCard(EventModel ev, {bool faded = false}) { + final theme = Theme.of(context); + final title = ev.title ?? ev.name ?? ''; + final dateLabel = + (ev.startDate != null && ev.endDate != null && ev.startDate == ev.endDate) + ? ev.startDate! + : ((ev.startDate != null && ev.endDate != null) + ? '${ev.startDate} - ${ev.endDate}' + : (ev.startDate ?? '')); + final location = ev.place ?? ''; + final imageUrl = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty) + ? ev.thumbImg! + : (ev.images.isNotEmpty ? ev.images.first.image : null); + + final titleColor = faded ? theme.hintColor : (theme.textTheme.bodyLarge?.color); + final subtitleColor = faded + ? theme.hintColor.withValues(alpha: 0.7) + : theme.hintColor; + + return GestureDetector( + onTap: () { + if (ev.id != null) { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id)), + ); + } + }, + child: Container( + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image + Expanded( + flex: 3, + child: ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(14)), + child: _buildCardImage(imageUrl, theme), + ), + ), + // Text content + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title + Text( + title, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + color: titleColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + // 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: subtitleColor), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 4), + // 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( + location, + style: theme.textTheme.bodySmall?.copyWith(color: subtitleColor), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + /// Helper to build the image widget for a desktop grid card. + Widget _buildCardImage(String? imageUrl, ThemeData theme) { + if (imageUrl != null && imageUrl.trim().isNotEmpty) { + if (imageUrl.startsWith('http')) { + return CachedNetworkImage( + imageUrl: imageUrl, + memCacheWidth: 400, + memCacheHeight: 400, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + placeholder: (_, __) => Container(color: theme.dividerColor), + errorWidget: (_, __, ___) => Container( + color: theme.dividerColor, + child: Icon(Icons.event, size: 32, color: theme.hintColor), + ), + ); + } + if (!kIsWeb) { + final path = imageUrl; + if (path.startsWith('/') || path.contains(Platform.pathSeparator)) { + final file = File(path); + if (file.existsSync()) { + return Image.file( + file, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: theme.dividerColor, + child: Icon(Icons.event, size: 32, color: theme.hintColor), + ), + ); + } + } + } + return Image.asset( + imageUrl, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: theme.dividerColor, + child: Icon(Icons.event, size: 32, color: theme.hintColor), + ), + ); + } + return Container( + color: theme.dividerColor, + child: Icon(Icons.event, size: 32, color: theme.hintColor), + ); + } + // ═══════════════════════════════════════════════ // BUILD // ═══════════════════════════════════════════════ @@ -1022,6 +1551,7 @@ class _ProfileScreenState extends State final theme = Theme.of(context); const double headerHeight = 200.0; const double cardTopOffset = 130.0; + final width = MediaQuery.of(context).size.width; Widget sectionTitle(String text) => Padding( padding: const EdgeInsets.fromLTRB(18, 16, 18, 12), @@ -1032,6 +1562,12 @@ class _ProfileScreenState extends State ), ); + // ── DESKTOP / LANDSCAPE layout ───────────────────────────────────────── + if (width >= AppConstants.desktopBreakpoint) { + return _buildDesktopLayout(context, theme); + } + + // ── MOBILE layout ───────────────────────────────────────────────────── return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, // CustomScrollView: only visible event cards are built — no full-tree Column renders diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 97c43b3..0dfc59f 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -149,7 +149,7 @@ class _SearchScreenState extends State { } } catch (_) {} - if (mounted) Navigator.of(context).pop('${pos.latitude.toStringAsFixed(5)},${pos.longitude.toStringAsFixed(5)}'); + if (mounted) Navigator.of(context).pop('Current Location'); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not determine location: $e'))); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 69d3245..6ebdef2 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -3,7 +3,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'login_screen.dart'; import 'desktop_login_screen.dart'; import '../core/theme_manager.dart'; -import 'privacy_policy_screen.dart'; // new import +import 'privacy_policy_screen.dart'; import '../core/app_decoration.dart'; class SettingsScreen extends StatefulWidget { @@ -15,7 +15,8 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { bool _notifications = true; - String _appVersion = '1.2(p)'; + String _appVersion = '1.6(p)'; + int _selectedSection = 0; // 0=Preferences, 1=Account, 2=About @override void initState() { @@ -100,16 +101,209 @@ class _SettingsScreenState extends State { ); } + // ── Settings content sections ──────────────────────────────────────────── + Widget _buildPreferencesSection() { + const primary = Color(0xFF0B63D6); + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(18, 0, 18, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + Container( + margin: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration(color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(12), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))]), + child: SwitchListTile( + tileColor: Theme.of(context).cardColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + value: _notifications, + onChanged: (v) => _saveNotifications(v), + title: const Text('Reminders'), + secondary: const Icon(Icons.notifications, color: primary), + ), + ), + const SizedBox(height: 8), + Container( + margin: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration(color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(12), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))]), + child: ValueListenableBuilder( + valueListenable: ThemeManager.themeMode, + builder: (context, mode, _) { + return SwitchListTile( + tileColor: Theme.of(context).cardColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + value: mode == ThemeMode.dark, + onChanged: (v) => ThemeManager.setThemeMode(v ? ThemeMode.dark : ThemeMode.light), + title: const Text('Dark Mode'), + secondary: const Icon(Icons.dark_mode, color: primary), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildAccountSection() { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(18, 8, 18, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTile( + icon: Icons.person, + title: 'Edit Profile', + subtitle: 'Change username, email or photo', + onTap: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Open Profile tab'))), + ), + const SizedBox(height: 24), + Center( + child: ElevatedButton( + onPressed: _confirmLogout, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + ), + child: const Text('Logout', style: TextStyle(color: Colors.white)), + ), + ), + ], + ), + ); + } + + Widget _buildAboutSection() { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(18, 8, 18, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTile(icon: Icons.info_outline, title: 'App Version', subtitle: _appVersion, onTap: () {}), + const SizedBox(height: 12), + _buildTile( + icon: Icons.privacy_tip_outlined, + title: 'Privacy Policy', + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PrivacyPolicyScreen())), + ), + ], + ), + ); + } + + Widget _buildActiveSection() { + switch (_selectedSection) { + case 1: return _buildAccountSection(); + case 2: return _buildAboutSection(); + default: return _buildPreferencesSection(); + } + } + @override Widget build(BuildContext context) { const primary = Color(0xFF0B63D6); + final width = MediaQuery.of(context).size.width; + final isLandscape = width >= 820; + // ── LANDSCAPE layout ────────────────────────────────────────────────── + if (isLandscape) { + const navIcons = [Icons.tune, Icons.person_outline, Icons.info_outline]; + const navLabels = ['Preferences', 'Account', 'About']; + + return Row( + children: [ + // Left: settings nav on gradient + Flexible( + flex: 1, + child: RepaintBoundary( + child: Container( + decoration: AppDecoration.blueGradient, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(20, 24, 20, 20), + child: Text('Settings', style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w700)), + ), + ...List.generate(navLabels.length, (i) { + final isActive = _selectedSection == i; + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => setState(() => _selectedSection = i), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.fromLTRB(16, 0, 16, 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: isActive ? Colors.white : Colors.white.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + ), + child: Row(children: [ + Icon(navIcons[i], size: 20, color: isActive ? primary : Colors.white70), + const SizedBox(width: 12), + Text(navLabels[i], style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: isActive ? primary : Colors.white)), + ]), + ), + ), + ); + }), + const Spacer(), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), + child: OutlinedButton.icon( + onPressed: _confirmLogout, + icon: const Icon(Icons.logout, color: Colors.white70, size: 18), + label: const Text('Logout', style: TextStyle(color: Colors.white70)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.white24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ), + ), + ), + ), + ), + // Right: settings content + Flexible( + flex: 2, + child: RepaintBoundary( + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 12), + child: Text( + navLabels[_selectedSection], + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + Expanded(child: _buildActiveSection()), + ], + ), + ), + ), + ), + ], + ); + } + + // ── MOBILE layout ───────────────────────────────────────────────────── return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: SafeArea( child: Column( children: [ - // Header Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 18, 20, 18), @@ -131,41 +325,27 @@ class _SettingsScreenState extends State { ], ), ), - const SizedBox(height: 18), - - // Content Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(18, 0, 18, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Account const Text('Account', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 12), _buildTile( icon: Icons.person, title: 'Edit Profile', subtitle: 'Change username, email or photo', - onTap: () { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Open Profile tab (demo)'))); - }, + onTap: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Open Profile tab (demo)'))), ), const SizedBox(height: 12), - - // Preferences const Text('Preferences', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 12), - - // Reminders switch wrapped in card-like container Container( margin: const EdgeInsets.symmetric(vertical: 6), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(12), - boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))], - ), + decoration: BoxDecoration(color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(12), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))]), child: SwitchListTile( tileColor: Theme.of(context).cardColor, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), @@ -175,54 +355,34 @@ class _SettingsScreenState extends State { secondary: const Icon(Icons.notifications, color: primary), ), ), - const SizedBox(height: 8), - - // Dark Mode switch wrapped in card-like container and hooked to ThemeManager Container( margin: const EdgeInsets.symmetric(vertical: 6), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(12), - boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))], - ), + decoration: BoxDecoration(color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(12), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))]), child: ValueListenableBuilder( valueListenable: ThemeManager.themeMode, - builder: (context, mode, _) { - final isDark = mode == ThemeMode.dark; - return SwitchListTile( - tileColor: Theme.of(context).cardColor, - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - value: isDark, - onChanged: (v) => ThemeManager.setThemeMode(v ? ThemeMode.dark : ThemeMode.light), - title: const Text('Dark Mode'), - secondary: const Icon(Icons.dark_mode, color: primary), - ); - }, + builder: (context, mode, _) => SwitchListTile( + tileColor: Theme.of(context).cardColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + value: mode == ThemeMode.dark, + onChanged: (v) => ThemeManager.setThemeMode(v ? ThemeMode.dark : ThemeMode.light), + title: const Text('Dark Mode'), + secondary: const Icon(Icons.dark_mode, color: primary), + ), ), ), - const SizedBox(height: 18), - - // About const Text('About', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 12), _buildTile(icon: Icons.info_outline, title: 'App Version', subtitle: _appVersion, onTap: () {}), const SizedBox(height: 12), - - // Privacy Policy tile now navigates to PrivacyPolicyScreen _buildTile( icon: Icons.privacy_tip_outlined, title: 'Privacy Policy', subtitle: 'Demo app', - onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PrivacyPolicyScreen())); - }, + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PrivacyPolicyScreen())), ), - const SizedBox(height: 24), - - // Logout area Center( child: Column( children: [ @@ -240,7 +400,6 @@ class _SettingsScreenState extends State { ], ), ), - const SizedBox(height: 32), ], ), diff --git a/lib/widgets/desktop_sidebar.dart b/lib/widgets/desktop_sidebar.dart new file mode 100644 index 0000000..1f13222 --- /dev/null +++ b/lib/widgets/desktop_sidebar.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import '../core/app_decoration.dart'; +import '../core/constants.dart'; + +class DesktopSidebar extends StatelessWidget { + final int selectedIndex; + final ValueChanged onIndexChanged; + + const DesktopSidebar({ + Key? key, + required this.selectedIndex, + required this.onIndexChanged, + }) : super(key: key); + + static const _navItems = <_NavDef>[ + _NavDef(Icons.home_outlined, Icons.home, 'Home', 0), + _NavDef(Icons.calendar_today_outlined, Icons.calendar_today, 'Calendar', 1), + _NavDef(Icons.person_outline, Icons.person, 'Profile', 2), + ]; + + static const _bottomItems = <_NavDef>[ + _NavDef(Icons.settings_outlined, Icons.settings, 'Settings', 5), + _NavDef(Icons.help_outline, Icons.help, 'Help', -1), + ]; + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: Container( + width: AppConstants.sidebarExpandedWidth, + decoration: AppDecoration.blueGradient, + child: SafeArea( + child: Column( + children: [ + // Logo + Padding( + padding: const EdgeInsets.only(left: 24, top: 20, right: 24), + child: Align( + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.auto_awesome, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'EVENTIFY', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + + // Main nav items + Column( + children: _navItems + .map((item) => _buildNavItem(item)) + .toList(), + ), + + const Spacer(), + + // Bottom nav items + Column( + children: _bottomItems + .map((item) => _buildNavItem(item)) + .toList(), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } + + Widget _buildNavItem(_NavDef item) { + final selected = selectedIndex == item.index; + final icon = selected ? item.activeIcon : item.icon; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: InkWell( + onTap: () => onIndexChanged(item.index), + borderRadius: BorderRadius.circular(12), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 48, + margin: const EdgeInsets.symmetric(vertical: 2), + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: selected ? Colors.white : Colors.transparent, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon( + icon, + size: 22, + color: selected + ? const Color(0xFF0F45CF) + : Colors.white.withValues(alpha: 0.85), + ), + const SizedBox(width: 12), + Text( + item.label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: selected + ? const Color(0xFF0F45CF) + : Colors.white.withValues(alpha: 0.85), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _NavDef { + final IconData icon; + final IconData activeIcon; + final String label; + final int index; + const _NavDef(this.icon, this.activeIcon, this.label, this.index); +} diff --git a/lib/widgets/desktop_topbar.dart b/lib/widgets/desktop_topbar.dart new file mode 100644 index 0000000..e794e46 --- /dev/null +++ b/lib/widgets/desktop_topbar.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; + +class DesktopTopBar extends StatelessWidget { + final String username; + final String? profileImage; + final VoidCallback? onSearchTap; + final VoidCallback? onNotificationTap; + final VoidCallback? onAvatarTap; + + const DesktopTopBar({ + Key? key, + required this.username, + this.profileImage, + this.onSearchTap, + this.onNotificationTap, + this.onAvatarTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + height: 64, + padding: const EdgeInsets.symmetric(horizontal: 24), + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + border: Border( + bottom: BorderSide( + color: theme.dividerColor.withValues(alpha: 0.3), + ), + ), + ), + child: Row( + children: [ + // Left: search bar + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: SizedBox( + height: 44, + child: TextField( + onTap: onSearchTap, + readOnly: onSearchTap != null, + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.surfaceContainerHighest + .withValues(alpha: 0.4), + prefixIcon: Icon(Icons.search, color: theme.hintColor), + hintText: 'Search', + hintStyle: theme.textTheme.bodyMedium + ?.copyWith(color: theme.hintColor), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 0), + ), + style: theme.textTheme.bodyLarge, + ), + ), + ), + ), + + const SizedBox(width: 16), + + // Right: notification bell + avatar + Stack( + children: [ + IconButton( + onPressed: onNotificationTap, + icon: Icon( + Icons.notifications_none, + color: theme.iconTheme.color, + ), + ), + Positioned( + right: 6, + top: 6, + child: CircleAvatar( + radius: 8, + backgroundColor: Colors.red, + child: Text( + '2', + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ), + ], + ), + const SizedBox(width: 8), + GestureDetector( + onTap: onAvatarTap, + child: _buildAvatar(), + ), + ], + ), + ); + } + + Widget _buildAvatar() { + if (profileImage != null && profileImage!.trim().isNotEmpty) { + final url = profileImage!.trim(); + if (url.startsWith('http')) { + return CircleAvatar( + radius: 20, + backgroundColor: Colors.grey.shade200, + backgroundImage: NetworkImage(url), + onBackgroundImageError: (_, __) {}, + ); + } + } + + final name = username.trim(); + String initials = 'U'; + if (name.isNotEmpty) { + if (name.contains('@')) { + initials = name[0].toUpperCase(); + } else { + final parts = name.split(' ').where((p) => p.isNotEmpty).toList(); + initials = parts.isEmpty + ? 'U' + : parts.take(2).map((p) => p[0].toUpperCase()).join(); + } + } + + return CircleAvatar( + radius: 20, + backgroundColor: Colors.blue.shade600, + child: Text( + initials, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ); + } +} diff --git a/lib/widgets/landscape_section_header.dart b/lib/widgets/landscape_section_header.dart new file mode 100644 index 0000000..dc648fd --- /dev/null +++ b/lib/widgets/landscape_section_header.dart @@ -0,0 +1,58 @@ +// lib/widgets/landscape_section_header.dart +// +// Consistent section header for the right panel of landscape layouts. +// Shows a title, optional subtitle, and optional trailing action widget. + +import 'package:flutter/material.dart'; + +class LandscapeSectionHeader extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? trailing; + final EdgeInsetsGeometry padding; + + const LandscapeSectionHeader({ + Key? key, + required this.title, + this.subtitle, + this.trailing, + this.padding = const EdgeInsets.fromLTRB(24, 24, 24, 12), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Padding( + padding: padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: -0.3, + ), + ), + if (subtitle != null) ...[ + const SizedBox(height: 2), + Text( + subtitle!, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.textTheme.bodySmall?.color?.withOpacity(0.6), + ), + ), + ], + ], + ), + ), + if (trailing != null) trailing!, + ], + ), + ); + } +} diff --git a/lib/widgets/landscape_shell.dart b/lib/widgets/landscape_shell.dart new file mode 100644 index 0000000..40ff348 --- /dev/null +++ b/lib/widgets/landscape_shell.dart @@ -0,0 +1,67 @@ +// lib/widgets/landscape_shell.dart +// +// Reusable two-panel landscape scaffold for all desktop/wide screens. +// Left panel uses the brand dark-blue gradient; right panel is the content area. +// +// Usage: +// LandscapeShell( +// leftPanel: MyLeftContent(), +// rightPanel: MyRightContent(), +// ) + +import 'package:flutter/material.dart'; +import '../core/app_decoration.dart'; + +class LandscapeShell extends StatelessWidget { + final Widget leftPanel; + final Widget rightPanel; + + /// Flex weight for left panel (default 2 → ~40% of width) + final int leftFlex; + + /// Flex weight for right panel (default 3 → ~60% of width) + final int rightFlex; + + /// Optional background color for right panel (defaults to scaffold background) + final Color? rightBackground; + + const LandscapeShell({ + Key? key, + required this.leftPanel, + required this.rightPanel, + this.leftFlex = 2, + this.rightFlex = 3, + this.rightBackground, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final bg = rightBackground ?? Theme.of(context).scaffoldBackgroundColor; + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Left panel — dark blue gradient ────────────────────────────── + Flexible( + flex: leftFlex, + child: RepaintBoundary( + child: Container( + decoration: AppDecoration.blueGradient, + child: leftPanel, + ), + ), + ), + + // ── Right panel — content area ──────────────────────────────────── + Flexible( + flex: rightFlex, + child: RepaintBoundary( + child: ColoredBox( + color: bg, + child: rightPanel, + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/responsive_shell.dart b/lib/widgets/responsive_shell.dart new file mode 100644 index 0000000..50ae20a --- /dev/null +++ b/lib/widgets/responsive_shell.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../core/constants.dart'; +import 'desktop_sidebar.dart'; +import 'desktop_topbar.dart'; + +class ResponsiveShell extends StatefulWidget { + final int currentIndex; + final ValueChanged onIndexChanged; + final Widget child; + final bool showTopBar; + + const ResponsiveShell({ + Key? key, + required this.currentIndex, + required this.onIndexChanged, + required this.child, + this.showTopBar = true, + }) : super(key: key); + + @override + State createState() => _ResponsiveShellState(); +} + +class _ResponsiveShellState extends State { + String _username = 'Guest'; + String? _profileImage; + + @override + void initState() { + super.initState(); + _loadPreferences(); + } + + Future _loadPreferences() async { + final prefs = await SharedPreferences.getInstance(); + if (!mounted) return; + setState(() { + _username = prefs.getString('display_name') ?? + prefs.getString('username') ?? + 'Guest'; + _profileImage = prefs.getString('profileImage'); + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final width = constraints.maxWidth; + + // Mobile — no shell + if (width < AppConstants.desktopBreakpoint) { + return widget.child; + } + + return Scaffold( + body: Row( + children: [ + DesktopSidebar( + selectedIndex: widget.currentIndex, + onIndexChanged: widget.onIndexChanged, + ), + Expanded( + child: Column( + children: [ + if (widget.showTopBar) + DesktopTopBar( + username: _username, + profileImage: _profileImage, + onAvatarTap: () => widget.onIndexChanged(2), + ), + Expanded( + child: RepaintBoundary(child: widget.child), + ), + ], + ), + ), + ], + ), + ); + }); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 08a0bdd..0d2b0fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: figma description: A Flutter event app publish_to: 'none' -version: 1.5.0+15 +version: 1.6.1+17 environment: sdk: ">=2.17.0 <3.0.0"