diff --git a/analyze_output.txt b/analyze_output.txt new file mode 100644 index 0000000..3bce1be --- /dev/null +++ b/analyze_output.txt @@ -0,0 +1,145 @@ +Analyzing figma_event_app... + +warning - The include file 'package:flutter_lints/flutter.yaml' in 'D:\projects\figma_event_app\analysis_options.yaml' can't be found when analyzing 'D:\projects\figma_event_app' - analysis_options.yaml:10:10 - include_file_not_found +warning - Unused import: 'package:intl/intl.dart' - lib\features\events\services\events_service.dart:2:8 - unused_import +warning - Unused import: 'dart:math' - lib\screens\calendar_screen.dart:2:8 - unused_import +warning - The value of the field '_loadingMonth' isn't used - lib\screens\calendar_screen.dart:23:8 - unused_field + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:252:77 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:255:99 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:365:65 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:417:62 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:491:63 - deprecated_member_use +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\calendar_screen.dart:528:36 - unnecessary_null_comparison +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\calendar_screen.dart:528:57 - unnecessary_null_comparison +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\calendar_screen.dart:530:24 - unnecessary_null_comparison +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\calendar_screen.dart:530:45 - unnecessary_null_comparison +warning - The left operand can't be null, so the right operand is never executed - lib\screens\calendar_screen.dart:530:105 - dead_null_aware_expression +warning - The left operand can't be null, so the right operand is never executed - lib\screens\calendar_screen.dart:548:43 - dead_null_aware_expression + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:551:111 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:557:111 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:135:41 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:159:79 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:175:37 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:177:56 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:209:47 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:241:31 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:243:50 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:244:53 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:259:72 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:278:71 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:308:51 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:389:140 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:400:88 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:553:111 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:679:55 - deprecated_member_use +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:774:39 - dead_null_aware_expression +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:776:33 - dead_null_aware_expression +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:795:36 - unnecessary_null_comparison +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:795:57 - unnecessary_null_comparison +warning - The '!' will have no effect because the receiver can't be null - lib\screens\home_desktop_screen.dart:796:22 - unnecessary_non_null_assertion +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:797:25 - unnecessary_null_comparison +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:797:46 - unnecessary_null_comparison +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:797:107 - dead_null_aware_expression + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:806:43 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:807:43 - deprecated_member_use +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:829:42 - dead_null_aware_expression + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:844:84 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:865:84 - deprecated_member_use +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:898:36 - unnecessary_null_comparison +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:898:57 - unnecessary_null_comparison +warning - The '!' will have no effect because the receiver can't be null - lib\screens\home_desktop_screen.dart:899:22 - unnecessary_non_null_assertion +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:900:25 - unnecessary_null_comparison +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:900:46 - unnecessary_null_comparison +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:900:107 - dead_null_aware_expression + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:910:43 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:911:43 - deprecated_member_use +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:932:42 - dead_null_aware_expression + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:943:84 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:955:84 - deprecated_member_use +warning - The declaration '_bookEventAtIndex' isn't referenced - lib\screens\home_screen.dart:119:8 - unused_element + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:143:47 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:144:34 - deprecated_member_use +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:239:55 - dead_null_aware_expression +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:319:68 - dead_null_aware_expression +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:320:64 - dead_null_aware_expression +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:330:45 - unnecessary_null_comparison + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:389:56 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:462:47 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:521:35 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:541:41 - deprecated_member_use +warning - The value of the local variable 'theme' isn't used - lib\screens\home_screen.dart:556:11 - unused_local_variable + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:578:43 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:580:62 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:604:43 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:606:62 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:677:59 - deprecated_member_use +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:712:22 - unnecessary_null_comparison +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:743:44 - dead_null_aware_expression +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:1015:18 - unnecessary_null_comparison + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:1022:58 - deprecated_member_use +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:1037:43 - dead_null_aware_expression +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:1076:18 - unnecessary_null_comparison + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:1126:49 - deprecated_member_use +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:1162:36 - dead_null_aware_expression +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:1231:18 - unnecessary_null_comparison + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:1239:48 - deprecated_member_use +warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:1313:42 - dead_null_aware_expression +warning - The declaration '_getShortEmailLabel' isn't referenced - lib\screens\home_screen.dart:1385:10 - unused_element + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:265:43 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:275:43 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:284:43 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:293:43 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:302:43 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:399:42 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:445:35 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:447:54 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:524:59 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:531:59 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:619:51 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:669:47 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:741:46 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:784:31 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:788:35 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:852:50 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:855:52 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:865:56 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:923:48 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:926:50 - deprecated_member_use +warning - The value of the local variable 'headingStyle' isn't used - lib\screens\privacy_policy_screen.dart:86:11 - unused_local_variable +warning - The value of the field '_upcomingEvents' isn't used - lib\screens\profile_screen.dart:30:20 - unused_field +warning - The declaration '_topIcon' isn't referenced - lib\screens\profile_screen.dart:290:10 - unused_element + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\profile_screen.dart:298:58 - deprecated_member_use +warning - The left operand can't be null, so the right operand is never executed - lib\screens\profile_screen.dart:306:42 - dead_null_aware_expression +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:307:37 - unnecessary_null_comparison +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:307:59 - unnecessary_null_comparison +warning - The '!' will have no effect because the receiver can't be null - lib\screens\profile_screen.dart:308:23 - unnecessary_non_null_assertion +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:309:26 - unnecessary_null_comparison +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:309:48 - unnecessary_null_comparison +warning - The left operand can't be null, so the right operand is never executed - lib\screens\profile_screen.dart:309:112 - dead_null_aware_expression + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\profile_screen.dart:314:108 - deprecated_member_use + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\profile_screen.dart:345:68 - deprecated_member_use +warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:349:21 - unnecessary_null_comparison + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\profile_screen.dart:368:82 - deprecated_member_use +warning - The value of the local variable 'gradient' isn't used - lib\screens\profile_screen.dart:397:11 - unused_local_variable + info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\search_screen.dart:130:52 - deprecated_member_use + +flutter : 122 issues +found. (ran in 5.2s) +At line:1 char:1 ++ flutter analyze +2>&1 | Out-File +-Encoding utf8 D:\pro +jects\figma_even ... ++ +~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo + : NotSpe + cified: (122 iss + ues found. (ran +in 5.2s):String) +[], RemoteExcep +tion + + FullyQualified + ErrorId : Native + CommandError + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ff0b212..74bf722 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,15 +5,19 @@ - - + + + + + + + + diff --git a/assets/icon/hand_stop.svg b/assets/icon/hand_stop.svg new file mode 100644 index 0000000..0775369 --- /dev/null +++ b/assets/icon/hand_stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 6266644..1a8b602 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ import Flutter import UIKit +import GoogleMaps @main @objc class AppDelegate: FlutterAppDelegate { @@ -7,6 +8,7 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + GMSServices.provideAPIKey("YOUR_GOOGLE_MAPS_API_KEY") GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/lib/features/events/models/event_models.dart b/lib/features/events/models/event_models.dart index 11de701..cf8c864 100644 --- a/lib/features/events/models/event_models.dart +++ b/lib/features/events/models/event_models.dart @@ -51,6 +51,14 @@ class EventModel { final String? eventStatus; final String? cancelledReason; + // Geo / location fields + final double? latitude; + final double? longitude; + final String? locationName; + + // Structured important info list [{title, value}, ...] + final List> importantInfo; + EventModel({ required this.id, required this.name, @@ -70,8 +78,36 @@ class EventModel { this.venueName, this.eventStatus, this.cancelledReason, + this.latitude, + this.longitude, + this.locationName, + this.importantInfo = const [], }); + /// Safely parse a double from backend (may arrive as String or num) + static double? _parseDouble(dynamic raw) { + if (raw == null) return null; + if (raw is num) return raw.toDouble(); + if (raw is String) return double.tryParse(raw); + return null; + } + + /// Safely parse important_info from backend (list of {title, value} maps) + static List> _parseImportantInfo(dynamic raw) { + if (raw is List) { + return raw.map>((e) { + if (e is Map) { + return { + 'title': (e['title'] ?? '').toString(), + 'value': (e['value'] ?? '').toString(), + }; + } + return {'title': '', 'value': e.toString()}; + }).toList(); + } + return []; + } + factory EventModel.fromJson(Map j) { final imgs = []; if (j['images'] is List) { @@ -99,6 +135,10 @@ class EventModel { venueName: j['venue_name'] as String?, eventStatus: j['event_status'] as String?, cancelledReason: j['cancelled_reason'] as String?, + latitude: _parseDouble(j['latitude']), + longitude: _parseDouble(j['longitude']), + locationName: j['location_name'] as String?, + importantInfo: _parseImportantInfo(j['important_info']), ); } } diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 7cc73b9..105c78f 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -12,6 +12,7 @@ import 'contribute_screen.dart'; import 'learn_more_screen.dart'; import 'search_screen.dart'; import '../core/app_decoration.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @@ -125,46 +126,69 @@ class _HomeScreenState extends State with SingleTickerProviderStateM required String label, required bool selected, required VoidCallback onTap, + String? imageUrl, IconData? icon, }) { final theme = Theme.of(context); - return InkWell( - borderRadius: BorderRadius.circular(20), + return GestureDetector( onTap: onTap, child: Container( - height: 40, - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 16), + width: 110, decoration: BoxDecoration( - color: selected ? theme.colorScheme.primary : theme.cardColor, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: selected ? theme.colorScheme.primary : theme.dividerColor, - width: 1, - ), - boxShadow: selected - ? [ - BoxShadow( - color: theme.colorScheme.primary.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 4), - ) - ] - : [], + color: selected ? theme.colorScheme.primary : Colors.white, + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: selected + ? theme.colorScheme.primary.withOpacity(0.35) + : Colors.black.withOpacity(0.06), + blurRadius: selected ? 12 : 8, + offset: const Offset(0, 4), + ), + ], ), - child: Row( - mainAxisSize: MainAxisSize.min, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - if (icon != null) ...[ - Icon(icon, size: 16, color: selected ? Colors.white : theme.colorScheme.primary), - const SizedBox(width: 6), - ], - Text( - label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: selected ? Colors.white : theme.textTheme.bodyLarge?.color, + // Image / Icon area + SizedBox( + height: 56, + width: 56, + child: imageUrl != null && imageUrl.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + imageUrl, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => Icon( + icon ?? Icons.category, + size: 36, + color: selected ? Colors.white : theme.colorScheme.primary, + ), + ), + ) + : Icon( + icon ?? Icons.category, + size: 36, + color: selected ? Colors.white : theme.colorScheme.primary, + ), + ), + const SizedBox(height: 10), + // Label + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: selected + ? Colors.white + : theme.textTheme.bodyLarge?.color ?? Colors.black87, + ), ), ), ], @@ -367,39 +391,96 @@ class _HomeScreenState extends State with SingleTickerProviderStateM child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _bottomNavItem(0, Icons.home, 'Home'), - _bottomNavItem(1, Icons.calendar_today, 'Calendar'), - _bottomNavItem(2, Icons.volunteer_activism, 'Contribute'), - _bottomNavItem(3, Icons.person, 'Profile'), + _bottomNavItem( + 0, + Icon( + Icons.home, + color: _selectedIndex == 0 + ? theme.colorScheme.primary + : theme.iconTheme.color, + ), + 'Home', + ), + _bottomNavItem( + 1, + Icon( + Icons.calendar_today, + color: _selectedIndex == 1 + ? theme.colorScheme.primary + : theme.iconTheme.color, + ), + 'Calendar', + ), + _bottomNavItem( + 2, + SvgPicture.asset( + 'assets/icon/hand_stop.svg', + height: 24, + width: 24, + colorFilter: ColorFilter.mode( + _selectedIndex == 2 + ? theme.colorScheme.primary + : theme.iconTheme.color!, + BlendMode.srcIn, + ), + ), + 'Contribute', + ), + _bottomNavItem( + 3, + Icon( + Icons.person, + color: _selectedIndex == 3 + ? theme.colorScheme.primary + : theme.iconTheme.color, + ), + 'Profile', + ), + ], ), ); } - Widget _bottomNavItem(int index, IconData icon, String label) { + Widget _bottomNavItem(int index, Widget icon, String label) { final theme = Theme.of(context); bool active = _selectedIndex == index; + return GestureDetector( onTap: () { setState(() { _selectedIndex = index; }); }, - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: active ? theme.colorScheme.primary.withOpacity(0.08) : Colors.transparent, - shape: BoxShape.circle, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: active + ? theme.colorScheme.primary.withOpacity(0.08) + : Colors.transparent, + shape: BoxShape.circle, + ), + child: icon, ), - child: Icon(icon, color: active ? theme.colorScheme.primary : theme.iconTheme.color), - ), - const SizedBox(height: 4), - Text(label, style: theme.textTheme.bodySmall?.copyWith(color: active ? theme.colorScheme.primary : theme.iconTheme.color, fontSize: 12)), - ]), + const SizedBox(height: 4), + Text( + label, + style: theme.textTheme.bodySmall?.copyWith( + color: active + ? theme.colorScheme.primary + : theme.iconTheme.color, + fontSize: 12, + ), + ), + ], + ), ); } + // Get hero events (first 4 events for the carousel) List get _heroEvents => _events.take(4).toList(); @@ -537,7 +618,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM // Hero image carousel (PageView) and fixed indicators under it. _heroEvents.isEmpty ? SizedBox( - height: 360, + height: 240, child: Center( child: _loading ? const CircularProgressIndicator(color: Colors.white) : const Text('No events available', style: TextStyle(color: Colors.white70)), ), @@ -546,7 +627,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM children: [ // PageView with only the images/titles SizedBox( - height: 360, + height: 300, child: PageView.builder( controller: _heroPageController, onPageChanged: (page) { @@ -560,9 +641,9 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ), // fixed indicators (outside PageView) - const SizedBox(height: 12), + const SizedBox(height: 20), SizedBox( - height: 28, + height: 20, child: Center( child: AnimatedBuilder( animation: _heroPageController, @@ -575,8 +656,8 @@ class _HomeScreenState extends State with SingleTickerProviderStateM mainAxisSize: MainAxisSize.min, children: List.generate(_heroEvents.length, (i) { final dx = (i - page).abs(); - final t = 1.0 - dx.clamp(0.0, 1.0); // 1 when focused, 0 when far - final width = 10 + (36 - 10) * t; // interpolate between 10 and 36 + final t = 1.0 - dx.clamp(0.0, 1.0); + final width = 7 + (24 - 7) * t; final opacity = 0.35 + (0.65 * t); return GestureDetector( onTap: () { @@ -589,12 +670,12 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } }, child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8), + margin: const EdgeInsets.symmetric(horizontal: 5), width: width, - height: 10, + height: 7, decoration: BoxDecoration( color: Colors.white.withOpacity(opacity), - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.circular(4), ), ), ); @@ -614,9 +695,8 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } - /// Build a hero image card (image + gradient + title). - /// If there's no image, show the AppDecoration blue gradient rounded background - /// and a black overlay gradient for contrast. + /// Build a hero image card with the image only (rounded), + /// and the title text placed below the image. Widget _buildHeroEventImage(EventModel event) { String? img; if (event.thumbImg != null && event.thumbImg!.isNotEmpty) { @@ -626,7 +706,6 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } final radius = 24.0; - final startDate = event.startDate ?? ''; return GestureDetector( onTap: () { @@ -636,75 +715,46 @@ class _HomeScreenState extends State with SingleTickerProviderStateM }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), - child: ClipRRect( - borderRadius: BorderRadius.circular(radius), - child: Stack( - fit: StackFit.expand, - children: [ - // If image available show it; otherwise use AppDecoration blue gradient. - if (img != null && img.isNotEmpty) - Image.network( - img, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => Container(decoration: AppDecoration.blueGradientRounded(radius)), - ) - else - Container( - decoration: AppDecoration.blueGradientRounded(radius), - ), - - // BLACK gradient overlay to darken bottom area for text (stronger to match your reference) - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.black.withOpacity(0.72), // strong black near bottom for contrast - Colors.black.withOpacity(0.38), - Colors.black.withOpacity(0.08), // subtle near top - ], - stops: const [0.0, 0.45, 1.0], - ), - ), - ), - - // Title and date positioned bottom-left - Padding( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 18), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (startDate.isNotEmpty) - Text( - startDate, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - fontWeight: FontWeight.w600, + child: Column( + children: [ + // Image only (no text overlay) + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: SizedBox( + width: double.infinity, + child: img != null && img.isNotEmpty + ? Image.network( + img, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + Container(decoration: AppDecoration.blueGradientRounded(radius)), + ) + : Container( + decoration: AppDecoration.blueGradientRounded(radius), ), - ), - if (startDate.isNotEmpty) const SizedBox(height: 8), - Text( - event.title ?? event.name ?? '', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 22, - fontWeight: FontWeight.bold, - height: 1.1, - shadows: [ - Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)), - ], - ), - ), - ], ), ), - ], - ), + ), + + // Title text outside the image + const SizedBox(height: 12), + Text( + event.title ?? event.name ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + height: 1.2, + shadows: [ + Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)), + ], + ), + ), + ], ), ), ); @@ -769,9 +819,9 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ), ), - // Category chips + // Category chips (card-style) SizedBox( - height: 48, + height: 140, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), @@ -782,15 +832,16 @@ class _HomeScreenState extends State with SingleTickerProviderStateM selected: _selectedTypeId == -1, onTap: () => _onSelectType(-1), ), - const SizedBox(width: 10), + const SizedBox(width: 12), for (final t in _types) ...[ _categoryChip( label: t.name, + imageUrl: t.iconUrl, icon: _getIconForType(t.name), selected: _selectedTypeId == t.id, onTap: () => _onSelectType(t.id), ), - const SizedBox(width: 10), + const SizedBox(width: 12), ], ], ), @@ -798,33 +849,42 @@ class _HomeScreenState extends State with SingleTickerProviderStateM const SizedBox(height: 16), - // Event cards - if (_loading) - const Padding( - padding: EdgeInsets.all(40), - child: Center(child: CircularProgressIndicator()), - ) - else if (_events.isEmpty) - Padding( - padding: const EdgeInsets.all(40), - child: Center( - child: Text( - 'No events found', - style: theme.textTheme.bodyLarge?.copyWith(color: theme.hintColor), + // --- NEW: when All Events is active, show only "types that have events" + if (_selectedTypeId == -1) ...[ + if (_loading) + const Padding( + padding: EdgeInsets.all(40), + child: Center(child: CircularProgressIndicator()), + ) + else + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + for (final t in _types) + if (_events.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[ + _buildTypeSection(t), + const SizedBox(height: 18), + ], + const SizedBox(height: 24), + ], ), ), - ) - else - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - for (int i = 0; i < _events.length; i++) ...[ - _buildEventCard(_events[i], i), - ], - ], + ] else ...[ + // Selected a specific type -> show filtered events in vertical list (full cards) + if (_loading) + const Padding( + padding: EdgeInsets.all(40), + child: Center(child: CircularProgressIndicator()), + ) + else + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: _events.map((e) => _buildFullWidthCard(e)).toList(), + ), ), - ), + ], // Bottom padding for nav bar const SizedBox(height: 100), @@ -832,6 +892,471 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } + /// Build a type section that follows your requested layout rules: + /// - If type has <= 5 events => single horizontal row of compact cards. + /// - If type has >= 6 events => arrange events into column groups of 3 (so visually there are 3 rows across horizontally scrollable columns). + Widget _buildTypeSection(EventTypeModel type) { + final theme = Theme.of(context); + final eventsForType = _events.where((e) => e.eventTypeId == type.id).toList(); + final n = eventsForType.length; + + // Header row + Widget header = Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(type.name, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + TextButton( + onPressed: () { + _onSelectType(type.id); + }, + child: Text('View All', style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600)), + ), + ], + ), + ); + + // If <= 5 events: show one horizontal row using _buildHorizontalEventCard + if (n <= 5) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + header, + const SizedBox(height: 8), + SizedBox( + height: 290, // card height: image 180 + text ~110 + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (ctx, idx) => _buildHorizontalEventCard(eventsForType[idx]), + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemCount: eventsForType.length, + ), + ), + ], + ); + } + + // For 6+ events: arrange into columns where each column has up to 3 stacked cards. + final columnsCount = (n / 3).ceil(); + final columnWidth = 260.0; // narrower so second column peeks in + final verticalCardHeight = 120.0; // each stacked card height matches sample + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + header, + const SizedBox(height: 8), + + // Container height must accommodate 3 stacked cards + small gaps + SizedBox( + height: (verticalCardHeight * 3) + 16, // 3 cards + spacing + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (ctx, colIndex) { + // Build one column: contains up to 3 items: indices colIndex*3 + 0/1/2 + return Container( + width: columnWidth, + child: Column( + children: [ + // top card + if ((colIndex * 3 + 0) < n) + SizedBox( + height: verticalCardHeight, + child: _buildStackedCard(eventsForType[colIndex * 3 + 0]), + ) + else + const SizedBox(height: 0), + const SizedBox(height: 8), + // middle card + if ((colIndex * 3 + 1) < n) + SizedBox( + height: verticalCardHeight, + child: _buildStackedCard(eventsForType[colIndex * 3 + 1]), + ) + else + const SizedBox(height: 0), + const SizedBox(height: 8), + // bottom card + if ((colIndex * 3 + 2) < n) + SizedBox( + height: verticalCardHeight, + child: _buildStackedCard(eventsForType[colIndex * 3 + 2]), + ) + else + const SizedBox(height: 0), + ], + ), + ); + }, + itemCount: columnsCount, + ), + ), + ], + ); + } + + /// A stacked card styled to match your sample (left square thumbnail, bold title). + /// REMOVED: price/rating row (per your request). + Widget _buildStackedCard(EventModel e) { + final theme = Theme.of(context); + String? img; + if (e.thumbImg != null && e.thumbImg!.isNotEmpty) { + img = e.thumbImg; + } else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) { + img = e.images.first.image; + } + + return GestureDetector( + onTap: () { + if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))); + }, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 0), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: theme.shadowColor.withOpacity(0.06), blurRadius: 12, offset: const Offset(0, 8))], + ), + padding: const EdgeInsets.all(12), + child: Row( + children: [ + // thumbnail square (rounded) + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: img != null && img.isNotEmpty + ? Image.network(img, width: 96, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(width: 96, color: theme.dividerColor)) + : Container(width: 96, height: double.infinity, color: theme.dividerColor), + ), + const SizedBox(width: 14), + Expanded( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ + Text(e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 18)), + // removed price/rating row here per request + ]), + ), + // optional heart icon aligned top-right + Icon(Icons.favorite_border, color: theme.hintColor), + ], + ), + ), + ); + } + + /// Compact card used inside the one-row layout for small counts (<=5). + /// Matches Figma: vertical card with image, date badge, title, location, "Free". + Widget _buildHorizontalEventCard(EventModel e) { + final theme = Theme.of(context); + String? img; + if (e.thumbImg != null && e.thumbImg!.isNotEmpty) { + img = e.thumbImg; + } else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) { + img = e.images.first.image; + } + + // Parse day & month for the date badge + String day = ''; + String month = ''; + try { + final parts = e.startDate.split('-'); + if (parts.length == 3) { + day = int.parse(parts[2]).toString(); + const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']; + month = months[int.parse(parts[1]) - 1]; + } + } catch (_) {} + + final venue = e.venueName ?? e.place ?? ''; + + return GestureDetector( + onTap: () { + if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))); + }, + child: SizedBox( + width: 220, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image with date badge + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(18), + child: img != null && img.isNotEmpty + ? Image.network( + img, + width: 220, + height: 180, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + width: 220, + height: 180, + decoration: BoxDecoration( + color: theme.dividerColor, + borderRadius: BorderRadius.circular(18), + ), + child: Icon(Icons.image, size: 40, color: theme.hintColor), + ), + ) + : Container( + width: 220, + height: 180, + decoration: BoxDecoration( + color: theme.dividerColor, + borderRadius: BorderRadius.circular(18), + ), + child: Icon(Icons.image, size: 40, color: theme.hintColor), + ), + ), + // Date badge + if (day.isNotEmpty) + Positioned( + top: 10, + right: 10, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + day, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: Colors.black87, + height: 1.1, + ), + ), + Text( + month, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Colors.black54, + height: 1.2, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 10), + // Title + Text( + e.title ?? e.name ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + if (venue.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + venue, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.hintColor, + fontSize: 13, + ), + ), + ], + const SizedBox(height: 4), + Text( + 'Free', + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + ], + ), + ), + ); + } + + /// Format a date string (YYYY-MM-DD) to short display like "4 Mar". + String _formatDateShort(String dateStr) { + try { + final parts = dateStr.split('-'); + if (parts.length == 3) { + final day = int.parse(parts[2]); + const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + final month = months[int.parse(parts[1]) - 1]; + return '$day $month'; + } + } catch (_) {} + return dateStr; + } + + /// Full width card used when a single type is selected (vertical list). + /// Matches Figma: large image, badge, title, date + venue. + Widget _buildFullWidthCard(EventModel e) { + final theme = Theme.of(context); + String? img; + if (e.thumbImg != null && e.thumbImg!.isNotEmpty) { + img = e.thumbImg; + } else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) { + img = e.images.first.image; + } + + // Build date range string + final startShort = _formatDateShort(e.startDate); + final endShort = _formatDateShort(e.endDate); + final dateRange = startShort == endShort ? startShort : '$startShort - $endShort'; + + final venue = e.venueName ?? e.place ?? ''; + + return GestureDetector( + onTap: () { + if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 18), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow(color: theme.shadowColor.withOpacity(0.10), blurRadius: 16, offset: const Offset(0, 6)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image with badge + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + child: img != null && img.isNotEmpty + ? Image.network( + img, + width: double.infinity, + height: 200, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + width: double.infinity, + height: 200, + decoration: BoxDecoration( + color: theme.dividerColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Icon(Icons.image, size: 48, color: theme.hintColor), + ), + ) + : Container( + width: double.infinity, + height: 200, + decoration: BoxDecoration( + color: theme.dividerColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Icon(Icons.image, size: 48, color: theme.hintColor), + ), + ), + // "ADDED BY EVENTIFY" badge + Positioned( + top: 14, + left: 14, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.star, color: Colors.white, size: 14), + SizedBox(width: 4), + Text( + 'ADDED BY EVENTIFY', + style: TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ], + ), + // Title + date/venue + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + e.title ?? e.name ?? '', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + fontSize: 17, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.calendar_today_outlined, size: 14, color: theme.hintColor), + const SizedBox(width: 4), + Text( + dateRange, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.hintColor, + fontSize: 13, + ), + ), + if (venue.isNotEmpty) ...[ + const SizedBox(width: 12), + Icon(Icons.location_on_outlined, size: 14, color: theme.hintColor), + const SizedBox(width: 3), + Expanded( + child: Text( + venue, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.hintColor, + fontSize: 13, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ], + ), + ], + ), + ), + ], + ), + ), + ); + } + IconData _getIconForType(String typeName) { final name = typeName.toLowerCase(); if (name.contains('music')) return Icons.music_note; @@ -857,85 +1382,6 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } } - Widget _buildEventCard(EventModel e, int index) { - final theme = Theme.of(context); - String? img; - if (e.thumbImg != null && e.thumbImg!.isNotEmpty) { - img = e.thumbImg; - } else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) { - img = e.images.first.image; - } - - return GestureDetector( - onTap: () { - if (e.id != null) { - Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))); - } - }, - child: Container( - margin: const EdgeInsets.only(bottom: 18), - decoration: BoxDecoration( - color: theme.cardColor, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow(color: theme.shadowColor.withOpacity(0.12), blurRadius: 18, offset: const Offset(0, 8)), - BoxShadow(color: theme.shadowColor.withOpacity(0.04), blurRadius: 6, offset: const Offset(0, 2)), - ], - ), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), - child: img != null && img.isNotEmpty - ? Image.network(img, fit: BoxFit.cover, width: double.infinity, height: 160) - : Image.asset('assets/images/event1.jpg', fit: BoxFit.cover, width: double.infinity, height: 160), - ), - Padding( - padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - e.title ?? e.name ?? '', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 8), - Row( - children: [ - Icon(Icons.calendar_today, size: 14, color: theme.colorScheme.primary), - const SizedBox(width: 6), - Flexible( - flex: 2, - child: Text( - '${e.startDate ?? ''}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withOpacity(0.9), fontSize: 13), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text('•', style: TextStyle(color: theme.textTheme.bodySmall?.color?.withOpacity(0.4))), - ), - Icon(Icons.location_on, size: 14, color: theme.colorScheme.primary), - const SizedBox(width: 6), - Flexible( - flex: 3, - child: Text( - e.place ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withOpacity(0.9), fontSize: 13), - ), - ), - ], - ), - ]), - ) - ]), - ), - ); - } - String _getShortEmailLabel() { try { final parts = _username.split('@'); diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart index a8835ad..13ef221 100644 --- a/lib/screens/learn_more_screen.dart +++ b/lib/screens/learn_more_screen.dart @@ -1,9 +1,13 @@ // lib/screens/learn_more_screen.dart - +import 'dart:async'; +import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; + import '../features/events/models/event_models.dart'; import '../features/events/services/events_service.dart'; -import 'booking_screen.dart'; class LearnMoreScreen extends StatefulWidget { final int eventId; @@ -20,22 +24,50 @@ class _LearnMoreScreenState extends State { EventModel? _event; String? _error; + // Carousel + final PageController _pageController = PageController(); + int _currentPage = 0; + Timer? _autoScrollTimer; + + // About section + bool _aboutExpanded = false; + + // Wishlist (UI-only) + bool _wishlisted = false; + + // Google Map + GoogleMapController? _mapController; + MapType _mapType = MapType.normal; + bool _showMapControls = false; + @override void initState() { super.initState(); _loadEvent(); } + @override + void dispose() { + _autoScrollTimer?.cancel(); + _pageController.dispose(); + _mapController?.dispose(); + super.dispose(); + } + + // --------------------------------------------------------------------------- + // Data loading + // --------------------------------------------------------------------------- + Future _loadEvent() async { setState(() { _loading = true; _error = null; }); - try { final ev = await _service.getEventDetails(widget.eventId); if (!mounted) return; setState(() => _event = ev); + _startAutoScroll(); } catch (e) { if (!mounted) return; setState(() => _error = e.toString()); @@ -44,131 +76,1030 @@ class _LearnMoreScreenState extends State { } } - Widget _buildImageCarousel() { - final imgs = _event?.images ?? []; - final thumb = _event?.thumbImg; + // --------------------------------------------------------------------------- + // Carousel helpers + // --------------------------------------------------------------------------- + + List get _imageUrls { final list = []; - + if (_event == null) return list; + final thumb = _event!.thumbImg; if (thumb != null && thumb.isNotEmpty) list.add(thumb); - for (final i in imgs) { - if (i.image.isNotEmpty && !list.contains(i.image)) list.add(i.image); + for (final img in _event!.images) { + if (img.image.isNotEmpty && !list.contains(img.image)) list.add(img.image); } - - if (list.isEmpty) { - return Container( - height: 220, - color: Colors.grey.shade200, - child: const Center(child: Icon(Icons.event, size: 80, color: Colors.grey)), - ); - } - - return SizedBox( - height: 220, - child: PageView.builder( - itemCount: list.length, - itemBuilder: (context, i) => Image.network(list[i], fit: BoxFit.cover, width: double.infinity), - ), - ); + return list; } + void _startAutoScroll() { + _autoScrollTimer?.cancel(); + final count = _imageUrls.length; + if (count <= 1) return; + _autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (_) { + if (!_pageController.hasClients) return; + final next = (_currentPage + 1) % count; + _pageController.animateToPage(next, + duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); + }); + } + + // --------------------------------------------------------------------------- + // Date formatting + // --------------------------------------------------------------------------- + + String _formattedDateRange() { + if (_event == null) return ''; + try { + final s = DateTime.parse(_event!.startDate); + final e = DateTime.parse(_event!.endDate); + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + if (s.year == e.year && s.month == e.month && s.day == e.day) { + return '${s.day} ${months[s.month - 1]}'; + } + if (s.month == e.month && s.year == e.year) { + return '${s.day} - ${e.day} ${months[s.month - 1]}'; + } + return '${s.day} ${months[s.month - 1]} - ${e.day} ${months[e.month - 1]}'; + } catch (_) { + return '${_event!.startDate} – ${_event!.endDate}'; + } + } + + // --------------------------------------------------------------------------- + // Actions + // --------------------------------------------------------------------------- + + Future _shareEvent() async { + final title = _event?.title ?? _event?.name ?? 'Check out this event'; + final url = + 'https://uat.eventifyplus.com/events/${widget.eventId}'; + await Share.share('$title\n$url', subject: title); + } + + Future _openUrl(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } + + void _viewLargerMap() { + if (_event?.latitude == null || _event?.longitude == null) return; + _openUrl( + 'https://www.google.com/maps/search/?api=1&query=${_event!.latitude},${_event!.longitude}'); + } + + void _getDirections() { + if (_event?.latitude == null || _event?.longitude == null) return; + _openUrl( + 'https://www.google.com/maps/dir/?api=1&destination=${_event!.latitude},${_event!.longitude}'); + } + + // --------------------------------------------------------------------------- + // Map camera helpers + // --------------------------------------------------------------------------- + + void _moveCamera(double latDelta, double lngDelta) { + _mapController?.animateCamera(CameraUpdate.scrollBy(lngDelta * 80, -latDelta * 80)); + } + + void _zoom(double amount) { + _mapController?.animateCamera(CameraUpdate.zoomBy(amount)); + } + + // --------------------------------------------------------------------------- + // BUILD + // --------------------------------------------------------------------------- + @override Widget build(BuildContext context) { final theme = Theme.of(context); + if (_loading) { + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + body: _buildLoadingShimmer(theme), + ); + } + + if (_error != null) { + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + body: Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, size: 56, color: theme.colorScheme.error), + const SizedBox(height: 16), + Text('Something went wrong', + style: theme.textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Text(_error!, textAlign: TextAlign.center, style: theme.textTheme.bodyMedium), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: _loadEvent, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + ), + ], + ), + ), + ), + ); + } + + if (_event == null) { + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + body: const Center(child: Text('Event not found')), + ); + } + + final screenHeight = MediaQuery.of(context).size.height; + final imageHeight = screenHeight * 0.50; + final overlap = 30.0; + return Scaffold( - appBar: AppBar( - title: const Text('Event Details'), - ), - body: _loading - ? const Center(child: CircularProgressIndicator()) - : _error != null - ? Center(child: Text('Error: $_error')) - : _event == null - ? const Center(child: Text('Event not found')) - : SingleChildScrollView( - child: DefaultTextStyle.merge( - // force child Text widgets to use theme-aware foreground color (works in light/dark) - style: TextStyle(color: theme.colorScheme.onSurface, height: 1.45), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildImageCarousel(), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Title — use theme typography - Text( - _event!.title ?? _event!.name ?? '', - style: theme.textTheme.headlineSmall?.copyWith(fontSize: 22, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), + backgroundColor: theme.scaffoldBackgroundColor, + body: Stack( + children: [ + // ── LAYER 1: Image carousel (background) ── + _buildImageCarousel(theme, imageHeight), - // Meta row (date, location) — icons will use theme icon color - Row( - children: [ - Icon(Icons.calendar_today, size: 16, color: theme.iconTheme.color), - const SizedBox(width: 6), - Text( - '${_event!.startDate}${_event!.startTime != null ? ' • ${_event!.startTime}' : ''}', - style: theme.textTheme.bodyMedium, - ), - const SizedBox(width: 12), - Icon(Icons.location_on, size: 16, color: theme.iconTheme.color), - const SizedBox(width: 6), - Flexible(child: Text(_event!.place ?? _event!.venueName ?? '', style: theme.textTheme.bodyMedium)), - ], - ), - const SizedBox(height: 12), + // ── LAYER 2: Scrollable content with overlapping white card ── + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Transparent spacer — shows the image behind + SizedBox(height: imageHeight - overlap), - // Description — themed body text (no hardcoded black) - Text( - _event!.description ?? '', - style: theme.textTheme.bodyMedium, - ), - const SizedBox(height: 16), + // White card with rounded top corners overlapping image + Container( + width: double.infinity, + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(28), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, -6), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitleSection(theme), + _buildAboutSection(theme), + if (_event!.latitude != null && _event!.longitude != null) ...[ + _buildVenueSection(theme), + _buildGetDirectionsButton(theme), + ], + if (_event!.importantInfo.isNotEmpty) + _buildImportantInfoSection(theme), + if (_event!.importantInfo.isEmpty && + (_event!.importantInformation ?? '').isNotEmpty) + _buildImportantInfoFallback(theme), + const SizedBox(height: 100), + ], + ), + ), + ], + ), + ), - // Important section (if present) - if ((_event!.importantInformation ?? '').isNotEmpty) ...[ - Text('Important', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), - const SizedBox(height: 6), - Text(_event!.importantInformation!, style: theme.textTheme.bodyMedium), - const SizedBox(height: 12), - ], - - // Book button - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => BookingScreen( - onBook: () { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Booked (demo)'))); - }, - image: _event!.thumbImg ?? '', - ), - ), - ); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), - child: Text('Book Tickets'), - ), - ), - ], - ), - ], + // ── LAYER 3: Floating icon row (above scrollview so taps work) ── + Positioned( + top: MediaQuery.of(context).padding.top + 10, + left: 16, + right: 16, + child: Row( + children: [ + _squareIconButton( + icon: Icons.arrow_back, + onTap: () => Navigator.pop(context), + ), + // Pill-shaped page indicators (centered) + Expanded( + child: _imageUrls.length > 1 + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_imageUrls.length, (i) { + final active = i == _currentPage; + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(horizontal: 3), + width: active ? 18 : 8, + height: 6, + decoration: BoxDecoration( + color: active + ? Colors.white + : Colors.white.withOpacity(0.45), + borderRadius: BorderRadius.circular(3), ), + ); + }), + ) + : const SizedBox.shrink(), + ), + _squareIconButton( + icon: Icons.ios_share_outlined, + onTap: _shareEvent, + ), + const SizedBox(width: 10), + _squareIconButton( + icon: _wishlisted ? Icons.favorite : Icons.favorite_border, + iconColor: _wishlisted ? Colors.redAccent : Colors.white, + onTap: () => setState(() => _wishlisted = !_wishlisted), + ), + ], + ), + ), + ], + ), + ); + } + + // --------------------------------------------------------------------------- + // 1. LOADING SHIMMER + // --------------------------------------------------------------------------- + + Widget _buildLoadingShimmer(ThemeData theme) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Placeholder image + Container( + height: MediaQuery.of(context).size.height * 0.42, + decoration: BoxDecoration( + color: theme.dividerColor.withOpacity(0.3), + borderRadius: BorderRadius.circular(28), + ), + ), + const SizedBox(height: 24), + // Placeholder title + Container( + height: 28, + width: 220, + decoration: BoxDecoration( + color: theme.dividerColor.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(height: 12), + Container( + height: 16, + width: 140, + decoration: BoxDecoration( + color: theme.dividerColor.withOpacity(0.3), + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(height: 20), + Container( + height: 16, + width: double.infinity, + decoration: BoxDecoration( + color: theme.dividerColor.withOpacity(0.3), + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(height: 8), + Container( + height: 16, + width: double.infinity, + decoration: BoxDecoration( + color: theme.dividerColor.withOpacity(0.3), + borderRadius: BorderRadius.circular(6), + ), + ), + ], + ), + ), + ); + } + + // --------------------------------------------------------------------------- + // 2. IMAGE CAROUSEL WITH BLURRED BACKGROUND + // --------------------------------------------------------------------------- + + Widget _buildImageCarousel(ThemeData theme, double carouselHeight) { + final images = _imageUrls; + final topPad = MediaQuery.of(context).padding.top; + + return SizedBox( + height: carouselHeight, + child: Stack( + children: [ + // ---- Blurred background (image or blue gradient) ---- + Positioned.fill( + child: images.isNotEmpty + ? ClipRect( + child: Stack( + fit: StackFit.expand, + children: [ + Image.network( + images[_currentPage], + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)], + ), + ), + ), + ), + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25), + child: Container( + color: Colors.black.withOpacity(0.15), + ), + ), + ], + ), + ) + : Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)], + ), + ), + ), + ), + + // ---- Foreground image with rounded corners ---- + if (images.isNotEmpty) + Positioned( + top: topPad + 56, // below the icon row + left: 20, + right: 20, + bottom: 16, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: PageView.builder( + controller: _pageController, + onPageChanged: (i) => setState(() => _currentPage = i), + itemCount: images.length, + itemBuilder: (_, i) => Image.network( + images[i], + fit: BoxFit.cover, + width: double.infinity, + errorBuilder: (_, __, ___) => Container( + color: theme.dividerColor, + child: Icon(Icons.broken_image, size: 48, color: theme.hintColor), + ), + ), + ), + ), + ), + + // ---- No-image placeholder ---- + if (images.isEmpty) + Positioned( + top: topPad + 56, + left: 20, + right: 20, + bottom: 16, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + ), + child: const Center( + child: Icon(Icons.event, size: 80, color: Colors.white70), + ), + ), + ), + ], + ), + ); + } + + /// Square icon button with rounded corners and translucent white background + Widget _squareIconButton({ + required IconData icon, + required VoidCallback onTap, + Color iconColor = Colors.white, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.3)), + ), + child: Icon(icon, color: iconColor, size: 22), + ), + ); + } + + // --------------------------------------------------------------------------- + // 3. TITLE & DATE + // --------------------------------------------------------------------------- + + Widget _buildTitleSection(ThemeData theme) { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _event!.title ?? _event!.name, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + fontSize: 26, + height: 1.25, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.calendar_today_outlined, + size: 16, color: theme.hintColor), + const SizedBox(width: 6), + Text( + _formattedDateRange(), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.hintColor, + fontSize: 15, + ), + ), + ], + ), + ], + ), + ); + } + + // --------------------------------------------------------------------------- + // 4. ABOUT THE EVENT + // --------------------------------------------------------------------------- + + Widget _buildAboutSection(ThemeData theme) { + final desc = _event!.description ?? ''; + if (desc.isEmpty) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'About the Event', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + fontSize: 20, + ), + ), + const SizedBox(height: 10), + AnimatedCrossFade( + firstChild: Text( + desc, + maxLines: 4, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyMedium?.copyWith( + height: 1.55, + color: theme.textTheme.bodyMedium?.color?.withOpacity(0.75), + ), + ), + secondChild: Text( + desc, + style: theme.textTheme.bodyMedium?.copyWith( + height: 1.55, + color: theme.textTheme.bodyMedium?.color?.withOpacity(0.75), + ), + ), + crossFadeState: + _aboutExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 300), + ), + const SizedBox(height: 6), + GestureDetector( + onTap: () => setState(() => _aboutExpanded = !_aboutExpanded), + child: Text( + _aboutExpanded ? 'Read Less' : 'Read More', + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w700, + fontSize: 15, + ), + ), + ), + ], + ), + ); + } + + // --------------------------------------------------------------------------- + // 5. VENUE LOCATION (Google Map) + // --------------------------------------------------------------------------- + + Widget _buildVenueSection(ThemeData theme) { + final lat = _event!.latitude!; + final lng = _event!.longitude!; + final venueLabel = _event!.locationName ?? _event!.venueName ?? _event!.place ?? ''; + + return Padding( + padding: const EdgeInsets.fromLTRB(20, 28, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Venue Location', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + fontSize: 20, + ), + ), + const SizedBox(height: 14), + + // Map container + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SizedBox( + height: 280, + child: Stack( + children: [ + GoogleMap( + initialCameraPosition: CameraPosition( + target: LatLng(lat, lng), + zoom: 15, + ), + mapType: _mapType, + markers: { + Marker( + markerId: const MarkerId('event'), + position: LatLng(lat, lng), + infoWindow: InfoWindow(title: venueLabel), + ), + }, + myLocationButtonEnabled: false, + zoomControlsEnabled: false, + scrollGesturesEnabled: true, + rotateGesturesEnabled: false, + tiltGesturesEnabled: false, + onMapCreated: (c) => _mapController = c, + ), + + // "View larger map" – top left + Positioned( + top: 10, + left: 10, + child: GestureDetector( + onTap: _viewLargerMap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 6, + ), + ], + ), + child: Text( + 'View larger map', + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ), + ), + ), + + // Map type toggle – bottom left + Positioned( + bottom: 12, + left: 12, + child: _mapControlButton( + icon: _mapType == MapType.normal + ? Icons.satellite_alt + : Icons.map_outlined, + onTap: () { + setState(() { + _mapType = _mapType == MapType.normal + ? MapType.satellite + : MapType.normal; + }); + }, + ), + ), + + // Map controls toggle – bottom right + Positioned( + bottom: 12, + right: 12, + child: _mapControlButton( + icon: Icons.open_with_rounded, + onTap: () => setState(() => _showMapControls = !_showMapControls), + ), + ), + + // Directional pad overlay + if (_showMapControls) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.25), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Top row: Up + Zoom In + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _mapControlButton( + icon: Icons.keyboard_arrow_up, + onTap: () => _moveCamera(1, 0)), + const SizedBox(width: 16), + _mapControlButton( + icon: Icons.add, + onTap: () => _zoom(1)), + ], + ), + const SizedBox(height: 10), + // Middle row: Left + Right + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _mapControlButton( + icon: Icons.keyboard_arrow_left, + onTap: () => _moveCamera(0, -1)), + const SizedBox(width: 60), + _mapControlButton( + icon: Icons.keyboard_arrow_right, + onTap: () => _moveCamera(0, 1)), + ], + ), + const SizedBox(height: 10), + // Bottom row: Down + Zoom Out + Close + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _mapControlButton( + icon: Icons.keyboard_arrow_down, + onTap: () => _moveCamera(-1, 0)), + const SizedBox(width: 16), + _mapControlButton( + icon: Icons.remove, + onTap: () => _zoom(-1)), + const SizedBox(width: 16), + _mapControlButton( + icon: Icons.close, + onTap: () => + setState(() => _showMapControls = false)), + ], ), ], ), ), ), + ], + ), + ), + ), + + // Venue name card + if (venueLabel.isNotEmpty) + Container( + width: double.infinity, + margin: const EdgeInsets.only(top: 14), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withOpacity(0.06), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + venueLabel, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + if (_event!.place != null && _event!.place != venueLabel) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + _event!.place!, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.hintColor, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _mapControlButton({ + required IconData icon, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.92), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 6, + ), + ], + ), + child: Icon(icon, color: Colors.grey.shade700, size: 22), + ), + ); + } + + // --------------------------------------------------------------------------- + // 6. GET DIRECTIONS BUTTON + // --------------------------------------------------------------------------- + + Widget _buildGetDirectionsButton(ThemeData theme) { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 0), + child: SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton.icon( + onPressed: _getDirections, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 2, + ), + icon: const Icon(Icons.directions, size: 22), + label: const Text( + 'Get Directions', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700), + ), + ), + ), + ); + } + + // --------------------------------------------------------------------------- + // 7. IMPORTANT INFORMATION (structured list) + // --------------------------------------------------------------------------- + + Widget _buildImportantInfoSection(ThemeData theme) { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 28, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Important Information', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + fontSize: 20, + ), + ), + const SizedBox(height: 14), + for (final info in _event!.importantInfo) + Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: theme.colorScheme.primary.withOpacity(0.12), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.info_outline, + size: 20, color: theme.colorScheme.primary), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + info['title'] ?? '', + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + info['value'] ?? '', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.hintColor, + height: 1.4, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + // --------------------------------------------------------------------------- + // 7b. IMPORTANT INFO FALLBACK (parse HTML string into cards) + // --------------------------------------------------------------------------- + + /// Strip HTML tags and decode common HTML entities + String _stripHtml(String html) { + // Remove all HTML tags + var text = html.replaceAll(RegExp(r'<[^>]*>'), ''); + // Decode common HTML entities + text = text + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(' ', ' '); + return text.trim(); + } + + /// Parse an HTML important_information string into a list of {title, value} maps + List> _parseHtmlImportantInfo(String raw) { + // Strip HTML tags, preserving
as a newline separator first + var text = raw + .replaceAll(RegExp(r'', caseSensitive: false), '\n') + .replaceAll(RegExp(r'<[^>]*>'), ''); + // Decode entities + text = text + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(' ', ' '); + + // Split by newlines first + var lines = text + .split('\n') + .map((l) => l.trim()) + .where((l) => l.isNotEmpty) + .toList(); + + // If we only have 1 line, items might be separated by emoji characters + // (some categories don't use
between items, e.g. "...etc.🚌 Bus:") + if (lines.length <= 1 && text.trim().isNotEmpty) { + final parts = text.trim().split( + RegExp(r'(?=[\u2600-\u27BF]|[\u{1F300}-\u{1FFFF}])', unicode: true), + ); + final emojiLines = parts + .map((l) => l.trim()) + .where((l) => l.isNotEmpty) + .toList(); + if (emojiLines.length > 1) { + lines = emojiLines; + } + } + + final items = >[]; + for (final line in lines) { + // Split on first colon to get title:value + final colonIdx = line.indexOf(':'); + if (colonIdx > 0 && colonIdx < line.length - 1) { + items.add({ + 'title': line.substring(0, colonIdx + 1).trim(), + 'value': line.substring(colonIdx + 1).trim(), + }); + } else { + items.add({'title': line, 'value': ''}); + } + } + return items; + } + + Widget _buildImportantInfoFallback(ThemeData theme) { + final parsed = _parseHtmlImportantInfo(_event!.importantInformation!); + + if (parsed.isEmpty) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.fromLTRB(20, 28, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Important Information', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + fontSize: 20, + ), + ), + const SizedBox(height: 14), + for (final info in parsed) + Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: theme.colorScheme.primary.withOpacity(0.12), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.info_outline, + size: 20, color: theme.colorScheme.primary), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + info['title'] ?? '', + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + if ((info['value'] ?? '').isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + info['value']!, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.hintColor, + height: 1.4, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ], + ), ); } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 64a0ece..7299b5c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2db3c22..786ff5c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 90d7d0f..bb0698f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,10 +7,16 @@ import Foundation import file_selector_macos import geolocator_apple +import path_provider_foundation +import share_plus import shared_preferences_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 27e303b..1f9710c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -206,6 +206,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.30" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -296,6 +304,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468" + url: "https://pub.dev" + source: hosted + version: "8.2.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + sha256: "9b0d6dab3de6955837575dc371dd772fcb5d0a90f6a4954e8c066472f9938550" + url: "https://pub.dev" + source: hosted + version: "2.14.2" + google_maps_flutter_android: + dependency: transitive + description: + name: google_maps_flutter_android + sha256: "98d7f5354f770f3e993db09fc798d40aeb6a254f04c1c468a94818ec2086e83e" + url: "https://pub.dev" + source: hosted + version: "2.18.12" + google_maps_flutter_ios: + dependency: transitive + description: + name: google_maps_flutter_ios + sha256: "38f0a9ee858b0de3a5105e7efe200f154eea8397eb0c36bea6b3810429fbc0e4" + url: "https://pub.dev" + source: hosted + version: "2.17.3" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + sha256: e8b1232419fcdd35c1fdafff96843f5a40238480365599d8ca661dde96d283dd + url: "https://pub.dev" + source: hosted + version: "2.14.1" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: d416602944e1859f3cbbaa53e34785c223fa0a11eddb34a913c964c5cbb5d8cf + url: "https://pub.dev" + source: hosted + version: "0.5.14+3" html: dependency: transitive description: @@ -472,6 +528,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -528,6 +616,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + url: "https://pub.dev" + source: hosted + version: "7.2.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" + url: "https://pub.dev" + source: hosted + version: "3.4.0" shared_preferences: dependency: "direct main" description: @@ -621,6 +733,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -669,6 +789,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" uuid: dependency: transitive description: @@ -677,6 +861,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b" + url: "https://pub.dev" + source: hosted + version: "1.1.20" vector_math: dependency: transitive description: @@ -701,6 +909,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a4b52f8..e2199ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,10 @@ dependencies: table_calendar: ^3.2.0 geolocator: ^9.0.2 geocoding: ^2.0.5 + flutter_svg: ^2.0.9 + google_maps_flutter: ^2.5.0 + url_launcher: ^6.2.1 + share_plus: ^7.2.1 dev_dependencies: flutter_test: @@ -26,6 +30,7 @@ flutter: uses-material-design: true assets: - assets/images/ + - assets/icon/hand_stop.svg diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f35b3a6..58f82d3 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,16 @@ #include #include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 389222b..8f2921f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,8 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows geolocator_windows + share_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST