feat: rebuild desktop UI to match Figma + website, hero slider improvements

- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 13:28:19 +05:30
parent 9dd78be03e
commit bc6fde1b90
21 changed files with 2938 additions and 1285 deletions

View File

@@ -22,8 +22,8 @@ android {
applicationId = "com.sicherhaven.eventify" applicationId = "com.sicherhaven.eventify"
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = 15 versionCode = 17
versionName = "1.5(p)" versionName = "1.6.1(p)"
} }
// ---------- SIGNING CONFIG ---------- // ---------- SIGNING CONFIG ----------
@@ -51,9 +51,9 @@ android {
// Use the release signing config created above // Use the release signing config created above
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false isMinifyEnabled = true
isShrinkResources = false isShrinkResources = true
// proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
} }
} }
} }

28
android/app/proguard-rules.pro vendored Normal file
View File

@@ -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

View File

@@ -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()
1 Component Old Implementation New Implementation (matching mvnew.eventifyplus.com/home)
2 Hero Height 120px blue gradient banner 400px immersive full-width hero with rounded corners (20px)
3 Background Image None (solid blue gradient via AppDecoration) Featured event image with CachedNetworkImage and BoxFit.cover
4 Ken Burns Animation None AnimationController 12s loop scaling 1.0 to 1.08 with easeInOut curve
5 Image Rotation None Timer.periodic every 6s cycling through first 5 events with AnimatedSwitcher crossfade
6 Dark Overlay None LinearGradient top-to-bottom rgba(0/0/0/0.3) to rgba(0/0/0/0.6)
7 Title Text Welcome Back + username (14px + 22px) Featured event title (36px weight 800) or fallback Discover Amazing Events Near You
8 Subtitle None Event date + venue with calendar and location icons (14px white70)
9 CTA Button None Learn More or Explore Events button (#2563EB bg white text rounded 12px with blue glow shadow)
10 Indicator Dots None Bottom-right pill dots (active=24px white elongated / inactive=8px white40) for up to 5 events
11 Fallback (no events) Blue gradient with username Blue gradient (#0F45CF to #082369) with static heading and subtitle
12 Featured Modal None showDialog with dark overlay (black87) plus 700x500 modal with event image gradient title and Learn More CTA
13 Close Button None Circular black50 button with white X icon top-right of modal
14 Animation Mixin None SingleTickerProviderStateMixin added to _HomeContentState
15 Disposal None AnimationController.dispose and Timer.cancel in dispose()

View File

@@ -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"
1 Phase File Action Priority Description
2 1 lib/core/constants.dart MODIFY Critical Add desktop breakpoints: wideDesktopBreakpoint=1200, sidebarExpandedWidth=240, sidebarCollapsedWidth=72, topBarHeight=64
3 1 lib/widgets/desktop_sidebar.dart CREATE Critical Collapsible sidebar with icon-only (820-1200px) and full (1200px+) modes, AnimatedContainer transitions
4 1 lib/widgets/desktop_topbar.dart CREATE Critical Responsive top bar with LayoutBuilder: search field, notifications, avatar, location
5 1 lib/widgets/responsive_shell.dart CREATE Critical Master responsive scaffold wrapping all desktop screens: sidebar + topbar + content
6 1 lib/screens/responsive_layout.dart MODIFY High Add three-tier breakpoint support (mobile/tablet/desktop) and ScreenSize enum
7 1 lib/main.dart MODIFY Critical Update StartupScreen to use ResponsiveShell for desktop routing
8 2 lib/screens/home_desktop_screen.dart REWRITE Critical Split 1077-line monolith into thin orchestrator + focused content widget
9 2 lib/screens/home_desktop_screen.dart REWRITE Critical Replace marquee with rich hero carousel (PageView.builder matching mobile)
10 2 lib/screens/home_desktop_screen.dart REWRITE Critical Replace fixed 3-column grid with SliverGrid + SliverGridDelegateWithMaxCrossAxisExtent
11 2 lib/screens/home_desktop_screen.dart REWRITE High Add MouseRegion + SystemMouseCursors.click on all tappable cards
12 3 lib/screens/desktop_login_screen.dart REWRITE High Video background + glassmorphism card matching mobile login style
13 3 lib/screens/desktop_login_screen.dart REWRITE High Continue as Guest button prominent
14 4 lib/screens/calendar_screen.dart MODIFY Medium Remove LandscapeShell; use Row with Card-based calendar left + event list right inside ResponsiveShell
15 4 lib/screens/calendar_screen.dart MODIFY Medium New _eventCardLandscape() with larger images (200px), full title, date, location
16 4 lib/screens/profile_screen.dart MODIFY Medium Remove LandscapeShell; Row with profile card left + tabbed events right
17 4 lib/screens/profile_screen.dart MODIFY Medium DefaultTabController with Ongoing/Upcoming/Past tabs + SliverList per tab
18 4 lib/screens/contribute_screen.dart MODIFY Medium Remove LandscapeShell; Row with contributor info/nav left + tab content right
19 4 lib/screens/contribute_screen.dart MODIFY Medium Vertical nav list for landscape tabs instead of horizontal TabBar
20 4 lib/screens/settings_screen.dart MODIFY Medium Remove LandscapeShell; Row with settings nav left + content right
21 4 lib/screens/settings_screen.dart MODIFY Medium Fix logout navigation to use unified approach
22 4 lib/screens/search_screen.dart MODIFY Medium Add landscape layout: full-width search bar + Row(popular cities, results)
23 4 lib/screens/learn_more_screen.dart MODIFY Low Replace hard-coded 820 with AppConstants; add RepaintBoundary; LayoutBuilder for height
24 5 lib/widgets/landscape_shell.dart DELETE High Replaced by responsive_shell.dart
25 5 lib/widgets/landscape_section_header.dart DELETE or REWRITE Low Generalize into SectionHeader or delete if unused
26 5 lib/core/app_decoration.dart MODIFY Low Add desktop-specific decorations: cardShadow, sidebarDecoration, topBarShadow
27 8 lib/features/events/services/events_service.dart MODIFY Critical Set requiresAuth: false on getEventTypes, getEventsByPincode, getEventDetails, getEventsByMonthYear

View File

@@ -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
1 Phase File Change Type Description
2 1 lib/core/constants.dart MODIFY Updated sidebarExpandedWidth to 262px and removed sidebarCollapsedWidth
3 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
4 1 lib/widgets/desktop_topbar.dart REWRITE Figma match: search input field plus notification bell plus user avatar and removed location widget
5 1 lib/widgets/responsive_shell.dart REWRITE Simplified to always show 262px sidebar on desktop and removed collapsed mode
6 2 lib/screens/home_desktop_screen.dart REWRITE Website-matching hero (400px Ken Burns animation plus dark overlay plus CTA) replacing welcome banner
7 2 lib/screens/home_desktop_screen.dart MODIFY Pill-shaped category chips (border-radius 999px) with blue active state matching website
8 2 lib/screens/home_desktop_screen.dart MODIFY Event cards enhanced with date badge overlay on image and blue/green icons for date/venue
9 2 lib/screens/home_desktop_screen.dart MODIFY Background color changed to #FAFBFC (off-white) matching website
10 2 lib/screens/home_desktop_screen.dart ADD Featured event modal dialog with large image plus gradient plus Learn More CTA plus close button
11 3 lib/screens/calendar_screen.dart MODIFY Desktop layout: calendar grid 60% left plus events panel 40% right with white background
12 3 lib/screens/calendar_screen.dart MODIFY Event cards use blue and green dot indicators for date and venue
13 4 lib/screens/profile_screen.dart MODIFY Desktop layout: full-width profile banner plus 3-column event grids for Upcoming and Past
14 4 lib/screens/profile_screen.dart MODIFY Added _buildDesktopLayout and _buildDesktopEventSection and _buildDesktopEventGridCard
15 5 lib/screens/learn_more_screen.dart MODIFY Desktop layout: full-width 300px hero image plus About/Venue two-column plus gallery strip
16 5 lib/screens/learn_more_screen.dart MODIFY Added horizontal gallery with overflow count badge and Book Your Spot CTA
17 6 lib/features/events/services/events_service.dart VERIFIED Guest access confirmed working with requiresAuth false on 4 read endpoints

View File

@@ -1,10 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AppConstants { class AppConstants {
// Layout // Layout — breakpoints
static const double desktopBreakpoint = 820; static const double desktopBreakpoint = 820;
static const double wideDesktopBreakpoint = 1200;
static const double tabletBreakpoint = 600; static const double tabletBreakpoint = 600;
// Desktop sidebar
static const double sidebarExpandedWidth = 262;
static const double topBarHeight = 64;
static const double desktopHorizontalPadding = 24;
// Padding & Radius // Padding & Radius
static const double defaultPadding = 16; static const double defaultPadding = 16;
static const double cardRadius = 14; static const double cardRadius = 14;

View File

@@ -9,7 +9,7 @@ class EventsService {
/// Get event types (POST to /events/type-list/) /// Get event types (POST to /events/type-list/)
Future<List<EventTypeModel>> getEventTypes() async { Future<List<EventTypeModel>> getEventTypes() async {
final res = await _api.post(ApiEndpoints.eventTypes); final res = await _api.post(ApiEndpoints.eventTypes, requiresAuth: false);
final list = <EventTypeModel>[]; final list = <EventTypeModel>[];
final data = res['event_types'] ?? res['event_types'] ?? res; final data = res['event_types'] ?? res['event_types'] ?? res;
if (data is List) { if (data is List) {
@@ -27,7 +27,7 @@ class EventsService {
/// Get events filtered by pincode (POST to /events/pincode-events/) /// Get events filtered by pincode (POST to /events/pincode-events/)
/// Use pincode='all' to fetch all events. /// Use pincode='all' to fetch all events.
Future<List<EventModel>> getEventsByPincode(String pincode) async { Future<List<EventModel>> 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 = <EventModel>[]; final list = <EventModel>[];
final events = res['events'] ?? res['data'] ?? []; final events = res['events'] ?? res['data'] ?? [];
if (events is List) { if (events is List) {
@@ -40,7 +40,7 @@ class EventsService {
/// Event details /// Event details
Future<EventModel> getEventDetails(int eventId) async { Future<EventModel> 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<String, dynamic>.from(res)); return EventModel.fromJson(Map<String, dynamic>.from(res));
} }
@@ -48,7 +48,7 @@ class EventsService {
/// Accepts month string and year int. /// Accepts month string and year int.
/// Returns Map with 'dates' (list of YYYY-MM-DD) and 'date_events' (list with counts). /// Returns Map with 'dates' (list of YYYY-MM-DD) and 'date_events' (list with counts).
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async { Future<Map<String, dynamic>> 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 // expected keys: dates, total_number_of_events, date_events
return res; return res;
} }

View File

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

View File

@@ -13,6 +13,7 @@ import 'package:share_plus/share_plus.dart';
import '../core/app_decoration.dart'; import '../core/app_decoration.dart';
import '../features/gamification/models/gamification_models.dart'; import '../features/gamification/models/gamification_models.dart';
import '../features/gamification/providers/gamification_provider.dart'; import '../features/gamification/providers/gamification_provider.dart';
import '../widgets/landscape_section_header.dart';
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Tier colour map // Tier colour map
@@ -138,156 +139,184 @@ class _ContributeScreenState extends State<ContributeScreen>
static const _desktopTabIcons = [Icons.edit_note, null, null]; static const _desktopTabIcons = [Icons.edit_note, null, null];
Widget _buildDesktopLayout(BuildContext context, GamificationProvider provider) { 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 profile = provider.profile;
final tier = profile?.tier ?? ContributorTier.BRONZE; final tier = profile?.tier ?? ContributorTier.BRONZE;
final lifetimeEp = profile?.lifetimeEp ?? 0; 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]; const thresholds = [0, 100, 500, 1500, 5000];
final tierIdx = tier.index; final tierIdx = tier.index;
final nextThresh = tierIdx < 4 ? thresholds[tierIdx + 1] : thresholds[4]; final nextThresh = tierIdx < 4 ? thresholds[tierIdx + 1] : thresholds[4];
final prevThresh = thresholds[tierIdx]; final prevThresh = thresholds[tierIdx];
final progress = tierIdx >= 4 ? 1.0 : (lifetimeEp - prevThresh) / (nextThresh - prevThresh); final progress = tierIdx >= 4 ? 1.0 : (lifetimeEp - prevThresh) / (nextThresh - prevThresh);
final tierColor = _tierColors[tier] ?? Colors.white;
return SingleChildScrollView( return SafeArea(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24), child: SingleChildScrollView(
child: Center( physics: const BouncingScrollPhysics(),
child: ConstrainedBox( child: Column(
constraints: const BoxConstraints(maxWidth: 900), crossAxisAlignment: CrossAxisAlignment.stretch,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.center, const SizedBox(height: 24),
children: [ // Title
// Title const Padding(
const Text('Contributor Dashboard', padding: EdgeInsets.symmetric(horizontal: 20),
style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, color: Color(0xFF111827))), child: Text(
const SizedBox(height: 6), 'Contributor\nDashboard',
const Text('Track your impact, earn rewards, and climb the ranks!', style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w800, height: 1.2),
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,
),
),
],
),
),
),
);
}),
),
), ),
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 ── // Contributor Level badge
Container( Padding(
width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.all(24), child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: const LinearGradient( color: Colors.white.withOpacity(0.1),
colors: [Color(0xFF0F45CF), Color(0xFF3B82F6)], borderRadius: BorderRadius.circular(14),
begin: Alignment.topLeft, border: Border.all(color: tierColor.withOpacity(0.5)),
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, Container(
children: [ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
Column( decoration: BoxDecoration(
crossAxisAlignment: CrossAxisAlignment.start, color: tierColor.withOpacity(0.2),
children: [ borderRadius: BorderRadius.circular(20),
const Text('Contributor Level', border: Border.all(color: tierColor.withOpacity(0.6)),
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)),
],
), ),
Container( child: Text(tierLabel(tier), style: TextStyle(color: tierColor, fontWeight: FontWeight.w700, fontSize: 12)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ),
decoration: BoxDecoration( const Spacer(),
color: Colors.white.withOpacity(0.2), Text('$lifetimeEp pts', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14)),
borderRadius: BorderRadius.circular(20), ]),
), const SizedBox(height: 10),
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),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: progress.clamp(0.0, 1.0), value: progress.clamp(0.0, 1.0),
minHeight: 8, minHeight: 6,
backgroundColor: Colors.white.withOpacity(0.2), backgroundColor: Colors.white24,
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white), valueColor: AlwaysStoppedAnimation<Color>(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 ── // Vertical tab navigation
_buildDesktopTabBody(context, provider), ...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) { Widget _buildDesktopTabBody(BuildContext context, GamificationProvider provider) {
switch (_activeTab) { switch (_activeTab) {
case 0: case 0:

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../core/auth/auth_guard.dart'; import '../core/auth/auth_guard.dart';
import 'package:cached_network_image/cached_network_image.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/models/event_models.dart';
import '../features/events/services/events_service.dart'; import '../features/events/services/events_service.dart';
@@ -61,15 +62,15 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
super.dispose(); super.dispose();
} }
void _startAutoScroll() { void _startAutoScroll({Duration delay = const Duration(seconds: 5)}) {
_autoScrollTimer?.cancel(); _autoScrollTimer?.cancel();
_autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (timer) { _autoScrollTimer = Timer.periodic(delay, (timer) {
if (_heroEvents.isEmpty) return; if (_heroEvents.isEmpty) return;
final nextPage = (_heroPageNotifier.value + 1) % _heroEvents.length; final nextPage = (_heroPageNotifier.value + 1) % _heroEvents.length;
if (_heroPageController.hasClients) { if (_heroPageController.hasClients) {
_heroPageController.animateToPage( _heroPageController.animateToPage(
nextPage, nextPage,
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
} }
@@ -80,7 +81,14 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
setState(() => _loading = true); setState(() => _loading = true);
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
_username = prefs.getString('display_name') ?? prefs.getString('username') ?? ''; _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'; _pincode = prefs.getString('pincode') ?? 'all';
try { try {
@@ -482,7 +490,26 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
// Get hero events (first 4 events for the carousel) // Get hero events (first 4 events for the carousel)
List<EventModel> get _heroEvents => _events.take(4).toList(); List<EventModel> 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 // Date filter state
String _selectedDateFilter = ''; String _selectedDateFilter = '';
@@ -1131,42 +1158,51 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
// Featured carousel // Featured carousel
_heroEvents.isEmpty _heroEvents.isEmpty
? SizedBox( ? _loading
height: 280, ? const Padding(
child: Center( padding: EdgeInsets.symmetric(horizontal: 8),
child: _loading child: SizedBox(
? const CircularProgressIndicator(color: Colors.white) height: 320,
: const Text('No events available', style: TextStyle(color: Colors.white70)), child: _HeroShimmer(),
), ),
) )
: const SizedBox(
height: 280,
child: Center(
child: Text('No events available',
style: TextStyle(color: Colors.white70)),
),
)
: Column( : Column(
children: [ children: [
SizedBox( RepaintBoundary(
height: 320, child: SizedBox(
child: PageView.builder( height: 320,
controller: _heroPageController, child: PageView.builder(
onPageChanged: (page) { controller: _heroPageController,
_heroPageNotifier.value = page; onPageChanged: (page) {
// Reset 3-second countdown so user always gets full read time _heroPageNotifier.value = page;
_startAutoScroll(); // 8s delay after manual swipe for full read time
}, _startAutoScroll(delay: const Duration(seconds: 8));
itemCount: _heroEvents.length, },
itemBuilder: (context, index) { itemCount: _heroEvents.length,
// Scale animation: active card = 1.0, adjacent = 0.94 itemBuilder: (context, index) {
return AnimatedBuilder( // Scale animation: active card = 1.0, adjacent = 0.94
animation: _heroPageController, return AnimatedBuilder(
builder: (context, child) { animation: _heroPageController,
double scale = index == _heroPageNotifier.value ? 1.0 : 0.94; builder: (context, child) {
if (_heroPageController.position.haveDimensions) { double scale = index == _heroPageNotifier.value ? 1.0 : 0.94;
scale = (1.0 - if (_heroPageController.position.haveDimensions) {
(_heroPageController.page! - index).abs() * 0.06) scale = (1.0 -
.clamp(0.94, 1.0); (_heroPageController.page! - index).abs() * 0.06)
} .clamp(0.94, 1.0);
return Transform.scale(scale: scale, child: child); }
}, return Transform.scale(scale: scale, child: child);
child: _buildHeroEventImage(_heroEvents[index]), },
); child: _buildHeroEventImage(_heroEvents[index]),
}, );
},
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -1185,7 +1221,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
valueListenable: _heroPageNotifier, valueListenable: _heroPageNotifier,
builder: (context, currentPage, _) { builder: (context, currentPage, _) {
return SizedBox( return SizedBox(
height: 12, height: 44,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: List.generate( children: List.generate(
@@ -1199,13 +1235,21 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
} }
}, },
child: Container( child: SizedBox(
margin: const EdgeInsets.symmetric(horizontal: 4), width: 44,
width: isActive ? 24 : 8, height: 44,
height: 8, child: Center(
decoration: BoxDecoration( child: AnimatedContainer(
color: isActive ? Colors.white : Colors.white.withOpacity(0.4), duration: const Duration(milliseconds: 200),
borderRadius: BorderRadius.circular(4), 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<HomeScreen> with SingleTickerProviderStateM
img != null && img.isNotEmpty img != null && img.isNotEmpty
? CachedNetworkImage( ? CachedNetworkImage(
imageUrl: img, imageUrl: img,
memCacheWidth: 800, memCacheWidth: 700,
fit: BoxFit.cover, fit: BoxFit.cover,
placeholder: (_, __) => const _HeroShimmer(radius: radius), placeholder: (_, __) => const _HeroShimmer(radius: radius),
errorWidget: (_, __, ___) => errorWidget: (_, __, ___) =>
@@ -1272,7 +1316,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
), ),
), ),
// ── Layer 2: FEATURED glassmorphism badge (top-left) ── // ── Layer 2: Event type glassmorphism badge (top-left) ──
Positioned( Positioned(
top: 14, top: 14,
left: 14, left: 14,
@@ -1283,18 +1327,18 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.18), color: Colors.white.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(20), 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, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.star_rounded, color: Colors.amber, size: 13), const Icon(Icons.star_rounded, color: Colors.amber, size: 13),
SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'FEATURED', _getEventTypeName(event),
style: TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
@@ -1339,7 +1383,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
color: Colors.white70, size: 12), color: Colors.white70, size: 12),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
event.startDate!, _formatDate(event.startDate!),
style: const TextStyle( style: const TextStyle(
color: Colors.white70, color: Colors.white70,
fontSize: 12, fontSize: 12,
@@ -2198,7 +2242,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
/// Renders a blue-toned scan-line effect matching the app's colour palette. /// Renders a blue-toned scan-line effect matching the app's colour palette.
class _HeroShimmer extends StatefulWidget { class _HeroShimmer extends StatefulWidget {
final double radius; final double radius;
const _HeroShimmer({required this.radius}); const _HeroShimmer({this.radius = 24.0});
@override @override
State<_HeroShimmer> createState() => _HeroShimmerState(); State<_HeroShimmer> createState() => _HeroShimmerState();

View File

@@ -11,6 +11,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import '../features/events/models/event_models.dart'; import '../features/events/models/event_models.dart';
import '../features/events/services/events_service.dart'; import '../features/events/services/events_service.dart';
import '../core/auth/auth_guard.dart'; import '../core/auth/auth_guard.dart';
import '../core/constants.dart';
class LearnMoreScreen extends StatefulWidget { class LearnMoreScreen extends StatefulWidget {
final int eventId; final int eventId;
@@ -227,10 +228,279 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
} }
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final screenWidth = mediaQuery.size.width;
final screenHeight = mediaQuery.size.height; final screenHeight = mediaQuery.size.height;
final imageHeight = screenHeight * 0.45; final imageHeight = screenHeight * 0.45;
final topPadding = mediaQuery.padding.top; 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( return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
body: Stack( body: Stack(

View File

@@ -12,6 +12,7 @@ import 'learn_more_screen.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
import '../core/app_decoration.dart'; import '../core/app_decoration.dart';
import '../core/constants.dart'; import '../core/constants.dart';
import '../widgets/landscape_section_header.dart';
class ProfileScreen extends StatefulWidget { class ProfileScreen extends StatefulWidget {
const ProfileScreen({Key? key}) : super(key: key); const ProfileScreen({Key? key}) : super(key: key);
@@ -1013,6 +1014,534 @@ class _ProfileScreenState extends State<ProfileScreen>
); );
} }
// ═══════════════════════════════════════════════
// 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<EventModel> 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<EventModel> 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 // BUILD
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
@@ -1022,6 +1551,7 @@ class _ProfileScreenState extends State<ProfileScreen>
final theme = Theme.of(context); final theme = Theme.of(context);
const double headerHeight = 200.0; const double headerHeight = 200.0;
const double cardTopOffset = 130.0; const double cardTopOffset = 130.0;
final width = MediaQuery.of(context).size.width;
Widget sectionTitle(String text) => Padding( Widget sectionTitle(String text) => Padding(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 12), padding: const EdgeInsets.fromLTRB(18, 16, 18, 12),
@@ -1032,6 +1562,12 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
); );
// ── DESKTOP / LANDSCAPE layout ─────────────────────────────────────────
if (width >= AppConstants.desktopBreakpoint) {
return _buildDesktopLayout(context, theme);
}
// ── MOBILE layout ─────────────────────────────────────────────────────
return Scaffold( return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
// CustomScrollView: only visible event cards are built — no full-tree Column renders // CustomScrollView: only visible event cards are built — no full-tree Column renders

View File

@@ -149,7 +149,7 @@ class _SearchScreenState extends State<SearchScreen> {
} }
} catch (_) {} } 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) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not determine location: $e'))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not determine location: $e')));

View File

@@ -3,7 +3,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'login_screen.dart'; import 'login_screen.dart';
import 'desktop_login_screen.dart'; import 'desktop_login_screen.dart';
import '../core/theme_manager.dart'; import '../core/theme_manager.dart';
import 'privacy_policy_screen.dart'; // new import import 'privacy_policy_screen.dart';
import '../core/app_decoration.dart'; import '../core/app_decoration.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
@@ -15,7 +15,8 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> { class _SettingsScreenState extends State<SettingsScreen> {
bool _notifications = true; bool _notifications = true;
String _appVersion = '1.2(p)'; String _appVersion = '1.6(p)';
int _selectedSection = 0; // 0=Preferences, 1=Account, 2=About
@override @override
void initState() { void initState() {
@@ -100,16 +101,209 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
// ── 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<ThemeMode>(
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const primary = Color(0xFF0B63D6); 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( return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
// Header
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.fromLTRB(20, 18, 20, 18), padding: const EdgeInsets.fromLTRB(20, 18, 20, 18),
@@ -131,41 +325,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
], ],
), ),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
// Content
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(18, 0, 18, 24), padding: const EdgeInsets.fromLTRB(18, 0, 18, 24),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Account
const Text('Account', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const Text('Account', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildTile( _buildTile(
icon: Icons.person, icon: Icons.person,
title: 'Edit Profile', title: 'Edit Profile',
subtitle: 'Change username, email or photo', subtitle: 'Change username, email or photo',
onTap: () { onTap: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Open Profile tab (demo)'))),
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Open Profile tab (demo)')));
},
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Preferences
const Text('Preferences', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const Text('Preferences', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12), const SizedBox(height: 12),
// Reminders switch wrapped in card-like container
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 6), margin: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(12), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))]),
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))],
),
child: SwitchListTile( child: SwitchListTile(
tileColor: Theme.of(context).cardColor, tileColor: Theme.of(context).cardColor,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
@@ -175,54 +355,34 @@ class _SettingsScreenState extends State<SettingsScreen> {
secondary: const Icon(Icons.notifications, color: primary), secondary: const Icon(Icons.notifications, color: primary),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Dark Mode switch wrapped in card-like container and hooked to ThemeManager
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 6), margin: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(12), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))]),
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 4))],
),
child: ValueListenableBuilder<ThemeMode>( child: ValueListenableBuilder<ThemeMode>(
valueListenable: ThemeManager.themeMode, valueListenable: ThemeManager.themeMode,
builder: (context, mode, _) { builder: (context, mode, _) => SwitchListTile(
final isDark = mode == ThemeMode.dark; tileColor: Theme.of(context).cardColor,
return SwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
tileColor: Theme.of(context).cardColor, value: mode == ThemeMode.dark,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), onChanged: (v) => ThemeManager.setThemeMode(v ? ThemeMode.dark : ThemeMode.light),
value: isDark, title: const Text('Dark Mode'),
onChanged: (v) => ThemeManager.setThemeMode(v ? ThemeMode.dark : ThemeMode.light), secondary: const Icon(Icons.dark_mode, color: primary),
title: const Text('Dark Mode'), ),
secondary: const Icon(Icons.dark_mode, color: primary),
);
},
), ),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
// About
const Text('About', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const Text('About', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildTile(icon: Icons.info_outline, title: 'App Version', subtitle: _appVersion, onTap: () {}), _buildTile(icon: Icons.info_outline, title: 'App Version', subtitle: _appVersion, onTap: () {}),
const SizedBox(height: 12), const SizedBox(height: 12),
// Privacy Policy tile now navigates to PrivacyPolicyScreen
_buildTile( _buildTile(
icon: Icons.privacy_tip_outlined, icon: Icons.privacy_tip_outlined,
title: 'Privacy Policy', title: 'Privacy Policy',
subtitle: 'Demo app', subtitle: 'Demo app',
onTap: () { onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PrivacyPolicyScreen())),
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PrivacyPolicyScreen()));
},
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Logout area
Center( Center(
child: Column( child: Column(
children: [ children: [
@@ -240,7 +400,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
], ],
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
], ],
), ),

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<int> 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<ResponsiveShell> createState() => _ResponsiveShellState();
}
class _ResponsiveShellState extends State<ResponsiveShell> {
String _username = 'Guest';
String? _profileImage;
@override
void initState() {
super.initState();
_loadPreferences();
}
Future<void> _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),
),
],
),
),
],
),
);
});
}
}

View File

@@ -1,7 +1,7 @@
name: figma name: figma
description: A Flutter event app description: A Flutter event app
publish_to: 'none' publish_to: 'none'
version: 1.5.0+15 version: 1.6.1+17
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"