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:
@@ -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
28
android/app/proguard-rules.pro
vendored
Normal 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
|
||||||
15
docs/hero_slider_changes.csv
Normal file
15
docs/hero_slider_changes.csv
Normal 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()
|
||||||
|
27
docs/landscape_changes.csv
Normal file
27
docs/landscape_changes.csv
Normal 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"
|
||||||
|
17
docs/landscape_rebuild_changes.csv
Normal file
17
docs/landscape_rebuild_changes.csv
Normal 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,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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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])),
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')));
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
141
lib/widgets/desktop_sidebar.dart
Normal file
141
lib/widgets/desktop_sidebar.dart
Normal 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);
|
||||||
|
}
|
||||||
142
lib/widgets/desktop_topbar.dart
Normal file
142
lib/widgets/desktop_topbar.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
lib/widgets/landscape_section_header.dart
Normal file
58
lib/widgets/landscape_section_header.dart
Normal 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!,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
lib/widgets/landscape_shell.dart
Normal file
67
lib/widgets/landscape_shell.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
lib/widgets/responsive_shell.dart
Normal file
84
lib/widgets/responsive_shell.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user