From 4beacfde0343a900c48f7a4945b09a2c83235580 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 5 Sep 2026 04:08:53 -0400 Subject: [PATCH] feat: unify group rides and social experience --- .../group_rides/create_group_ride_screen.dart | 1091 ++++++++++------- .../group_rides/group_ride_detail_screen.dart | 969 +++++++++------ .../group_rides/group_rides_hub_screen.dart | 177 ++- .../group_rides/live_group_ride_screen.dart | 385 +++--- lib/features/social/friends_screen.dart | 333 +++-- lib/features/social/rider_profile_screen.dart | 700 +++++++---- lib/features/social/widgets/avatar_badge.dart | 2 +- ...al_group_rides_social_responsive_test.dart | 539 ++++++++ 8 files changed, 2779 insertions(+), 1417 deletions(-) create mode 100644 test/canonical_group_rides_social_responsive_test.dart diff --git a/lib/features/group_rides/create_group_ride_screen.dart b/lib/features/group_rides/create_group_ride_screen.dart index 7576964..783ffed 100644 --- a/lib/features/group_rides/create_group_ride_screen.dart +++ b/lib/features/group_rides/create_group_ride_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/responsive/viewport_size.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../services/group_rides/group_ride_models.dart'; import '../../services/group_rides/group_ride_providers.dart'; import '../../services/social/social_providers.dart'; @@ -158,7 +161,8 @@ class _CreateGroupRideScreenState extends ConsumerState { if (_titleController.text.trim().isEmpty) { _titleController.text = route.title; } - if (_descriptionController.text.trim().isEmpty && route.description.isNotEmpty) { + if (_descriptionController.text.trim().isEmpty && + route.description.isNotEmpty) { _descriptionController.text = route.description; } @@ -201,13 +205,13 @@ class _CreateGroupRideScreenState extends ConsumerState { }); } - void _showSelectRouteSheet() { + void _showSelectRouteSheet(MotorSignalThemeColors colors) { final user = ref.read(currentUserProvider); if (user == null) return; showModalBottomSheet( context: context, - backgroundColor: const Color(0xFF13151A), + backgroundColor: colors.surfaceContainerHigh, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), @@ -237,15 +241,16 @@ class _CreateGroupRideScreenState extends ConsumerState { ), const SizedBox(height: 12), routesAsync.when( - loading: () => const Center( + loading: () => Center( child: Padding( - padding: EdgeInsets.all(24), - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + padding: const EdgeInsets.all(24), + child: + CircularProgressIndicator(color: colors.cyberCyan), ), ), error: (err, _) => Text( 'Failed to load routes: $err', - style: const TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), data: (routes) { if (routes.isEmpty) { @@ -269,14 +274,18 @@ class _CreateGroupRideScreenState extends ConsumerState { itemBuilder: (ctx, idx) { final route = routes[idx]; return ListTile( - leading: const Icon(Icons.alt_route, color: Color(0xFF00E5FF)), + leading: Icon(Icons.alt_route, + color: colors.cyberCyan), title: Text( route.title, - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold), ), subtitle: Text( '${route.stops.length} stops • ${route.formattedDistanceMiles}', - style: const TextStyle(color: Colors.white54, fontSize: 12), + style: const TextStyle( + color: Colors.white54, fontSize: 12), ), onTap: () { Navigator.pop(sheetCtx); @@ -298,7 +307,7 @@ class _CreateGroupRideScreenState extends ConsumerState { ); } - void _addStopDialog() { + void _addStopDialog(MotorSignalThemeColors colors) { final nameCtrl = TextEditingController(); final notesCtrl = TextEditingController(); var stopType = GroupRideLocationType.fuel; @@ -307,7 +316,11 @@ class _CreateGroupRideScreenState extends ConsumerState { context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setDialogState) => AlertDialog( - backgroundColor: const Color(0xFF1B1E28), + backgroundColor: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: colors.outlineVariant), + ), title: const Text('Add Intermediate Stop', style: TextStyle(color: Colors.white, fontSize: 16)), content: Column( @@ -316,17 +329,29 @@ class _CreateGroupRideScreenState extends ConsumerState { TextField( controller: nameCtrl, style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Stop Name (e.g. Shell Fuel / Overlook)', - labelStyle: TextStyle(color: Colors.white70), + labelStyle: const TextStyle(color: Colors.white70), + filled: true, + fillColor: colors.surfaceContainer, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), ), ), const SizedBox(height: 10), DropdownButtonFormField( initialValue: stopType, - dropdownColor: const Color(0xFF1B1E28), + dropdownColor: colors.surfaceContainerHigh, style: const TextStyle(color: Colors.white), - decoration: const InputDecoration(labelText: 'Stop Type'), + decoration: InputDecoration( + labelText: 'Stop Type', + filled: true, + fillColor: colors.surfaceContainer, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), items: [ GroupRideLocationType.fuel, GroupRideLocationType.rest, @@ -347,9 +372,14 @@ class _CreateGroupRideScreenState extends ConsumerState { TextField( controller: notesCtrl, style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Notes (optional)', - labelStyle: TextStyle(color: Colors.white70), + labelStyle: const TextStyle(color: Colors.white70), + filled: true, + fillColor: colors.surfaceContainer, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), ), ), ], @@ -362,7 +392,7 @@ class _CreateGroupRideScreenState extends ConsumerState { ), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), onPressed: () { @@ -389,10 +419,13 @@ class _CreateGroupRideScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final colors = context.motorColors; + return Scaffold( - backgroundColor: const Color(0xFF0D0E11), + backgroundColor: colors.surfaceContainer, appBar: AppBar( - backgroundColor: const Color(0xFF13151A), + backgroundColor: colors.surfaceContainerHigh, + elevation: 0, title: const Text( 'CREATE GROUP RIDE', style: TextStyle( @@ -403,455 +436,92 @@ class _CreateGroupRideScreenState extends ConsumerState { ), actions: [ IconButton( - icon: const Icon(Icons.alt_route, color: Color(0xFF00E5FF)), + icon: Icon(Icons.alt_route, color: colors.cyberCyan), tooltip: 'Import Route', - onPressed: _showSelectRouteSheet, + onPressed: () => _showSelectRouteSheet(colors), ), TextButton( onPressed: _isSaving ? null : _submit, child: _isSaving - ? const SizedBox( + ? SizedBox( width: 16, height: 16, - child: CircularProgressIndicator(strokeWidth: 2), + child: CircularProgressIndicator( + strokeWidth: 2, + color: colors.cyberCyan, + ), ) - : const Text( + : Text( 'PUBLISH', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontWeight: FontWeight.bold, ), ), ), ], ), - body: Form( - key: _formKey, - child: ListView( - padding: const EdgeInsets.all(20), + body: ResponsiveLayout( + builder: (context, snapshot) { + final isWide = snapshot.sizeClass == WindowSizeClass.expanded || + (snapshot.isLandscape && snapshot.width >= 720); + + if (isWide) { + return _buildWideForm(context, colors, snapshot); + } + return _buildCompactForm(context, colors, snapshot); + }, + ), + ); + } + + Widget _buildWideForm( + BuildContext context, + MotorSignalThemeColors colors, + ResponsiveSnapshot snapshot, + ) { + return Form( + key: _formKey, + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (_errorMessage != null) - Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.redAccent.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.redAccent), - ), - child: Row( - children: [ - const Icon(Icons.error_outline, - color: Colors.redAccent, size: 20), - const SizedBox(width: 10), - Expanded( - child: Text( - _errorMessage!, - style: const TextStyle( - color: Colors.redAccent, fontSize: 12), - ), - ), - ], - ), - ), - - _buildSectionHeader('1. RIDE DETAILS'), - const SizedBox(height: 10), - TextFormField( - controller: _titleController, - style: const TextStyle(color: Colors.white), - decoration: _inputDecoration('Ride Title', 'e.g. Blood Mountain Loop'), - validator: (v) { - if (v == null || v.trim().length < 3) { - return 'Title must be at least 3 characters'; - } - return null; - }, - ), - const SizedBox(height: 12), - TextFormField( - controller: _descriptionController, - style: const TextStyle(color: Colors.white), - maxLines: 3, - decoration: _inputDecoration( - 'Description & Route Notes', - 'Pace, skill level, gear recommendations...', - ), - ), - - const SizedBox(height: 24), - _buildSectionHeader('2. DATE & SCHEDULE'), - const SizedBox(height: 10), - Card( - color: const Color(0xFF161922), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - leading: - const Icon(Icons.calendar_month, color: Color(0xFF00E5FF)), - title: const Text('Date', - style: TextStyle(color: Colors.white70, fontSize: 13)), - subtitle: Text( - '${_scheduledDate.month}/${_scheduledDate.day}/${_scheduledDate.year}', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - trailing: TextButton( - onPressed: () async { - final picked = await showDatePicker( - context: context, - initialDate: _scheduledDate, - firstDate: DateTime.now(), - lastDate: - DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null) { - setState(() => _scheduledDate = picked); - } - }, - child: const Text('CHANGE'), - ), - ), - const Divider(color: Colors.white12), - Row( - children: [ - Expanded( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Meetup Arrival', - style: TextStyle( - color: Colors.white70, fontSize: 12)), - subtitle: Text( - _meetupTime.format(context), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - onTap: () async { - final picked = await showTimePicker( - context: context, - initialTime: _meetupTime, - ); - if (picked != null) { - setState(() => _meetupTime = picked); - } - }, - ), - ), - Expanded( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Kickstands Up (KSU)', - style: TextStyle( - color: Colors.white70, fontSize: 12)), - subtitle: Text( - _departureTime.format(context), - style: const TextStyle( - color: Color(0xFF00E5FF), - fontWeight: FontWeight.bold, - ), - ), - onTap: () async { - final picked = await showTimePicker( - context: context, - initialTime: _departureTime, - ); - if (picked != null) { - setState(() => _departureTime = picked); - } - }, - ), - ), - ], - ), - ], - ), - ), - ), - - const SizedBox(height: 24), - _buildSectionHeader('3. MEETING LOCATION'), - const SizedBox(height: 10), - TextFormField( - controller: _meetupNameController, - style: const TextStyle(color: Colors.white), - decoration: _inputDecoration( - 'Meeting Place Name', - 'e.g. Dahlonega Square / Shell Station', - ), - validator: (v) { - if (v == null || v.trim().isEmpty) { - return 'Please provide a meeting location name'; - } - return null; - }, - ), - const SizedBox(height: 12), - TextFormField( - controller: _meetupAddressController, - style: const TextStyle(color: Colors.white), - decoration: _inputDecoration( - 'Address or Landmark', - 'e.g. 123 Main St, Dahlonega, GA', - ), - ), - const SizedBox(height: 12), - TextFormField( - controller: _meetupNotesController, - style: const TextStyle(color: Colors.white), - decoration: _inputDecoration( - 'Meeting Notes (optional)', - 'e.g. Park on the north side near pumps', - ), - ), - - const SizedBox(height: 24), - _buildSectionHeader('4. DESTINATION & STOPS'), - const SizedBox(height: 10), - SwitchListTile( - contentPadding: EdgeInsets.zero, - activeThumbColor: const Color(0xFF00E5FF), - title: const Text('Specific Destination', - style: TextStyle(color: Colors.white)), - value: _hasDestination, - onChanged: (val) => setState(() => _hasDestination = val), - ), - if (_hasDestination) ...[ - const SizedBox(height: 8), - TextFormField( - controller: _destNameController, - style: const TextStyle(color: Colors.white), - decoration: _inputDecoration( - 'Destination Name', - 'e.g. Brasstown Bald Summit', - ), - ), - const SizedBox(height: 12), - TextFormField( - controller: _destAddressController, - style: const TextStyle(color: Colors.white), - decoration: _inputDecoration( - 'Destination Address (optional)', - 'e.g. Hiawassee, GA', - ), - ), - ], - - const SizedBox(height: 16), - // Route Pre-fill / Attachment - Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFF161922), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF2A2E3D)), - ), - child: Row( - children: [ - const Icon(Icons.alt_route, color: Color(0xFF00E5FF), size: 22), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _selectedRouteTitle != null - ? 'Route: $_selectedRouteTitle' - : 'Plan with Saved Route', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 13, - ), - overflow: TextOverflow.ellipsis, - ), - Text( - _selectedRouteTitle != null - ? '$_selectedRouteStopCount stops • ${(_selectedRouteDistanceMeters != null ? (_selectedRouteDistanceMeters! / 1609.34).toStringAsFixed(1) : 0)} mi' - : 'Optional: import stops & coordinates', - style: const TextStyle(color: Colors.white54, fontSize: 11), - ), - ], - ), - ), - if (_selectedRouteTitle != null) - IconButton( - icon: const Icon(Icons.close, color: Colors.white54, size: 18), - tooltip: 'Clear route', - onPressed: () { - setState(() { - _selectedRouteId = null; - _selectedRouteTitle = null; - _selectedRouteDistanceMeters = null; - _selectedRouteStopCount = null; - }); - }, - ) - else - OutlinedButton( - onPressed: _showSelectRouteSheet, - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Color(0xFF00E5FF)), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: const Text( - 'IMPORT', - style: TextStyle( - color: Color(0xFF00E5FF), - fontSize: 11, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - + if (_errorMessage != null) _buildErrorBanner(colors), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Intermediate Stops', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, + // Left Column: Details, Schedule, Privacy + Expanded( + flex: 5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRideDetailsSection(colors), + const SizedBox(height: 24), + _buildScheduleSection(colors), + const SizedBox(height: 24), + _buildPrivacySection(colors), + ], ), ), - TextButton.icon( - onPressed: _addStopDialog, - icon: const Icon(Icons.add_location_alt, size: 16), - label: const Text('ADD STOP'), + const SizedBox(width: 24), + // Right Column: Meeting Point, Destination & Route + Expanded( + flex: 5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMeetingPointSection(colors), + const SizedBox(height: 24), + _buildDestinationAndStopsSection(colors), + const SizedBox(height: 24), + _buildSubmitButton(colors), + ], + ), ), ], ), - if (_stops.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: Text( - 'No intermediate stops planned. Ride goes directly from Meetup to Destination.', - style: TextStyle(color: Colors.white38, fontSize: 12), - ), - ) - else - ReorderableListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - onReorderItem: (oldIndex, newIndex) { - setState(() { - final item = _stops.removeAt(oldIndex); - _stops.insert(newIndex, item); - }); - }, - children: [ - for (int i = 0; i < _stops.length; i++) - ListTile( - key: ValueKey('stop_$i'), - tileColor: const Color(0xFF161922), - leading: CircleAvatar( - radius: 12, - backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.2), - child: Text( - '${i + 1}', - style: const TextStyle( - fontSize: 11, - color: Color(0xFF00E5FF), - fontWeight: FontWeight.bold, - ), - ), - ), - title: Text( - _stops[i].displayName, - style: const TextStyle(color: Colors.white, fontSize: 13), - ), - subtitle: Text( - _stops[i].locationType.label, - style: const TextStyle(color: Colors.white54, fontSize: 11), - ), - trailing: IconButton( - icon: const Icon(Icons.delete_outline, - size: 18, color: Colors.white38), - onPressed: () => setState(() => _stops.removeAt(i)), - ), - ), - ], - ), - - const SizedBox(height: 24), - _buildSectionHeader('5. PRIVACY & PARTICIPATION'), - const SizedBox(height: 10), - DropdownButtonFormField( - initialValue: _visibility, - dropdownColor: const Color(0xFF1B1E28), - style: const TextStyle(color: Colors.white), - decoration: _inputDecoration('Visibility', ''), - items: GroupRideVisibility.values.map((v) { - return DropdownMenuItem(value: v, child: Text(v.label)); - }).toList(), - onChanged: (val) { - if (val != null) setState(() => _visibility = val); - }, - ), - const SizedBox(height: 12), - DropdownButtonFormField( - initialValue: _joinPolicy, - dropdownColor: const Color(0xFF1B1E28), - style: const TextStyle(color: Colors.white), - decoration: _inputDecoration('Join Policy', ''), - items: GroupRideJoinPolicy.values.map((p) { - return DropdownMenuItem(value: p, child: Text(p.label)); - }).toList(), - onChanged: (val) { - if (val != null) setState(() => _joinPolicy = val); - }, - ), - const SizedBox(height: 12), - TextFormField( - keyboardType: TextInputType.number, - style: const TextStyle(color: Colors.white), - decoration: _inputDecoration( - 'Participant Limit (optional)', - 'Leave blank for no limit (min 2)', - ), - onChanged: (val) { - final parsed = int.tryParse(val.trim()); - setState(() => _participantLimit = parsed); - }, - ), - - const SizedBox(height: 32), - SizedBox( - width: double.infinity, - height: 48, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - onPressed: _isSaving ? null : _submit, - child: _isSaving - ? const CircularProgressIndicator(color: Colors.black) - : const Text( - 'CREATE GROUP RIDE', - style: TextStyle( - fontWeight: FontWeight.bold, - letterSpacing: 1.0, - ), - ), - ), - ), const SizedBox(height: 32), ], ), @@ -859,11 +529,518 @@ class _CreateGroupRideScreenState extends ConsumerState { ); } - Widget _buildSectionHeader(String title) { + Widget _buildCompactForm( + BuildContext context, + MotorSignalThemeColors colors, + ResponsiveSnapshot snapshot, + ) { + return Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(20), + children: [ + if (_errorMessage != null) _buildErrorBanner(colors), + _buildRideDetailsSection(colors), + const SizedBox(height: 24), + _buildScheduleSection(colors), + const SizedBox(height: 24), + _buildMeetingPointSection(colors), + const SizedBox(height: 24), + _buildDestinationAndStopsSection(colors), + const SizedBox(height: 24), + _buildPrivacySection(colors), + const SizedBox(height: 32), + _buildSubmitButton(colors), + const SizedBox(height: 32), + ], + ), + ); + } + + Widget _buildErrorBanner(MotorSignalThemeColors colors) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colors.raceRed.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: colors.raceRed), + ), + child: Row( + children: [ + Icon(Icons.error_outline, color: colors.raceRed, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + _errorMessage!, + style: TextStyle(color: colors.raceRed, fontSize: 12), + ), + ), + ], + ), + ); + } + + Widget _buildRideDetailsSection(MotorSignalThemeColors colors) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('1. RIDE DETAILS', colors), + const SizedBox(height: 10), + TextFormField( + controller: _titleController, + style: const TextStyle(color: Colors.white), + decoration: _inputDecoration( + 'Ride Title', + 'e.g. Blood Mountain Loop', + colors, + ), + validator: (v) { + if (v == null || v.trim().length < 3) { + return 'Title must be at least 3 characters'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _descriptionController, + style: const TextStyle(color: Colors.white), + maxLines: 3, + decoration: _inputDecoration( + 'Description & Route Notes', + 'Pace, skill level, gear recommendations...', + colors, + ), + ), + ], + ); + } + + Widget _buildScheduleSection(MotorSignalThemeColors colors) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('2. DATE & SCHEDULE', colors), + const SizedBox(height: 10), + Card( + color: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.calendar_month, color: colors.cyberCyan), + title: const Text('Date', + style: TextStyle(color: Colors.white70, fontSize: 13)), + subtitle: Text( + '${_scheduledDate.month}/${_scheduledDate.day}/${_scheduledDate.year}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + trailing: TextButton( + onPressed: () async { + final picked = await showDatePicker( + context: context, + initialDate: _scheduledDate, + firstDate: DateTime.now(), + lastDate: + DateTime.now().add(const Duration(days: 365)), + ); + if (picked != null) { + setState(() => _scheduledDate = picked); + } + }, + child: Text( + 'CHANGE', + style: TextStyle(color: colors.cyberCyan), + ), + ), + ), + const Divider(color: Colors.white12), + Row( + children: [ + Expanded( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Meetup Arrival', + style: TextStyle( + color: Colors.white70, fontSize: 12)), + subtitle: Text( + _meetupTime.format(context), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + onTap: () async { + final picked = await showTimePicker( + context: context, + initialTime: _meetupTime, + ); + if (picked != null) { + setState(() => _meetupTime = picked); + } + }, + ), + ), + Expanded( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Kickstands Up (KSU)', + style: TextStyle( + color: Colors.white70, fontSize: 12)), + subtitle: Text( + _departureTime.format(context), + style: TextStyle( + color: colors.cyberCyan, + fontWeight: FontWeight.bold, + ), + ), + onTap: () async { + final picked = await showTimePicker( + context: context, + initialTime: _departureTime, + ); + if (picked != null) { + setState(() => _departureTime = picked); + } + }, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildMeetingPointSection(MotorSignalThemeColors colors) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('3. MEETING LOCATION', colors), + const SizedBox(height: 10), + TextFormField( + controller: _meetupNameController, + style: const TextStyle(color: Colors.white), + decoration: _inputDecoration( + 'Meeting Place Name', + 'e.g. Dahlonega Square / Shell Station', + colors, + ), + validator: (v) { + if (v == null || v.trim().isEmpty) { + return 'Please provide a meeting location name'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _meetupAddressController, + style: const TextStyle(color: Colors.white), + decoration: _inputDecoration( + 'Address or Landmark', + 'e.g. 123 Main St, Dahlonega, GA', + colors, + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _meetupNotesController, + style: const TextStyle(color: Colors.white), + decoration: _inputDecoration( + 'Meeting Notes (optional)', + 'e.g. Park on the north side near pumps', + colors, + ), + ), + ], + ); + } + + Widget _buildDestinationAndStopsSection(MotorSignalThemeColors colors) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('4. DESTINATION & STOPS', colors), + const SizedBox(height: 10), + SwitchListTile( + contentPadding: EdgeInsets.zero, + activeThumbColor: colors.cyberCyan, + title: const Text('Specific Destination', + style: TextStyle(color: Colors.white)), + value: _hasDestination, + onChanged: (val) => setState(() => _hasDestination = val), + ), + if (_hasDestination) ...[ + const SizedBox(height: 8), + TextFormField( + controller: _destNameController, + style: const TextStyle(color: Colors.white), + decoration: _inputDecoration( + 'Destination Name', + 'e.g. Brasstown Bald Summit', + colors, + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _destAddressController, + style: const TextStyle(color: Colors.white), + decoration: _inputDecoration( + 'Destination Address (optional)', + 'e.g. Hiawassee, GA', + colors, + ), + ), + ], + const SizedBox(height: 16), + // Route Pre-fill / Attachment + Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: colors.outlineVariant), + ), + child: Row( + children: [ + Icon(Icons.alt_route, color: colors.cyberCyan, size: 22), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _selectedRouteTitle != null + ? 'Route: $_selectedRouteTitle' + : 'Plan with Saved Route', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 13, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + _selectedRouteTitle != null + ? '$_selectedRouteStopCount stops • ${(_selectedRouteDistanceMeters != null ? (_selectedRouteDistanceMeters! / 1609.34).toStringAsFixed(1) : 0)} mi' + : 'Optional: import stops & coordinates', + style: + const TextStyle(color: Colors.white54, fontSize: 11), + ), + ], + ), + ), + if (_selectedRouteTitle != null) + IconButton( + icon: + const Icon(Icons.close, color: Colors.white54, size: 18), + tooltip: 'Clear route', + onPressed: () { + setState(() { + _selectedRouteId = null; + _selectedRouteTitle = null; + _selectedRouteDistanceMeters = null; + _selectedRouteStopCount = null; + }); + }, + ) + else + OutlinedButton( + onPressed: () => _showSelectRouteSheet(colors), + style: OutlinedButton.styleFrom( + side: BorderSide(color: colors.cyberCyan), + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + 'IMPORT', + style: TextStyle( + color: colors.cyberCyan, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Flexible( + child: Text( + 'Intermediate Stops', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + TextButton.icon( + onPressed: () => _addStopDialog(colors), + icon: Icon(Icons.add_location_alt, + size: 16, color: colors.cyberCyan), + label: Text('ADD STOP', + style: TextStyle(color: colors.cyberCyan)), + ), + ], + ), + if (_stops.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Text( + 'No intermediate stops planned. Ride goes directly from Meetup to Destination.', + style: TextStyle(color: Colors.white38, fontSize: 12), + ), + ) + else + ReorderableListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + onReorderItem: (oldIndex, newIndex) { + setState(() { + final item = _stops.removeAt(oldIndex); + _stops.insert(newIndex, item); + }); + }, + children: [ + for (int i = 0; i < _stops.length; i++) + ListTile( + key: ValueKey('stop_$i'), + tileColor: colors.surfaceContainerHigh, + leading: CircleAvatar( + radius: 12, + backgroundColor: colors.cyberCyan.withValues(alpha: 0.2), + child: Text( + '${i + 1}', + style: TextStyle( + fontSize: 11, + color: colors.cyberCyan, + fontWeight: FontWeight.bold, + ), + ), + ), + title: Text( + _stops[i].displayName, + style: + const TextStyle(color: Colors.white, fontSize: 13), + ), + subtitle: Text( + _stops[i].locationType.label, + style: + const TextStyle(color: Colors.white54, fontSize: 11), + ), + trailing: IconButton( + icon: const Icon(Icons.delete_outline, + size: 18, color: Colors.white38), + onPressed: () => setState(() => _stops.removeAt(i)), + ), + ), + ], + ), + ], + ); + } + + Widget _buildPrivacySection(MotorSignalThemeColors colors) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('5. PRIVACY & PARTICIPATION', colors), + const SizedBox(height: 10), + DropdownButtonFormField( + initialValue: _visibility, + dropdownColor: colors.surfaceContainerHigh, + style: const TextStyle(color: Colors.white), + decoration: _inputDecoration('Visibility', '', colors), + items: GroupRideVisibility.values.map((v) { + return DropdownMenuItem(value: v, child: Text(v.label)); + }).toList(), + onChanged: (val) { + if (val != null) setState(() => _visibility = val); + }, + ), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: _joinPolicy, + dropdownColor: colors.surfaceContainerHigh, + style: const TextStyle(color: Colors.white), + decoration: _inputDecoration('Join Policy', '', colors), + items: GroupRideJoinPolicy.values.map((p) { + return DropdownMenuItem(value: p, child: Text(p.label)); + }).toList(), + onChanged: (val) { + if (val != null) setState(() => _joinPolicy = val); + }, + ), + const SizedBox(height: 12), + TextFormField( + keyboardType: TextInputType.number, + style: const TextStyle(color: Colors.white), + decoration: _inputDecoration( + 'Participant Limit (optional)', + 'Leave blank for no limit (min 2)', + colors, + ), + onChanged: (val) { + final parsed = int.tryParse(val.trim()); + setState(() => _participantLimit = parsed); + }, + ), + ], + ); + } + + Widget _buildSubmitButton(MotorSignalThemeColors colors) { + return SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: colors.cyberCyan, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: _isSaving ? null : _submit, + child: _isSaving + ? const CircularProgressIndicator(color: Colors.black) + : const Text( + 'PUBLISH GROUP RIDE', + style: TextStyle( + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + ), + ), + ), + ); + } + + Widget _buildSectionHeader(String title, MotorSignalThemeColors colors) { return Text( title, - style: const TextStyle( - color: Color(0xFF00E5FF), + style: TextStyle( + color: colors.cyberCyan, fontSize: 13, fontWeight: FontWeight.w900, letterSpacing: 1.0, @@ -871,21 +1048,29 @@ class _CreateGroupRideScreenState extends ConsumerState { ); } - InputDecoration _inputDecoration(String label, String hint) { + InputDecoration _inputDecoration( + String label, + String hint, + MotorSignalThemeColors colors, + ) { return InputDecoration( labelText: label, hintText: hint, labelStyle: const TextStyle(color: Colors.white70, fontSize: 13), hintStyle: const TextStyle(color: Colors.white24, fontSize: 12), filled: true, - fillColor: const Color(0xFF161922), + fillColor: colors.surfaceContainerHigh, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + borderSide: BorderSide(color: colors.outlineVariant), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: colors.outlineVariant), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF00E5FF)), + borderSide: BorderSide(color: colors.cyberCyan), ), ); } diff --git a/lib/features/group_rides/group_ride_detail_screen.dart b/lib/features/group_rides/group_ride_detail_screen.dart index dce93e5..db0dda8 100644 --- a/lib/features/group_rides/group_ride_detail_screen.dart +++ b/lib/features/group_rides/group_ride_detail_screen.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/responsive/viewport_size.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../services/auth/auth_providers.dart'; import '../../services/group_rides/group_ride_models.dart'; import '../../services/group_rides/group_ride_providers.dart'; @@ -42,13 +45,17 @@ class _GroupRideDetailScreenState extends ConsumerState { } } - void _showInviteFriendsSheet(BuildContext context, GroupRide ride) { + void _showInviteFriendsSheet( + BuildContext context, + GroupRide ride, + MotorSignalThemeColors colors, + ) { final friendsAsync = ref.read(myFriendsProvider); final service = ref.read(groupRideServiceProvider); showModalBottomSheet( context: context, - backgroundColor: const Color(0xFF161922), + backgroundColor: colors.surfaceContainerHigh, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), @@ -71,10 +78,10 @@ class _GroupRideDetailScreenState extends ConsumerState { return ListView( padding: const EdgeInsets.all(16), children: [ - const Text( + Text( 'INVITE FRIENDS', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontWeight: FontWeight.bold, letterSpacing: 1.0, ), @@ -100,7 +107,7 @@ class _GroupRideDetailScreenState extends ConsumerState { ), trailing: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), onPressed: () async { @@ -143,8 +150,15 @@ class _GroupRideDetailScreenState extends ConsumerState { ], ); }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (e, _) => Center(child: Text('Error: $e')), + loading: () => Center( + child: CircularProgressIndicator(color: colors.cyberCyan), + ), + error: (e, _) => Center( + child: Text( + 'Error: $e', + style: TextStyle(color: colors.raceRed), + ), + ), ); }, ); @@ -152,6 +166,7 @@ class _GroupRideDetailScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final colors = context.motorColors; final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId)); final participantsAsync = ref.watch( groupRideParticipantsProvider(widget.rideId), @@ -160,9 +175,10 @@ class _GroupRideDetailScreenState extends ConsumerState { final currentUid = user?.uid; return Scaffold( - backgroundColor: const Color(0xFF0D0E11), + backgroundColor: colors.surfaceContainer, appBar: AppBar( - backgroundColor: const Color(0xFF13151A), + backgroundColor: colors.surfaceContainerHigh, + elevation: 0, title: const Text( 'GROUP RIDE', style: TextStyle( @@ -195,121 +211,240 @@ class _GroupRideDetailScreenState extends ConsumerState { ); } catch (_) {} - return ListView( - padding: const EdgeInsets.all(20), - children: [ - if (_statusMessage != null) - Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.cyanAccent.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.cyanAccent), - ), - child: Text( - _statusMessage!, - style: const TextStyle( - color: Colors.cyanAccent, - fontSize: 13, - ), - ), - ), + return ResponsiveLayout( + builder: (context, snapshot) { + final isWide = + snapshot.sizeClass == WindowSizeClass.expanded || + (snapshot.isLandscape && snapshot.width >= 720); - // Header card - _buildHeaderCard(context, ride, isOrganizer), - const SizedBox(height: 16), + if (isWide) { + return _buildWideLayout( + context, + ride, + participants, + myParticipant, + isOrganizer, + currentUid, + colors, + ); + } - // Organizer Status Controls - if (isOrganizer && !ride.isCanceled && !ride.isCompleted) ...[ - _buildOrganizerControlCard(context, ride), - const SizedBox(height: 16), - ], - - // Dynamic action banner based on membership/organizer state - _buildActionBanner( + return _buildCompactLayout( context, ride, + participants, myParticipant, isOrganizer, currentUid, - ), - const SizedBox(height: 16), - - // Live Presence Ephemeral Section (for active members) - if (ride.isLiveEligible && - myParticipant != null && - myParticipant.status == ParticipantStatus.accepted) ...[ - _buildLivePresenceSection( - context, - ride, - myParticipant, - currentUid, - ), - const SizedBox(height: 16), - ], - - // Schedule card - _buildScheduleCard(ride), - const SizedBox(height: 16), - - // Meeting location & Destination - _buildLocationsCard(ride), - const SizedBox(height: 16), - - // Route Card - _buildRouteCard(context, ride, isOrganizer, currentUid), - const SizedBox(height: 16), - - // Intermediate stops - if (ride.stops.isNotEmpty) ...[ - _buildStopsCard(ride), - const SizedBox(height: 16), - ], - - // Participants roster - _buildParticipantsCard( - ride, - participants, - isOrganizer, - currentUid, - ), - const SizedBox(height: 32), - ], + colors, + ); + }, ); }, - loading: () => const Center( - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + loading: () => Center( + child: CircularProgressIndicator(color: colors.cyberCyan), ), error: (e, _) => Center( child: Text( 'Error loading participants: $e', - style: const TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), ), ); }, - loading: () => const Center( - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + loading: () => Center( + child: CircularProgressIndicator(color: colors.cyberCyan), ), error: (e, _) => Center( child: Text( 'Error loading ride: $e', - style: const TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), ), ), ); } + Widget _buildWideLayout( + BuildContext context, + GroupRide ride, + List participants, + GroupRideParticipant? myParticipant, + bool isOrganizer, + String? currentUid, + MotorSignalThemeColors colors, + ) { + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left Column (flex 6): Primary Info, Action Banner, Schedule, Locations, Route, Live Presence + Expanded( + flex: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_statusMessage != null) ...[ + _buildStatusMessageBanner(colors), + const SizedBox(height: 16), + ], + _buildHeaderCard(context, ride, isOrganizer, colors), + const SizedBox(height: 16), + _buildActionBanner( + context, + ride, + myParticipant, + isOrganizer, + currentUid, + colors, + ), + const SizedBox(height: 16), + if (ride.isLiveEligible && + myParticipant != null && + myParticipant.status == ParticipantStatus.accepted) ...[ + _buildLivePresenceSection( + context, + ride, + myParticipant, + currentUid, + colors, + ), + const SizedBox(height: 16), + ], + _buildScheduleCard(ride, colors), + const SizedBox(height: 16), + _buildLocationsCard(ride, colors), + const SizedBox(height: 16), + _buildRouteCard(context, ride, isOrganizer, currentUid, colors), + ], + ), + ), + const SizedBox(width: 20), + // Right Column (flex 5): Organizer Controls, Participants Roster, Intermediate Stops + Expanded( + flex: 5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isOrganizer && !ride.isCanceled && !ride.isCompleted) ...[ + _buildOrganizerControlCard(context, ride, colors), + const SizedBox(height: 16), + ], + _buildParticipantsCard( + ride, + participants, + isOrganizer, + currentUid, + colors, + ), + if (ride.stops.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildStopsCard(ride, colors), + ], + ], + ), + ), + ], + ), + ); + } + + Widget _buildCompactLayout( + BuildContext context, + GroupRide ride, + List participants, + GroupRideParticipant? myParticipant, + bool isOrganizer, + String? currentUid, + MotorSignalThemeColors colors, + ) { + return ListView( + padding: const EdgeInsets.all(20), + children: [ + if (_statusMessage != null) ...[ + _buildStatusMessageBanner(colors), + const SizedBox(height: 16), + ], + _buildHeaderCard(context, ride, isOrganizer, colors), + const SizedBox(height: 16), + if (isOrganizer && !ride.isCanceled && !ride.isCompleted) ...[ + _buildOrganizerControlCard(context, ride, colors), + const SizedBox(height: 16), + ], + _buildActionBanner( + context, + ride, + myParticipant, + isOrganizer, + currentUid, + colors, + ), + const SizedBox(height: 16), + if (ride.isLiveEligible && + myParticipant != null && + myParticipant.status == ParticipantStatus.accepted) ...[ + _buildLivePresenceSection( + context, + ride, + myParticipant, + currentUid, + colors, + ), + const SizedBox(height: 16), + ], + _buildScheduleCard(ride, colors), + const SizedBox(height: 16), + _buildLocationsCard(ride, colors), + const SizedBox(height: 16), + _buildRouteCard(context, ride, isOrganizer, currentUid, colors), + const SizedBox(height: 16), + if (ride.stops.isNotEmpty) ...[ + _buildStopsCard(ride, colors), + const SizedBox(height: 16), + ], + _buildParticipantsCard( + ride, + participants, + isOrganizer, + currentUid, + colors, + ), + const SizedBox(height: 32), + ], + ); + } + + Widget _buildStatusMessageBanner(MotorSignalThemeColors colors) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colors.cyberCyan.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: colors.cyberCyan), + ), + child: Text( + _statusMessage!, + style: TextStyle( + color: colors.cyberCyan, + fontSize: 13, + ), + ), + ); + } + Widget _buildHeaderCard( BuildContext context, GroupRide ride, bool isOrganizer, + MotorSignalThemeColors colors, ) { return Card( - color: const Color(0xFF161922), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), child: Padding( padding: const EdgeInsets.all(18), child: Column( @@ -317,57 +452,64 @@ class _GroupRideDetailScreenState extends ConsumerState { children: [ Row( children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: ride.isCanceled - ? Colors.redAccent.withValues(alpha: 0.2) - : const Color(0xFF00E5FF).withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - ride.status.label.toUpperCase(), - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w900, - color: ride.isCanceled - ? Colors.redAccent - : const Color(0xFF00E5FF), - ), + Expanded( + child: Wrap( + spacing: 8, + runSpacing: 6, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: ride.isCanceled + ? colors.raceRed.withValues(alpha: 0.2) + : colors.cyberCyan.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + ride.status.label.toUpperCase(), + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w900, + color: ride.isCanceled + ? colors.raceRed + : colors.cyberCyan, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + ride.visibility.label.toUpperCase(), + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.white70, + ), + ), + ), + ], ), ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: Colors.white10, - borderRadius: BorderRadius.circular(6), - ), - child: Text( - ride.visibility.label.toUpperCase(), - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Colors.white70, - ), - ), - ), - const Spacer(), if (isOrganizer && !ride.isCanceled) PopupMenuButton( icon: const Icon(Icons.more_vert, color: Colors.white70), - color: const Color(0xFF1B1E28), + color: colors.surfaceContainerHigh, onSelected: (val) async { if (val == 'invite') { - _showInviteFriendsSheet(context, ride); + _showInviteFriendsSheet(context, ride, colors); } else if (val == 'cancel') { - _confirmCancelRide(ride); + _confirmCancelRide(ride, colors); } }, itemBuilder: (ctx) => [ @@ -378,11 +520,11 @@ class _GroupRideDetailScreenState extends ConsumerState { style: TextStyle(color: Colors.white), ), ), - const PopupMenuItem( + PopupMenuItem( value: 'cancel', child: Text( 'Cancel Ride', - style: TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), ), ], @@ -401,7 +543,7 @@ class _GroupRideDetailScreenState extends ConsumerState { const SizedBox(height: 6), Text( 'Organized by @${ride.organizerHandle} (${ride.organizerDisplayName})', - style: const TextStyle(color: Color(0xFF00E5FF), fontSize: 13), + style: TextStyle(color: colors.cyberCyan, fontSize: 13), ), if (ride.description.isNotEmpty) ...[ const SizedBox(height: 12), @@ -419,25 +561,25 @@ class _GroupRideDetailScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: Colors.redAccent.withValues(alpha: 0.1), + color: colors.raceRed.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all( - color: Colors.redAccent.withValues(alpha: 0.4), + color: colors.raceRed.withValues(alpha: 0.4), ), ), child: Row( children: [ - const Icon( + Icon( Icons.info_outline, - color: Colors.redAccent, + color: colors.raceRed, size: 16, ), const SizedBox(width: 8), Expanded( child: Text( 'Canceled: ${ride.canceledReason}', - style: const TextStyle( - color: Colors.redAccent, + style: TextStyle( + color: colors.raceRed, fontSize: 12, ), ), @@ -458,10 +600,15 @@ class _GroupRideDetailScreenState extends ConsumerState { GroupRideParticipant? myParticipant, bool isOrganizer, String? currentUid, + MotorSignalThemeColors colors, ) { if (currentUid == null) { return Card( - color: const Color(0xFF161922), + color: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -474,7 +621,7 @@ class _GroupRideDetailScreenState extends ConsumerState { const SizedBox(height: 10), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), onPressed: () => context.push('/settings/account'), @@ -498,9 +645,9 @@ class _GroupRideDetailScreenState extends ConsumerState { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFF00E5FF).withValues(alpha: 0.1), + color: colors.cyberCyan.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF00E5FF)), + border: Border.all(color: colors.cyberCyan), ), child: Column( children: [ @@ -530,7 +677,9 @@ class _GroupRideDetailScreenState extends ConsumerState { groupRideId: ride.id, accept: false, ); - setState(() => _isProcessing = false); + if (mounted) { + setState(() => _isProcessing = false); + } }, child: const Text('DECLINE'), ), @@ -539,7 +688,7 @@ class _GroupRideDetailScreenState extends ConsumerState { Expanded( child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), onPressed: _isProcessing @@ -551,7 +700,9 @@ class _GroupRideDetailScreenState extends ConsumerState { groupRideId: ride.id, accept: true, ); - setState(() => _isProcessing = false); + if (mounted) { + setState(() => _isProcessing = false); + } }, child: const Text('ACCEPT & JOIN'), ), @@ -569,18 +720,20 @@ class _GroupRideDetailScreenState extends ConsumerState { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: Colors.amber.withValues(alpha: 0.1), + color: colors.warningAmber.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.amber.withValues(alpha: 0.4)), + border: Border.all( + color: colors.warningAmber.withValues(alpha: 0.4), + ), ), - child: const Row( + child: Row( children: [ - Icon(Icons.hourglass_top, color: Colors.amber, size: 20), - SizedBox(width: 10), + Icon(Icons.hourglass_top, color: colors.warningAmber, size: 20), + const SizedBox(width: 10), Expanded( child: Text( 'Join request pending organizer approval.', - style: TextStyle(color: Colors.amber, fontSize: 13), + style: TextStyle(color: colors.warningAmber, fontSize: 13), ), ), ], @@ -595,21 +748,21 @@ class _GroupRideDetailScreenState extends ConsumerState { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Row( + Row( children: [ - Icon(Icons.check_circle, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 6), - Text( + Icon(Icons.check_circle, color: colors.cyberCyan, size: 18), + const SizedBox(width: 6), + const Text( 'You are participating in this ride', style: TextStyle(color: Colors.white70, fontSize: 13), ), ], ), TextButton( - onPressed: () => _confirmLeaveRide(ride, currentUid), - child: const Text( + onPressed: () => _confirmLeaveRide(ride, currentUid, colors), + child: Text( 'LEAVE RIDE', - style: TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), ), ], @@ -621,7 +774,7 @@ class _GroupRideDetailScreenState extends ConsumerState { if (ride.joinPolicy == GroupRideJoinPolicy.open) { return ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, minimumSize: const Size.fromHeight(46), shape: RoundedRectangleBorder( @@ -656,8 +809,8 @@ class _GroupRideDetailScreenState extends ConsumerState { } else if (ride.joinPolicy == GroupRideJoinPolicy.requestToJoin) { return OutlinedButton.icon( style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), + foregroundColor: colors.cyberCyan, + side: BorderSide(color: colors.cyberCyan), minimumSize: const Size.fromHeight(46), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), @@ -691,23 +844,26 @@ class _GroupRideDetailScreenState extends ConsumerState { return const SizedBox.shrink(); } - Widget _buildScheduleCard(GroupRide ride) { + Widget _buildScheduleCard(GroupRide ride, MotorSignalThemeColors colors) { return Card( - color: const Color(0xFF161922), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Row( + Row( children: [ - Icon(Icons.schedule, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 8), + Icon(Icons.schedule, color: colors.cyberCyan, size: 18), + const SizedBox(width: 8), Text( 'SCHEDULE', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontWeight: FontWeight.bold, letterSpacing: 1.0, fontSize: 12, @@ -757,15 +913,15 @@ class _GroupRideDetailScreenState extends ConsumerState { const SizedBox(height: 4), Text( _formatTime(ride.departureAt), - style: const TextStyle( - color: Color(0xFF00E5FF), + style: TextStyle( + color: colors.cyberCyan, fontSize: 18, fontWeight: FontWeight.bold, ), ), - Text( + const Text( 'Departure', - style: const TextStyle( + style: TextStyle( color: Colors.white54, fontSize: 11, ), @@ -781,26 +937,32 @@ class _GroupRideDetailScreenState extends ConsumerState { ); } - Widget _buildLocationsCard(GroupRide ride) { + Widget _buildLocationsCard(GroupRide ride, MotorSignalThemeColors colors) { return Card( - color: const Color(0xFF161922), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Row( + Row( children: [ - Icon(Icons.place_outlined, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 8), - Text( - 'MEETING POINT & DESTINATION', - style: TextStyle( - color: Color(0xFF00E5FF), - fontWeight: FontWeight.bold, - letterSpacing: 1.0, - fontSize: 12, + Icon(Icons.place_outlined, color: colors.cyberCyan, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + 'MEETING POINT & DESTINATION', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: colors.cyberCyan, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + fontSize: 12, + ), ), ), ], @@ -809,10 +971,11 @@ class _GroupRideDetailScreenState extends ConsumerState { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const CircleAvatar( + CircleAvatar( radius: 12, - backgroundColor: Color(0xFF00E5FF), - child: Icon(Icons.two_wheeler, size: 14, color: Colors.black), + backgroundColor: colors.cyberCyan, + child: const Icon(Icons.two_wheeler, + size: 14, color: Colors.black), ), const SizedBox(width: 12), Expanded( @@ -850,9 +1013,9 @@ class _GroupRideDetailScreenState extends ConsumerState { ), ), IconButton( - icon: const Icon( + icon: Icon( Icons.navigation_outlined, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), tooltip: 'Open in Map App', onPressed: () => _launchMapNavigation(ride.meetingPoint), @@ -867,10 +1030,11 @@ class _GroupRideDetailScreenState extends ConsumerState { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const CircleAvatar( + CircleAvatar( radius: 12, - backgroundColor: Color(0xFFFFB300), - child: Icon(Icons.flag, size: 14, color: Colors.black), + backgroundColor: colors.warningAmber, + child: + const Icon(Icons.flag, size: 14, color: Colors.black), ), const SizedBox(width: 12), Expanded( @@ -897,9 +1061,9 @@ class _GroupRideDetailScreenState extends ConsumerState { ), ), IconButton( - icon: const Icon( + icon: Icon( Icons.navigation_outlined, - color: Color(0xFFFFB300), + color: colors.warningAmber, ), tooltip: 'Open in Map App', onPressed: () => _launchMapNavigation(ride.destination!), @@ -918,12 +1082,16 @@ class _GroupRideDetailScreenState extends ConsumerState { GroupRide ride, bool isOrganizer, String? currentUid, + MotorSignalThemeColors colors, ) { final hasRoute = ride.routeId != null; return Card( - color: const Color(0xFF161922), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -932,14 +1100,14 @@ class _GroupRideDetailScreenState extends ConsumerState { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Row( + Row( children: [ - Icon(Icons.alt_route, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 8), + Icon(Icons.alt_route, color: colors.cyberCyan, size: 18), + const SizedBox(width: 8), Text( 'ROUTE & WAYPOINTS', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontWeight: FontWeight.bold, letterSpacing: 1.0, fontSize: 12, @@ -954,13 +1122,13 @@ class _GroupRideDetailScreenState extends ConsumerState { vertical: 2, ), decoration: BoxDecoration( - color: const Color(0xFF00E5FF).withValues(alpha: 0.15), + color: colors.cyberCyan.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), child: Text( '${ride.routeStopCount ?? 0} STOPS', - style: const TextStyle( - color: Color(0xFF00E5FF), + style: TextStyle( + color: colors.cyberCyan, fontSize: 10, fontWeight: FontWeight.bold, ), @@ -996,7 +1164,7 @@ class _GroupRideDetailScreenState extends ConsumerState { icon: const Icon(Icons.map, size: 16), label: const Text('VIEW ROUTE / PLANNER'), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, textStyle: const TextStyle( fontSize: 12, @@ -1006,18 +1174,19 @@ class _GroupRideDetailScreenState extends ConsumerState { ), if (isOrganizer && !ride.isCanceled && !ride.isCompleted) ...[ OutlinedButton.icon( - onPressed: () => _showAttachRouteSheet(context, ride), - icon: const Icon( + onPressed: () => + _showAttachRouteSheet(context, ride, colors), + icon: Icon( Icons.swap_horiz, size: 16, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), - label: const Text( + label: Text( 'CHANGE', - style: TextStyle(color: Color(0xFF00E5FF)), + style: TextStyle(color: colors.cyberCyan), ), style: OutlinedButton.styleFrom( - side: const BorderSide(color: Color(0xFF2A2E3D)), + side: BorderSide(color: colors.outlineVariant), textStyle: const TextStyle(fontSize: 12), ), ), @@ -1026,7 +1195,8 @@ class _GroupRideDetailScreenState extends ConsumerState { try { final user = ref.read(currentUserProvider); if (user == null || user.uid.isEmpty) { - throw StateError('Sign-in required to detach route.'); + throw StateError( + 'Sign-in required to detach route.'); } await ref .read(routeServiceProvider) @@ -1053,17 +1223,17 @@ class _GroupRideDetailScreenState extends ConsumerState { } } }, - icon: const Icon( + icon: Icon( Icons.link_off, size: 16, - color: Color(0xFFFF5252), + color: colors.raceRed, ), - label: const Text( + label: Text( 'DETACH', - style: TextStyle(color: Color(0xFFFF5252)), + style: TextStyle(color: colors.raceRed), ), style: OutlinedButton.styleFrom( - side: const BorderSide(color: Color(0xFFFF5252)), + side: BorderSide(color: colors.raceRed), textStyle: const TextStyle(fontSize: 12), ), ), @@ -1094,7 +1264,7 @@ class _GroupRideDetailScreenState extends ConsumerState { icon: const Icon(Icons.add_road, size: 16), label: const Text('PLAN NEW ROUTE'), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, textStyle: const TextStyle( fontSize: 12, @@ -1104,18 +1274,19 @@ class _GroupRideDetailScreenState extends ConsumerState { ), OutlinedButton.icon( key: const ValueKey('attach_saved_route_button'), - onPressed: () => _showAttachRouteSheet(context, ride), - icon: const Icon( + onPressed: () => + _showAttachRouteSheet(context, ride, colors), + icon: Icon( Icons.link, size: 16, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), - label: const Text( + label: Text( 'ATTACH SAVED ROUTE', - style: TextStyle(color: Color(0xFF00E5FF)), + style: TextStyle(color: colors.cyberCyan), ), style: OutlinedButton.styleFrom( - side: const BorderSide(color: Color(0xFF2A2E3D)), + side: BorderSide(color: colors.outlineVariant), textStyle: const TextStyle(fontSize: 12), ), ), @@ -1129,13 +1300,17 @@ class _GroupRideDetailScreenState extends ConsumerState { ); } - void _showAttachRouteSheet(BuildContext context, GroupRide ride) { + void _showAttachRouteSheet( + BuildContext context, + GroupRide ride, + MotorSignalThemeColors colors, + ) { final user = ref.read(currentUserProvider); if (user == null) return; showModalBottomSheet( context: context, - backgroundColor: const Color(0xFF13151A), + backgroundColor: colors.surfaceContainerHigh, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), @@ -1160,17 +1335,16 @@ class _GroupRideDetailScreenState extends ConsumerState { ), const SizedBox(height: 12), routesAsync.when( - loading: () => const Center( + loading: () => Center( child: Padding( - padding: EdgeInsets.all(24), - child: CircularProgressIndicator( - color: Color(0xFF00E5FF), - ), + padding: const EdgeInsets.all(24), + child: + CircularProgressIndicator(color: colors.cyberCyan), ), ), error: (err, _) => Text( 'Failed to load routes: $err', - style: const TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), data: (routes) { if (routes.isEmpty) { @@ -1192,7 +1366,7 @@ class _GroupRideDetailScreenState extends ConsumerState { ); }, style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), child: const Text('CREATE A ROUTE'), @@ -1212,9 +1386,9 @@ class _GroupRideDetailScreenState extends ConsumerState { itemBuilder: (ctx, idx) { final route = routes[idx]; return ListTile( - leading: const Icon( + leading: Icon( Icons.alt_route, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), title: Text( route.title, @@ -1257,9 +1431,8 @@ class _GroupRideDetailScreenState extends ConsumerState { content: Text( 'Attached "${route.title}" to group ride.', ), - backgroundColor: const Color( - 0xFF00E676, - ), + backgroundColor: + colors.launchGreen, ), ); } @@ -1293,23 +1466,26 @@ class _GroupRideDetailScreenState extends ConsumerState { ); } - Widget _buildStopsCard(GroupRide ride) { + Widget _buildStopsCard(GroupRide ride, MotorSignalThemeColors colors) { return Card( - color: const Color(0xFF161922), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Row( + Row( children: [ - Icon(Icons.alt_route, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 8), + Icon(Icons.alt_route, color: colors.cyberCyan, size: 18), + const SizedBox(width: 8), Text( 'INTERMEDIATE STOPS', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontWeight: FontWeight.bold, letterSpacing: 1.0, fontSize: 12, @@ -1380,6 +1556,7 @@ class _GroupRideDetailScreenState extends ConsumerState { List participants, bool isOrganizer, String? currentUid, + MotorSignalThemeColors colors, ) { final active = participants .where((p) => p.status == ParticipantStatus.accepted) @@ -1394,8 +1571,11 @@ class _GroupRideDetailScreenState extends ConsumerState { final service = ref.read(groupRideServiceProvider); return Card( - color: const Color(0xFF161922), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -1404,30 +1584,40 @@ class _GroupRideDetailScreenState extends ConsumerState { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - const Icon( - Icons.people_alt_outlined, - color: Color(0xFF00E5FF), - size: 18, - ), - const SizedBox(width: 8), - Text( - 'PARTICIPANTS (${active.length}${ride.participantLimit != null ? '/${ride.participantLimit}' : ''})', - style: const TextStyle( - color: Color(0xFF00E5FF), - fontWeight: FontWeight.bold, - letterSpacing: 1.0, - fontSize: 12, + Expanded( + child: Row( + children: [ + Icon( + Icons.people_alt_outlined, + color: colors.cyberCyan, + size: 18, ), - ), - ], + const SizedBox(width: 8), + Expanded( + child: Text( + 'PARTICIPANTS (${active.length}${ride.participantLimit != null ? '/${ride.participantLimit}' : ''})', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: colors.cyberCyan, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + fontSize: 12, + ), + ), + ), + ], + ), ), if (isOrganizer && !ride.isCanceled) TextButton.icon( - onPressed: () => _showInviteFriendsSheet(context, ride), - icon: const Icon(Icons.person_add, size: 16), - label: const Text('INVITE'), + onPressed: () => + _showInviteFriendsSheet(context, ride, colors), + icon: Icon(Icons.person_add, + size: 16, color: colors.cyberCyan), + label: Text( + 'INVITE', + style: TextStyle(color: colors.cyberCyan), + ), ), ], ), @@ -1435,10 +1625,10 @@ class _GroupRideDetailScreenState extends ConsumerState { // Pending join requests (visible to organizer) if (isOrganizer && pendingRequests.isNotEmpty) ...[ - const Text( + Text( 'JOIN REQUESTS', style: TextStyle( - color: Colors.amber, + color: colors.warningAmber, fontWeight: FontWeight.bold, fontSize: 11, ), @@ -1459,7 +1649,7 @@ class _GroupRideDetailScreenState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ IconButton( - icon: const Icon(Icons.close, color: Colors.redAccent), + icon: Icon(Icons.close, color: colors.raceRed), onPressed: () async { await service.respondToJoinRequest( callerUid: currentUid!, @@ -1470,7 +1660,7 @@ class _GroupRideDetailScreenState extends ConsumerState { }, ), IconButton( - icon: const Icon(Icons.check, color: Color(0xFF00E5FF)), + icon: Icon(Icons.check, color: colors.cyberCyan), onPressed: () async { await service.respondToJoinRequest( callerUid: currentUid!, @@ -1492,16 +1682,16 @@ class _GroupRideDetailScreenState extends ConsumerState { contentPadding: EdgeInsets.zero, leading: CircleAvatar( backgroundColor: p.role == GroupRideRole.organizer - ? Colors.amber.withValues(alpha: 0.2) - : const Color(0xFF00E5FF).withValues(alpha: 0.2), + ? colors.warningAmber.withValues(alpha: 0.2) + : colors.cyberCyan.withValues(alpha: 0.2), child: Text( p.riderDisplayName.isNotEmpty ? p.riderDisplayName[0].toUpperCase() : '?', style: TextStyle( color: p.role == GroupRideRole.organizer - ? Colors.amber - : const Color(0xFF00E5FF), + ? colors.warningAmber + : colors.cyberCyan, fontWeight: FontWeight.bold, ), ), @@ -1516,46 +1706,46 @@ class _GroupRideDetailScreenState extends ConsumerState { ), trailing: isOrganizer && p.userId != currentUid && !ride.isCanceled - ? PopupMenuButton( - icon: const Icon( - Icons.more_vert, - size: 18, - color: Colors.white38, - ), - color: const Color(0xFF1B1E28), - onSelected: (action) async { - if (action == 'remove') { - await service.removeParticipant( - callerUid: currentUid!, - groupRideId: ride.id, - targetUid: p.userId, - ); - } else if (action == 'transfer') { - await service.transferOrganizer( - callerUid: currentUid!, - groupRideId: ride.id, - newOrganizerUid: p.userId, - ); - } - }, - itemBuilder: (ctx) => [ - const PopupMenuItem( - value: 'transfer', - child: Text( - 'Make Organizer', - style: TextStyle(color: Colors.white), + ? PopupMenuButton( + icon: const Icon( + Icons.more_vert, + size: 18, + color: Colors.white38, ), - ), - const PopupMenuItem( - value: 'remove', - child: Text( - 'Remove Rider', - style: TextStyle(color: Colors.redAccent), - ), - ), - ], - ) - : null, + color: colors.surfaceContainerHigh, + onSelected: (action) async { + if (action == 'remove') { + await service.removeParticipant( + callerUid: currentUid!, + groupRideId: ride.id, + targetUid: p.userId, + ); + } else if (action == 'transfer') { + await service.transferOrganizer( + callerUid: currentUid!, + groupRideId: ride.id, + newOrganizerUid: p.userId, + ); + } + }, + itemBuilder: (ctx) => [ + const PopupMenuItem( + value: 'transfer', + child: Text( + 'Make Organizer', + style: TextStyle(color: Colors.white), + ), + ), + PopupMenuItem( + value: 'remove', + child: Text( + 'Remove Rider', + style: TextStyle(color: colors.raceRed), + ), + ), + ], + ) + : null, ), // Invited riders (visible to organizer) @@ -1589,11 +1779,15 @@ class _GroupRideDetailScreenState extends ConsumerState { ); } - void _confirmCancelRide(GroupRide ride) { + void _confirmCancelRide(GroupRide ride, MotorSignalThemeColors colors) { showDialog( context: context, builder: (ctx) => AlertDialog( - backgroundColor: const Color(0xFF1B1E28), + backgroundColor: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: colors.outlineVariant), + ), title: const Text( 'Cancel Group Ride?', style: TextStyle(color: Colors.white), @@ -1612,7 +1806,7 @@ class _GroupRideDetailScreenState extends ConsumerState { ), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.redAccent, + backgroundColor: colors.raceRed, foregroundColor: Colors.white, ), onPressed: () async { @@ -1634,11 +1828,19 @@ class _GroupRideDetailScreenState extends ConsumerState { ); } - void _confirmLeaveRide(GroupRide ride, String currentUid) { + void _confirmLeaveRide( + GroupRide ride, + String currentUid, + MotorSignalThemeColors colors, + ) { showDialog( context: context, builder: (ctx) => AlertDialog( - backgroundColor: const Color(0xFF1B1E28), + backgroundColor: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: colors.outlineVariant), + ), title: const Text( 'Leave Group Ride?', style: TextStyle(color: Colors.white), @@ -1654,7 +1856,7 @@ class _GroupRideDetailScreenState extends ConsumerState { ), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.redAccent, + backgroundColor: colors.raceRed, foregroundColor: Colors.white, ), onPressed: () async { @@ -1677,33 +1879,37 @@ class _GroupRideDetailScreenState extends ConsumerState { return '$hour:$minute $period'; } - Widget _buildOrganizerControlCard(BuildContext context, GroupRide ride) { + Widget _buildOrganizerControlCard( + BuildContext context, + GroupRide ride, + MotorSignalThemeColors colors, + ) { final service = ref.read(groupRideServiceProvider); final currentUid = ref.read(currentUserProvider)?.uid; return Card( - color: const Color(0xFF1B1E28), + color: colors.surfaceContainerHigh, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: const BorderSide(color: Color(0xFF00E5FF), width: 0.8), + side: BorderSide(color: colors.cyberCyan, width: 0.8), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Row( + Row( children: [ Icon( Icons.admin_panel_settings, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, size: 18, ), - SizedBox(width: 8), + const SizedBox(width: 8), Text( 'ORGANIZER CONTROLS', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontWeight: FontWeight.bold, letterSpacing: 1.0, fontSize: 12, @@ -1724,11 +1930,9 @@ class _GroupRideDetailScreenState extends ConsumerState { if (ride.status == GroupRideStatus.scheduled) ...[ ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color( - 0xFF00E5FF, - ).withValues(alpha: 0.2), - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), + backgroundColor: colors.cyberCyan.withValues(alpha: 0.2), + foregroundColor: colors.cyberCyan, + side: BorderSide(color: colors.cyberCyan), ), icon: const Icon(Icons.group, size: 16), label: const Text('START ASSEMBLING'), @@ -1746,7 +1950,7 @@ class _GroupRideDetailScreenState extends ConsumerState { ), ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.two_wheeler, size: 16), @@ -1770,7 +1974,7 @@ class _GroupRideDetailScreenState extends ConsumerState { if (ride.status == GroupRideStatus.assembling) ...[ ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.two_wheeler, size: 16), @@ -1794,11 +1998,11 @@ class _GroupRideDetailScreenState extends ConsumerState { if (ride.status == GroupRideStatus.underway) ...[ ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: Colors.greenAccent.withValues( + backgroundColor: colors.launchGreen.withValues( alpha: 0.2, ), - foregroundColor: Colors.greenAccent, - side: const BorderSide(color: Colors.greenAccent), + foregroundColor: colors.launchGreen, + side: BorderSide(color: colors.launchGreen), ), icon: const Icon(Icons.flag, size: 16), label: const Text( @@ -1831,6 +2035,7 @@ class _GroupRideDetailScreenState extends ConsumerState { GroupRide ride, GroupRideParticipant myParticipant, String? currentUid, + MotorSignalThemeColors colors, ) { if (currentUid == null) return const SizedBox.shrink(); @@ -1841,11 +2046,11 @@ class _GroupRideDetailScreenState extends ConsumerState { final service = ref.read(groupRideServiceProvider); return Card( - color: const Color(0xFF161922), + color: colors.surfaceContainerHigh, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( - color: isCurrentlySharing ? const Color(0xFF00E5FF) : Colors.white12, + color: isCurrentlySharing ? colors.cyberCyan : colors.outlineVariant, width: isCurrentlySharing ? 1.2 : 0.8, ), ), @@ -1859,15 +2064,15 @@ class _GroupRideDetailScreenState extends ConsumerState { Icon( Icons.radar, color: isCurrentlySharing - ? const Color(0xFF00E5FF) + ? colors.cyberCyan : Colors.white70, size: 20, ), const SizedBox(width: 8), - const Text( + Text( 'LIVE RIDE PRESENCE', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontWeight: FontWeight.bold, letterSpacing: 1.0, fontSize: 12, @@ -1881,18 +2086,18 @@ class _GroupRideDetailScreenState extends ConsumerState { vertical: 3, ), decoration: BoxDecoration( - color: const Color(0xFF00E5FF).withValues(alpha: 0.15), + color: colors.cyberCyan.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), - child: const Row( + child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.circle, color: Color(0xFF00E5FF), size: 8), - SizedBox(width: 4), + Icon(Icons.circle, color: colors.cyberCyan, size: 8), + const SizedBox(width: 4), Text( 'BROADCASTING', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontSize: 10, fontWeight: FontWeight.bold, ), @@ -1907,25 +2112,28 @@ class _GroupRideDetailScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.amber.withValues(alpha: 0.1), + color: colors.warningAmber.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), border: Border.all( - color: Colors.amber.withValues(alpha: 0.4), + color: colors.warningAmber.withValues(alpha: 0.4), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Row( + Row( children: [ - Icon(Icons.lock_outline, color: Colors.amber, size: 16), - SizedBox(width: 6), - Text( - 'Master Location Privacy is OFF', - style: TextStyle( - color: Colors.amber, - fontWeight: FontWeight.bold, - fontSize: 12, + Icon(Icons.lock_outline, + color: colors.warningAmber, size: 16), + const SizedBox(width: 6), + Expanded( + child: Text( + 'Master Location Privacy is OFF', + style: TextStyle( + color: colors.warningAmber, + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), ), ], @@ -1938,7 +2146,7 @@ class _GroupRideDetailScreenState extends ConsumerState { const SizedBox(height: 8), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.amber, + backgroundColor: colors.warningAmber, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric( horizontal: 14, @@ -1982,13 +2190,13 @@ class _GroupRideDetailScreenState extends ConsumerState { : 'Disabled — other riders cannot see your location', style: TextStyle( color: isCurrentlySharing - ? const Color(0xFF00E5FF) + ? colors.cyberCyan : Colors.white38, fontSize: 11, ), ), value: isCurrentlySharing, - activeThumbColor: const Color(0xFF00E5FF), + activeThumbColor: colors.cyberCyan, onChanged: (val) async { if (val) { ref.read(activeLiveRideSharingIdProvider.notifier).state = @@ -2017,8 +2225,8 @@ class _GroupRideDetailScreenState extends ConsumerState { width: double.infinity, child: OutlinedButton.icon( style: OutlinedButton.styleFrom( - foregroundColor: Colors.redAccent, - side: const BorderSide(color: Colors.redAccent), + foregroundColor: colors.raceRed, + side: BorderSide(color: colors.raceRed), padding: const EdgeInsets.symmetric(vertical: 10), ), icon: const Icon(Icons.location_off, size: 16), @@ -2048,7 +2256,7 @@ class _GroupRideDetailScreenState extends ConsumerState { child: ElevatedButton.icon( key: const ValueKey('open_live_group_ride_map_button'), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( @@ -2099,8 +2307,8 @@ class _GroupRideDetailScreenState extends ConsumerState { Icon( p.isFresh ? Icons.circle : Icons.circle_outlined, color: p.isFresh - ? Colors.greenAccent - : Colors.amber, + ? colors.launchGreen + : colors.warningAmber, size: 10, ), const SizedBox(width: 8), @@ -2111,7 +2319,7 @@ class _GroupRideDetailScreenState extends ConsumerState { : 'Rider (${p.status.label})', style: TextStyle( color: p.userId == currentUid - ? const Color(0xFF00E5FF) + ? colors.cyberCyan : Colors.white, fontSize: 12, fontWeight: p.userId == currentUid @@ -2126,19 +2334,18 @@ class _GroupRideDetailScreenState extends ConsumerState { vertical: 2, ), decoration: BoxDecoration( - color: - (p.isFresh - ? Colors.greenAccent - : Colors.amber) - .withValues(alpha: 0.15), + color: (p.isFresh + ? colors.launchGreen + : colors.warningAmber) + .withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), child: Text( p.isFresh ? 'FRESH' : 'STALE', style: TextStyle( color: p.isFresh - ? Colors.greenAccent - : Colors.amber, + ? colors.launchGreen + : colors.warningAmber, fontSize: 9, fontWeight: FontWeight.w900, ), @@ -2150,7 +2357,7 @@ class _GroupRideDetailScreenState extends ConsumerState { ], ); }, - loading: () => const SizedBox( + loading: () => SizedBox( height: 24, child: Center( child: SizedBox( @@ -2158,7 +2365,7 @@ class _GroupRideDetailScreenState extends ConsumerState { height: 14, child: CircularProgressIndicator( strokeWidth: 2, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), ), ), diff --git a/lib/features/group_rides/group_rides_hub_screen.dart b/lib/features/group_rides/group_rides_hub_screen.dart index 32b97a4..53f29a2 100644 --- a/lib/features/group_rides/group_rides_hub_screen.dart +++ b/lib/features/group_rides/group_rides_hub_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/responsive/viewport_size.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../services/auth/auth_providers.dart'; import '../../services/group_rides/group_ride_models.dart'; import '../../services/group_rides/group_ride_providers.dart'; @@ -31,19 +34,20 @@ class _GroupRidesHubScreenState extends ConsumerState @override Widget build(BuildContext context) { + final colors = context.motorColors; final user = ref.watch(currentUserProvider); final isAuthed = user != null && !user.isAnonymous; return Scaffold( - backgroundColor: const Color(0xFF0D0E11), + backgroundColor: colors.surfaceContainer, appBar: AppBar( - backgroundColor: const Color(0xFF13151A), + backgroundColor: colors.surfaceContainerHigh, elevation: 0, - title: const Row( + title: Row( children: [ - Icon(Icons.two_wheeler, color: Color(0xFF00E5FF), size: 24), - SizedBox(width: 10), - Text( + Icon(Icons.two_wheeler, color: colors.cyberCyan, size: 24), + const SizedBox(width: 10), + const Text( 'GROUP RIDES', style: TextStyle( fontWeight: FontWeight.w900, @@ -55,9 +59,9 @@ class _GroupRidesHubScreenState extends ConsumerState ), bottom: TabBar( controller: _tabController, - indicatorColor: const Color(0xFF00E5FF), + indicatorColor: colors.cyberCyan, indicatorWeight: 3, - labelColor: const Color(0xFF00E5FF), + labelColor: colors.cyberCyan, unselectedLabelColor: Colors.white60, labelStyle: const TextStyle( fontWeight: FontWeight.bold, @@ -73,25 +77,30 @@ class _GroupRidesHubScreenState extends ConsumerState if (isAuthed) IconButton( key: const ValueKey('group_rides_create_appbar_button'), - icon: const Icon(Icons.add, color: Color(0xFF00E5FF)), + icon: Icon(Icons.add, color: colors.cyberCyan), tooltip: 'Create Group Ride', onPressed: () => context.push('/group-rides/create'), ), ], ), - body: !isAuthed - ? _buildAccountRequiredNotice(context) - : TabBarView( - controller: _tabController, - children: [ - _buildUpcomingTab(context, ref, user.uid), - _buildMyRidesTab(context, ref, user.uid), - ], - ), + body: ResponsiveLayout( + builder: (context, snapshot) { + if (!isAuthed) { + return _buildAccountRequiredNotice(context, colors); + } + return TabBarView( + controller: _tabController, + children: [ + _buildUpcomingTab(context, ref, user.uid, snapshot, colors), + _buildMyRidesTab(context, ref, user.uid, snapshot, colors), + ], + ); + }, + ), floatingActionButton: isAuthed ? FloatingActionButton.extended( key: const ValueKey('create_group_ride_fab'), - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, icon: const Icon(Icons.add_road), label: const Text( @@ -104,9 +113,12 @@ class _GroupRidesHubScreenState extends ConsumerState ); } - Widget _buildAccountRequiredNotice(BuildContext context) { + Widget _buildAccountRequiredNotice( + BuildContext context, + MotorSignalThemeColors colors, + ) { return Center( - child: Padding( + child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -115,12 +127,12 @@ class _GroupRidesHubScreenState extends ConsumerState padding: const EdgeInsets.all(20), decoration: BoxDecoration( shape: BoxShape.circle, - color: const Color(0xFF00E5FF).withValues(alpha: 0.1), + color: colors.cyberCyan.withValues(alpha: 0.1), ), - child: const Icon( + child: Icon( Icons.groups_outlined, size: 64, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), ), const SizedBox(height: 20), @@ -145,7 +157,7 @@ class _GroupRidesHubScreenState extends ConsumerState const SizedBox(height: 24), ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric( horizontal: 24, @@ -172,6 +184,8 @@ class _GroupRidesHubScreenState extends ConsumerState BuildContext context, WidgetRef ref, String currentUid, + ResponsiveSnapshot snapshot, + MotorSignalThemeColors colors, ) { final ridesAsync = ref.watch(upcomingGroupRidesProvider); @@ -179,6 +193,7 @@ class _GroupRidesHubScreenState extends ConsumerState data: (rides) { if (rides.isEmpty) { return _buildEmptyState( + colors: colors, icon: Icons.map_outlined, title: 'No Upcoming Group Rides', message: @@ -188,22 +203,43 @@ class _GroupRidesHubScreenState extends ConsumerState ); } + final isWide = snapshot.sizeClass == WindowSizeClass.expanded || + (snapshot.isLandscape && snapshot.width >= 720); + + if (isWide) { + final crossAxisCount = snapshot.width >= 1100 ? 3 : 2; + return GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 185, + ), + itemCount: rides.length, + itemBuilder: (context, index) { + final ride = rides[index]; + return _buildRideCard(context, ride, currentUid, colors); + }, + ); + } + return ListView.builder( padding: const EdgeInsets.all(16), itemCount: rides.length, itemBuilder: (context, index) { final ride = rides[index]; - return _buildRideCard(context, ride, currentUid); + return _buildRideCard(context, ride, currentUid, colors); }, ); }, - loading: () => const Center( - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + loading: () => Center( + child: CircularProgressIndicator(color: colors.cyberCyan), ), error: (e, _) => Center( child: Text( 'Error loading rides: $e', - style: const TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), ), ); @@ -213,6 +249,8 @@ class _GroupRidesHubScreenState extends ConsumerState BuildContext context, WidgetRef ref, String currentUid, + ResponsiveSnapshot snapshot, + MotorSignalThemeColors colors, ) { final ridesAsync = ref.watch(upcomingGroupRidesProvider); @@ -224,6 +262,7 @@ class _GroupRidesHubScreenState extends ConsumerState if (myRides.isEmpty) { return _buildEmptyState( + colors: colors, icon: Icons.sports_motorsports_outlined, title: 'You Haven\'t Created Any Rides', message: @@ -233,28 +272,50 @@ class _GroupRidesHubScreenState extends ConsumerState ); } + final isWide = snapshot.sizeClass == WindowSizeClass.expanded || + (snapshot.isLandscape && snapshot.width >= 720); + + if (isWide) { + final crossAxisCount = snapshot.width >= 1100 ? 3 : 2; + return GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 185, + ), + itemCount: myRides.length, + itemBuilder: (context, index) { + final ride = myRides[index]; + return _buildRideCard(context, ride, currentUid, colors); + }, + ); + } + return ListView.builder( padding: const EdgeInsets.all(16), itemCount: myRides.length, itemBuilder: (context, index) { final ride = myRides[index]; - return _buildRideCard(context, ride, currentUid); + return _buildRideCard(context, ride, currentUid, colors); }, ); }, - loading: () => const Center( - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + loading: () => Center( + child: CircularProgressIndicator(color: colors.cyberCyan), ), error: (e, _) => Center( child: Text( 'Error: $e', - style: const TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), ), ); } Widget _buildEmptyState({ + required MotorSignalThemeColors colors, required IconData icon, required String title, required String message, @@ -286,8 +347,8 @@ class _GroupRidesHubScreenState extends ConsumerState const SizedBox(height: 20), OutlinedButton( style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), + foregroundColor: colors.cyberCyan, + side: BorderSide(color: colors.cyberCyan), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -305,18 +366,19 @@ class _GroupRidesHubScreenState extends ConsumerState BuildContext context, GroupRide ride, String currentUid, + MotorSignalThemeColors colors, ) { final isOrganizer = ride.organizerUid == currentUid; return Card( margin: const EdgeInsets.only(bottom: 12), - color: const Color(0xFF161922), + color: colors.surfaceContainerHigh, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: isOrganizer - ? const Color(0xFF00E5FF).withValues(alpha: 0.4) - : Colors.white12, + ? colors.cyberCyan.withValues(alpha: 0.4) + : colors.outlineVariant, ), ), child: InkWell( @@ -337,8 +399,8 @@ class _GroupRidesHubScreenState extends ConsumerState ), decoration: BoxDecoration( color: ride.isCanceled - ? Colors.redAccent.withValues(alpha: 0.2) - : const Color(0xFF00E5FF).withValues(alpha: 0.15), + ? colors.raceRed.withValues(alpha: 0.2) + : colors.cyberCyan.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: Text( @@ -347,8 +409,8 @@ class _GroupRidesHubScreenState extends ConsumerState fontSize: 10, fontWeight: FontWeight.w900, color: ride.isCanceled - ? Colors.redAccent - : const Color(0xFF00E5FF), + ? colors.raceRed + : colors.cyberCyan, ), ), ), @@ -360,15 +422,15 @@ class _GroupRidesHubScreenState extends ConsumerState vertical: 2, ), decoration: BoxDecoration( - color: Colors.amber.withValues(alpha: 0.2), + color: colors.warningAmber.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), - child: const Text( + child: Text( 'ORGANIZER', style: TextStyle( fontSize: 9, fontWeight: FontWeight.bold, - color: Colors.amber, + color: colors.warningAmber, ), ), ), @@ -396,6 +458,8 @@ class _GroupRidesHubScreenState extends ConsumerState const SizedBox(height: 10), Text( ride.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 17, fontWeight: FontWeight.bold, @@ -405,18 +469,21 @@ class _GroupRidesHubScreenState extends ConsumerState const SizedBox(height: 6), Row( children: [ - const Icon( + Icon( Icons.schedule, size: 14, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), const SizedBox(width: 6), - Text( - 'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}', - style: const TextStyle( - fontSize: 12, - color: Colors.white70, - fontWeight: FontWeight.w500, + Expanded( + child: Text( + 'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}', + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + color: Colors.white70, + fontWeight: FontWeight.w500, + ), ), ), ], @@ -446,10 +513,10 @@ class _GroupRidesHubScreenState extends ConsumerState const SizedBox(height: 4), Row( children: [ - const Icon( + Icon( Icons.flag_outlined, size: 14, - color: Color(0xFFFFB300), + color: colors.warningAmber, ), const SizedBox(width: 6), Expanded( diff --git a/lib/features/group_rides/live_group_ride_screen.dart b/lib/features/group_rides/live_group_ride_screen.dart index 1574264..6ddf64e 100644 --- a/lib/features/group_rides/live_group_ride_screen.dart +++ b/lib/features/group_rides/live_group_ride_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:go_router/go_router.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../services/auth/auth_providers.dart'; import '../../services/group_rides/group_ride_models.dart'; import '../../services/group_rides/group_ride_providers.dart'; @@ -48,6 +50,7 @@ class _LiveGroupRideScreenState extends ConsumerState @override Widget build(BuildContext context) { + final colors = context.motorColors; final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId)); final participantsAsync = ref.watch( groupRideParticipantsProvider(widget.rideId), @@ -68,9 +71,9 @@ class _LiveGroupRideScreenState extends ConsumerState data: (ride) { if (ride == null) { return Scaffold( - backgroundColor: const Color(0xFF0D0E11), + backgroundColor: colors.surfaceContainer, appBar: AppBar( - backgroundColor: const Color(0xFF13151A), + backgroundColor: colors.surfaceContainerHigh, title: const Text('LIVE GROUP RIDE'), ), body: const Center( @@ -88,9 +91,9 @@ class _LiveGroupRideScreenState extends ConsumerState // Check if ride was closed if (ride.isCompleted || ride.isCanceled) { return Scaffold( - backgroundColor: const Color(0xFF0D0E11), + backgroundColor: colors.surfaceContainer, appBar: AppBar( - backgroundColor: const Color(0xFF13151A), + backgroundColor: colors.surfaceContainerHigh, title: Text( ride.title.toUpperCase(), style: const TextStyle( @@ -109,8 +112,8 @@ class _LiveGroupRideScreenState extends ConsumerState Icon( ride.isCompleted ? Icons.flag : Icons.cancel, color: ride.isCompleted - ? Colors.greenAccent - : Colors.redAccent, + ? colors.launchGreen + : colors.raceRed, size: 48, ), const SizedBox(height: 16), @@ -128,7 +131,7 @@ class _LiveGroupRideScreenState extends ConsumerState const SizedBox(height: 24), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), onPressed: () => context.pop(), @@ -202,7 +205,7 @@ class _LiveGroupRideScreenState extends ConsumerState points: attachedRoute.previewPoints .map((p) => AppLatLng(p.latitude, p.longitude)) .toList(), - color: const Color(0xFF00E5FF).withValues(alpha: 0.85), + color: colors.cyberCyan.withValues(alpha: 0.85), width: 3.5, ), ); @@ -217,10 +220,11 @@ class _LiveGroupRideScreenState extends ConsumerState currentUid: currentUid, leadUid: leadParticipant?.userId, attachedRoute: attachedRoute, + colors: colors, ); return Scaffold( - backgroundColor: const Color(0xFF0D0E11), + backgroundColor: colors.surfaceContainer, appBar: _buildAppBar( context: context, ride: ride, @@ -229,137 +233,153 @@ class _LiveGroupRideScreenState extends ConsumerState participants: acceptedParticipants, presences: presences, leadPresence: leadPresence, + colors: colors, ), - body: Stack( - children: [ - // 1. Unified Map - Positioned.fill( - child: UnifiedMapView( - center: center, - zoom: _mapZoom, - markers: markers, - polylines: polylines, - onPositionChanged: (pos) { - _customMapCenter = pos; - }, - ), - ), + body: ResponsiveLayout( + builder: (context, snapshot) { + final isShort = snapshot.height < 450; + final isLandscapeWide = + snapshot.isLandscape && snapshot.width >= 720; - // 2. Top Banners (Regroup, Master Privacy) - Positioned( - top: 8, - left: 8, - right: 8, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (regroupCaller != null) - _buildRegroupBanner( - context: context, - caller: regroupCaller, - participants: acceptedParticipants, - isLeadOrOrganizer: - isOrganizer || - (leadParticipant != null && - leadParticipant.userId == currentUid), - controller: broadcastController, - ), - if (!broadcastState.isMasterAllowed) - _buildMasterPrivacyBanner(context, ref), - ], - ), - ), - - // 3. Quick Status Bar & Broadcast Floating Control - Positioned( - bottom: 12, - left: 8, - right: 8, - child: SafeArea( - top: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Quick Status Selector - _buildQuickStatusBar( - currentStatus: broadcastState.status, - isBroadcasting: broadcastState.isBroadcasting, - onStatusSelected: (s) => - broadcastController.setStatus(s), - ), - const SizedBox(height: 8), - - // Bottom Control Strip (Opt-in toggle, Roster opener, Recenter) - _buildBottomControlStrip( - context: context, - ride: ride, - broadcastState: broadcastState, - broadcastController: broadcastController, - participants: acceptedParticipants, - presences: presences, - leadPresence: leadPresence, - currentUid: currentUid, - ), - ], + return Stack( + children: [ + // 1. Unified Map + Positioned.fill( + child: UnifiedMapView( + center: center, + zoom: _mapZoom, + markers: markers, + polylines: polylines, + onPositionChanged: (pos) { + _customMapCenter = pos; + }, + ), ), - ), - ), - ], + + // 2. Top Banners (Regroup, Master Privacy) + Positioned( + top: 8, + left: 8, + right: isLandscapeWide ? snapshot.width * 0.35 : 8, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (regroupCaller != null) + _buildRegroupBanner( + context: context, + caller: regroupCaller, + participants: acceptedParticipants, + isLeadOrOrganizer: + isOrganizer || + (leadParticipant != null && + leadParticipant.userId == + currentUid), + controller: broadcastController, + colors: colors, + ), + if (!broadcastState.isMasterAllowed) + _buildMasterPrivacyBanner( + context, ref, colors), + ], + ), + ), + + // 3. Quick Status Bar & Broadcast Floating Control + Positioned( + bottom: isShort ? 6 : 12, + left: 8, + right: 8, + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Quick Status Selector + _buildQuickStatusBar( + currentStatus: broadcastState.status, + isBroadcasting: + broadcastState.isBroadcasting, + onStatusSelected: (s) => + broadcastController.setStatus(s), + colors: colors, + isShort: isShort, + ), + SizedBox(height: isShort ? 6 : 8), + + // Bottom Control Strip (Opt-in toggle, Roster opener, Recenter) + _buildBottomControlStrip( + context: context, + ride: ride, + broadcastState: broadcastState, + broadcastController: broadcastController, + participants: acceptedParticipants, + presences: presences, + leadPresence: leadPresence, + currentUid: currentUid, + colors: colors, + isShort: isShort, + ), + ], + ), + ), + ), + ], + ); + }, ), ); }, - loading: () => const Scaffold( - backgroundColor: Color(0xFF0D0E11), + loading: () => Scaffold( + backgroundColor: colors.surfaceContainer, body: Center( - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + child: CircularProgressIndicator(color: colors.cyberCyan), ), ), error: (e, _) => Scaffold( - backgroundColor: const Color(0xFF0D0E11), + backgroundColor: colors.surfaceContainer, body: Center( child: Text( 'Error loading live presence: $e', - style: const TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), ), ), ); }, - loading: () => const Scaffold( - backgroundColor: Color(0xFF0D0E11), + loading: () => Scaffold( + backgroundColor: colors.surfaceContainer, body: Center( - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + child: CircularProgressIndicator(color: colors.cyberCyan), ), ), error: (e, _) => Scaffold( - backgroundColor: const Color(0xFF0D0E11), + backgroundColor: colors.surfaceContainer, body: Center( child: Text( 'Error loading participants: $e', - style: const TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), ), ), ); }, - loading: () => const Scaffold( - backgroundColor: Color(0xFF0D0E11), + loading: () => Scaffold( + backgroundColor: colors.surfaceContainer, body: Center( - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + child: CircularProgressIndicator(color: colors.cyberCyan), ), ), error: (e, _) => Scaffold( - backgroundColor: const Color(0xFF0D0E11), + backgroundColor: colors.surfaceContainer, body: Center( child: Text( 'Error loading ride: $e', - style: const TextStyle(color: Colors.redAccent), + style: TextStyle(color: colors.raceRed), ), ), ), ); } - PreferredSizeWidget _buildAppBar({ required BuildContext context, required GroupRide ride, @@ -368,9 +388,10 @@ class _LiveGroupRideScreenState extends ConsumerState required List participants, required List presences, required LiveGroupPresence? leadPresence, + required MotorSignalThemeColors colors, }) { return AppBar( - backgroundColor: const Color(0xFF13151A), + backgroundColor: colors.surfaceContainerHigh, elevation: 3, leading: IconButton( icon: const Icon(Icons.arrow_back), @@ -399,8 +420,8 @@ class _LiveGroupRideScreenState extends ConsumerState padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: ride.status == GroupRideStatus.underway - ? const Color(0xFF00E5FF).withValues(alpha: 0.2) - : Colors.amber.withValues(alpha: 0.2), + ? colors.cyberCyan.withValues(alpha: 0.2) + : colors.warningAmber.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Text( @@ -409,8 +430,8 @@ class _LiveGroupRideScreenState extends ConsumerState fontSize: 9, fontWeight: FontWeight.w900, color: ride.status == GroupRideStatus.underway - ? const Color(0xFF00E5FF) - : Colors.amber, + ? colors.cyberCyan + : colors.warningAmber, ), ), ), @@ -425,15 +446,15 @@ class _LiveGroupRideScreenState extends ConsumerState actions: [ if (isOrganizer) IconButton( - icon: const Icon( + icon: Icon( Icons.stop_circle_outlined, - color: Colors.redAccent, + color: colors.raceRed, ), tooltip: 'End Ride', - onPressed: () => _confirmEndRide(context, ride, currentUid), + onPressed: () => _confirmEndRide(context, ride, currentUid, colors), ), IconButton( - icon: const Icon(Icons.people_outline, color: Color(0xFF00E5FF)), + icon: Icon(Icons.people_outline, color: colors.cyberCyan), tooltip: 'Roster', onPressed: () => _showRosterSheet( context: context, @@ -441,6 +462,7 @@ class _LiveGroupRideScreenState extends ConsumerState presences: presences, leadPresence: leadPresence, currentUid: currentUid, + colors: colors, ), ), ], @@ -453,6 +475,7 @@ class _LiveGroupRideScreenState extends ConsumerState required List participants, required bool isLeadOrOrganizer, required LiveBroadcastController controller, + required MotorSignalThemeColors colors, }) { String callerName = caller.userId; try { @@ -466,7 +489,7 @@ class _LiveGroupRideScreenState extends ConsumerState margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: Colors.amber.withValues(alpha: 0.92), + color: colors.warningAmber.withValues(alpha: 0.92), borderRadius: BorderRadius.circular(8), boxShadow: const [ BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)), @@ -536,18 +559,19 @@ class _LiveGroupRideScreenState extends ConsumerState ); } - Widget _buildMasterPrivacyBanner(BuildContext context, WidgetRef ref) { + Widget _buildMasterPrivacyBanner( + BuildContext context, WidgetRef ref, MotorSignalThemeColors colors) { return Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: const Color(0xFF1E222D), + color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.amber.withValues(alpha: 0.6)), + border: Border.all(color: colors.warningAmber.withValues(alpha: 0.6)), ), child: Row( children: [ - const Icon(Icons.lock_outline, color: Colors.amber, size: 18), + Icon(Icons.lock_outline, color: colors.warningAmber, size: 18), const SizedBox(width: 8), const Expanded( child: Text( @@ -558,7 +582,7 @@ class _LiveGroupRideScreenState extends ConsumerState const SizedBox(width: 6), TextButton( style: TextButton.styleFrom( - backgroundColor: Colors.amber, + backgroundColor: colors.warningAmber, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), minimumSize: Size.zero, @@ -581,6 +605,8 @@ class _LiveGroupRideScreenState extends ConsumerState required LivePresenceStatus currentStatus, required bool isBroadcasting, required ValueChanged onStatusSelected, + required MotorSignalThemeColors colors, + bool isShort = false, }) { final statuses = [ (LivePresenceStatus.riding, 'RIDING', Icons.two_wheeler), @@ -592,11 +618,11 @@ class _LiveGroupRideScreenState extends ConsumerState ]; return Container( - height: 46, + height: isShort ? 40 : 46, decoration: BoxDecoration( - color: const Color(0xFF161922).withValues(alpha: 0.95), + color: colors.surfaceContainerHigh.withValues(alpha: 0.95), borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.white12), + border: Border.all(color: colors.outlineVariant), boxShadow: const [ BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)), ], @@ -610,13 +636,13 @@ class _LiveGroupRideScreenState extends ConsumerState final (status, label, icon) = statuses[index]; final isSelected = currentStatus == status; - Color activeColor = const Color(0xFF00E5FF); + Color activeColor = colors.cyberCyan; if (status == LivePresenceStatus.needFuel) activeColor = Colors.orangeAccent; if (status == LivePresenceStatus.regrouping) - activeColor = Colors.amber; + activeColor = colors.warningAmber; if (status == LivePresenceStatus.arrived) - activeColor = Colors.greenAccent; + activeColor = colors.launchGreen; return InkWell( borderRadius: BorderRadius.circular(6), @@ -670,13 +696,15 @@ class _LiveGroupRideScreenState extends ConsumerState required List presences, required LiveGroupPresence? leadPresence, required String? currentUid, + required MotorSignalThemeColors colors, + bool isShort = false, }) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + padding: EdgeInsets.symmetric(horizontal: 8, vertical: isShort ? 4 : 6), decoration: BoxDecoration( - color: const Color(0xFF161922).withValues(alpha: 0.95), + color: colors.surfaceContainerHigh.withValues(alpha: 0.95), borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.white12), + border: Border.all(color: colors.outlineVariant), boxShadow: const [ BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)), ], @@ -692,7 +720,7 @@ class _LiveGroupRideScreenState extends ConsumerState ? Icons.radar : Icons.location_off, color: broadcastState.isBroadcasting - ? const Color(0xFF00E5FF) + ? colors.cyberCyan : Colors.white38, size: 20, ), @@ -714,8 +742,8 @@ class _LiveGroupRideScreenState extends ConsumerState style: TextStyle( color: broadcastState.isBroadcasting ? (broadcastState.isPaused - ? Colors.amber - : const Color(0xFF00E5FF)) + ? colors.warningAmber + : colors.cyberCyan) : Colors.white60, fontWeight: FontWeight.bold, fontSize: 11, @@ -743,7 +771,7 @@ class _LiveGroupRideScreenState extends ConsumerState key: const ValueKey('live_ride_broadcast_switch'), value: broadcastState.isOptedIn, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - activeThumbColor: const Color(0xFF00E5FF), + activeThumbColor: colors.cyberCyan, onChanged: (val) => broadcastController.setOptIn(val), ), ], @@ -752,18 +780,18 @@ class _LiveGroupRideScreenState extends ConsumerState const SizedBox(width: 8), - // Recenter Map Button + // Recenter Map Button (min 48x48 tap target) IconButton( key: const ValueKey('live_ride_recenter_button'), style: IconButton.styleFrom( - backgroundColor: Colors.white10, + backgroundColor: colors.surfaceContainer, padding: const EdgeInsets.all(8), - minimumSize: const Size(36, 36), + minimumSize: const Size(48, 48), ), icon: const Icon( Icons.my_location, color: Colors.white70, - size: 18, + size: 20, ), tooltip: 'Center on Me', onPressed: () { @@ -792,13 +820,13 @@ class _LiveGroupRideScreenState extends ConsumerState ElevatedButton.icon( key: const ValueKey('live_ride_roster_button'), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15), - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), + backgroundColor: colors.cyberCyan.withValues(alpha: 0.15), + foregroundColor: colors.cyberCyan, + side: BorderSide(color: colors.cyberCyan), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - minimumSize: Size.zero, + minimumSize: const Size(48, 48), ), - icon: const Icon(Icons.format_list_bulleted, size: 15), + icon: const Icon(Icons.format_list_bulleted, size: 16), label: Text( '${participants.length}', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), @@ -809,6 +837,7 @@ class _LiveGroupRideScreenState extends ConsumerState presences: presences, leadPresence: leadPresence, currentUid: currentUid, + colors: colors, ), ), ], @@ -822,10 +851,11 @@ class _LiveGroupRideScreenState extends ConsumerState required List presences, required LiveGroupPresence? leadPresence, required String? currentUid, + required MotorSignalThemeColors colors, }) { showModalBottomSheet( context: context, - backgroundColor: const Color(0xFF161922), + backgroundColor: colors.surfaceContainerHigh, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), @@ -835,12 +865,12 @@ class _LiveGroupRideScreenState extends ConsumerState children: [ Row( children: [ - const Icon(Icons.people, color: Color(0xFF00E5FF), size: 18), + Icon(Icons.people, color: colors.cyberCyan, size: 18), const SizedBox(width: 8), - const Text( + Text( 'LIVE RIDER ROSTER', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontWeight: FontWeight.bold, letterSpacing: 1.0, fontSize: 13, @@ -853,13 +883,14 @@ class _LiveGroupRideScreenState extends ConsumerState ), ], ), - const Divider(height: 20, color: Colors.white12), + Divider(height: 20, color: colors.outlineVariant), for (final p in participants) ...[ _buildRosterItem( participant: p, presence: _findPresence(presences, p.userId), leadPresence: leadPresence, isCaller: p.userId == currentUid, + colors: colors, onCenter: (pos) { Navigator.pop(ctx); setState(() { @@ -881,6 +912,7 @@ class _LiveGroupRideScreenState extends ConsumerState required LiveGroupPresence? presence, required LiveGroupPresence? leadPresence, required bool isCaller, + required MotorSignalThemeColors colors, required ValueChanged onCenter, }) { // Freshness @@ -910,12 +942,12 @@ class _LiveGroupRideScreenState extends ConsumerState return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: const Color(0xFF1B1E28), + color: colors.surfaceContainer, borderRadius: BorderRadius.circular(8), border: Border.all( color: isCaller - ? const Color(0xFF00E5FF).withValues(alpha: 0.5) - : Colors.white10, + ? colors.cyberCyan.withValues(alpha: 0.5) + : colors.outlineVariant, ), ), child: Row( @@ -925,7 +957,7 @@ class _LiveGroupRideScreenState extends ConsumerState children: [ CircleAvatar( radius: 18, - backgroundColor: Colors.white10, + backgroundColor: colors.surfaceContainerHigh, child: Text( participant.riderDisplayName.isNotEmpty ? participant.riderDisplayName[0].toUpperCase() @@ -937,12 +969,12 @@ class _LiveGroupRideScreenState extends ConsumerState ), ), if (isLive) - const Positioned( + Positioned( bottom: 0, right: 0, child: CircleAvatar( radius: 5, - backgroundColor: Colors.greenAccent, + backgroundColor: colors.launchGreen, ), ), ], @@ -965,7 +997,7 @@ class _LiveGroupRideScreenState extends ConsumerState overflow: TextOverflow.ellipsis, style: TextStyle( color: isCaller - ? const Color(0xFF00E5FF) + ? colors.cyberCyan : Colors.white, fontWeight: FontWeight.bold, fontSize: 13, @@ -982,6 +1014,7 @@ class _LiveGroupRideScreenState extends ConsumerState decoration: BoxDecoration( color: _roleColor( participant.role, + colors, ).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(3), ), @@ -990,7 +1023,7 @@ class _LiveGroupRideScreenState extends ConsumerState style: TextStyle( fontSize: 9, fontWeight: FontWeight.bold, - color: _roleColor(participant.role), + color: _roleColor(participant.role, colors), ), ), ), @@ -1007,7 +1040,7 @@ class _LiveGroupRideScreenState extends ConsumerState style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, - color: _statusColor(presence.status), + color: _statusColor(presence.status, colors), ), ) else @@ -1041,18 +1074,18 @@ class _LiveGroupRideScreenState extends ConsumerState isLive ? 'LIVE' : '${presence.secondsAgo}s ago', style: TextStyle( color: isLive - ? Colors.greenAccent - : (isRecent ? Colors.amber : Colors.white38), + ? colors.launchGreen + : (isRecent ? colors.warningAmber : Colors.white38), fontSize: 10, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 6), IconButton( - icon: const Icon( + icon: Icon( Icons.gps_fixed, size: 16, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), constraints: const BoxConstraints(minWidth: 32, minHeight: 32), padding: EdgeInsets.zero, @@ -1070,12 +1103,12 @@ class _LiveGroupRideScreenState extends ConsumerState ); } - Color _roleColor(GroupRideRole role) { + Color _roleColor(GroupRideRole role, MotorSignalThemeColors colors) { switch (role) { case GroupRideRole.organizer: - return const Color(0xFF00E5FF); + return colors.cyberCyan; case GroupRideRole.lead: - return const Color(0xFFFFD700); + return colors.warningAmber; case GroupRideRole.sweep: return Colors.orangeAccent; case GroupRideRole.participant: @@ -1083,20 +1116,20 @@ class _LiveGroupRideScreenState extends ConsumerState } } - Color _statusColor(LivePresenceStatus status) { + Color _statusColor(LivePresenceStatus status, MotorSignalThemeColors colors) { switch (status) { case LivePresenceStatus.riding: - return Colors.greenAccent; + return colors.launchGreen; case LivePresenceStatus.stopped: return Colors.white70; case LivePresenceStatus.needFuel: return Colors.orangeAccent; case LivePresenceStatus.regrouping: - return Colors.amber; + return colors.warningAmber; case LivePresenceStatus.delayed: - return Colors.redAccent; + return colors.raceRed; case LivePresenceStatus.arrived: - return const Color(0xFF00E5FF); + return colors.cyberCyan; case LivePresenceStatus.offline: return Colors.white38; } @@ -1136,6 +1169,7 @@ class _LiveGroupRideScreenState extends ConsumerState required String? currentUid, required String? leadUid, MotorSignalRoute? attachedRoute, + required MotorSignalThemeColors colors, }) { final markers = []; @@ -1152,10 +1186,10 @@ class _LiveGroupRideScreenState extends ConsumerState : 'Meetup', iconWidget: Container( padding: const EdgeInsets.all(6), - decoration: const BoxDecoration( - color: Colors.greenAccent, + decoration: BoxDecoration( + color: colors.launchGreen, shape: BoxShape.circle, - boxShadow: [ + boxShadow: const [ BoxShadow( color: Colors.black45, blurRadius: 4, @@ -1186,10 +1220,10 @@ class _LiveGroupRideScreenState extends ConsumerState label: stop.displayName, iconWidget: Container( padding: const EdgeInsets.all(5), - decoration: const BoxDecoration( - color: Color(0xFF00E5FF), + decoration: BoxDecoration( + color: colors.cyberCyan, shape: BoxShape.circle, - boxShadow: [ + boxShadow: const [ BoxShadow( color: Colors.black45, blurRadius: 4, @@ -1276,10 +1310,10 @@ class _LiveGroupRideScreenState extends ConsumerState : 'Destination', iconWidget: Container( padding: const EdgeInsets.all(6), - decoration: const BoxDecoration( - color: Colors.redAccent, + decoration: BoxDecoration( + color: colors.raceRed, shape: BoxShape.circle, - boxShadow: [ + boxShadow: const [ BoxShadow( color: Colors.black45, blurRadius: 4, @@ -1310,11 +1344,11 @@ class _LiveGroupRideScreenState extends ConsumerState final isCaller = presence.userId == currentUid; // Color scheme - Color markerColor = const Color(0xFF00E5FF); - if (isLead) markerColor = const Color(0xFFFFD700); // Gold for lead + Color markerColor = colors.cyberCyan; + if (isLead) markerColor = colors.warningAmber; // Gold for lead if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep if (presence.status == LivePresenceStatus.regrouping) - markerColor = Colors.amber; + markerColor = colors.warningAmber; if (presence.status == LivePresenceStatus.needFuel) markerColor = Colors.deepOrangeAccent; @@ -1367,12 +1401,13 @@ class _LiveGroupRideScreenState extends ConsumerState BuildContext context, GroupRide ride, String? currentUid, + MotorSignalThemeColors colors, ) { if (currentUid == null) return; showDialog( context: context, builder: (ctx) => AlertDialog( - backgroundColor: const Color(0xFF161922), + backgroundColor: colors.surfaceContainerHigh, title: const Text( 'End Group Ride?', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), @@ -1391,7 +1426,7 @@ class _LiveGroupRideScreenState extends ConsumerState ), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.redAccent, + backgroundColor: colors.raceRed, foregroundColor: Colors.white, ), onPressed: () async { diff --git a/lib/features/social/friends_screen.dart b/lib/features/social/friends_screen.dart index 4451305..5535081 100644 --- a/lib/features/social/friends_screen.dart +++ b/lib/features/social/friends_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import '../../core/theme/evolution_theme_config.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/responsive/viewport_size.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../services/auth/auth_providers.dart'; import '../../services/social/rider_profile_models.dart'; import '../../services/social/social_providers.dart'; @@ -273,23 +275,18 @@ class _FriendsScreenState extends ConsumerState @override Widget build(BuildContext context) { + final colors = context.motorColors; final authState = ref.watch(authNotifierProvider); final isAuthed = authState.isAuthenticated; - final themeMode = ref.watch(dashboardThemeModeProvider); - final isEvolution = themeMode == DashboardThemeMode.evolution; - - final bgColor = isEvolution - ? Colors.black - : Theme.of(context).scaffoldBackgroundColor; - final titleFont = isEvolution ? 'Sora' : null; if (!isAuthed) { return Scaffold( - backgroundColor: bgColor, + backgroundColor: colors.surfaceContainer, appBar: AppBar( - title: Text( + backgroundColor: colors.surfaceContainerHigh, + title: const Text( 'Friends & Riders', - style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800), + style: TextStyle(fontWeight: FontWeight.w800), ), ), body: Center( @@ -298,16 +295,15 @@ class _FriendsScreenState extends ConsumerState child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.group_outlined, size: 64, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), const SizedBox(height: 16), - Text( + const Text( 'MOTORCLOUD ACCOUNT REQUIRED', style: TextStyle( - fontFamily: titleFont, fontWeight: FontWeight.w800, fontSize: 16, color: Colors.white, @@ -323,7 +319,7 @@ class _FriendsScreenState extends ConsumerState const SizedBox(height: 24), ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.login), @@ -345,11 +341,12 @@ class _FriendsScreenState extends ConsumerState final friendsCount = friendsAsync.value?.length ?? 0; return Scaffold( - backgroundColor: bgColor, + backgroundColor: colors.surfaceContainer, appBar: AppBar( - title: Text( + backgroundColor: colors.surfaceContainerHigh, + title: const Text( 'Friends & Riders', - style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800), + style: TextStyle(fontWeight: FontWeight.w800), ), actions: [ IconButton( @@ -360,8 +357,10 @@ class _FriendsScreenState extends ConsumerState ], bottom: TabBar( controller: _tabController, - indicatorColor: const Color(0xFF00E5FF), - labelColor: const Color(0xFF00E5FF), + isScrollable: true, + tabAlignment: TabAlignment.start, + indicatorColor: colors.cyberCyan, + labelColor: colors.cyberCyan, unselectedLabelColor: Colors.white60, tabs: [ Tab(text: 'FRIENDS ($friendsCount)'), @@ -378,7 +377,7 @@ class _FriendsScreenState extends ConsumerState vertical: 2, ), decoration: BoxDecoration( - color: Colors.amberAccent, + color: colors.warningAmber, borderRadius: BorderRadius.circular(10), ), child: Text( @@ -403,8 +402,15 @@ class _FriendsScreenState extends ConsumerState children: [ // TAB 1: FRIENDS LIST friendsAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (err, _) => Center(child: Text('Error: $err')), + loading: () => Center( + child: CircularProgressIndicator(color: colors.cyberCyan), + ), + error: (err, _) => Center( + child: Text( + 'Error: $err', + style: TextStyle(color: colors.raceRed), + ), + ), data: (friends) { if (friends.isEmpty) { return Center( @@ -437,7 +443,7 @@ class _FriendsScreenState extends ConsumerState const SizedBox(height: 16), ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.search), @@ -450,67 +456,112 @@ class _FriendsScreenState extends ConsumerState ); } - return ListView.separated( - padding: const EdgeInsets.all(16.0), - itemCount: friends.length, - separatorBuilder: (context, index) => - const Divider(color: Colors.white10), - itemBuilder: (context, index) { - final friend = friends[index]; - return ListTile( - contentPadding: EdgeInsets.zero, - leading: AvatarBadge( - avatarPreset: friend.avatarPreset, - photoUrl: friend.avatarUrl, - displayName: friend.displayName, - radius: 24, - ), - title: Text( - friend.displayName, - style: const TextStyle( - fontWeight: FontWeight.w700, - color: Colors.white, + return ResponsiveLayout( + builder: (context, snapshot) { + final isWide = snapshot.sizeClass == WindowSizeClass.expanded || + (snapshot.isLandscape && snapshot.width >= 720); + + if (isWide) { + final crossAxisCount = snapshot.width >= 1100 ? 3 : 2; + return GridView.builder( + padding: const EdgeInsets.all(20), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 88, ), - ), - subtitle: Text( - friend.displayHandle, - style: const TextStyle( - color: Color(0xFF00E5FF), - fontSize: 12, - ), - ), - trailing: PopupMenuButton( - icon: const Icon(Icons.more_vert, color: Colors.white60), - onSelected: (val) { - if (val == 'remove') { - _removeFriend(friend); - } else if (val == 'block') { - _blockRider(friend.uid, friend.handle, friend.displayName); - } + itemCount: friends.length, + itemBuilder: (context, index) { + final friend = friends[index]; + return Card( + color: colors.surfaceContainerHigh, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + child: Row( + children: [ + AvatarBadge( + avatarPreset: friend.avatarPreset, + photoUrl: friend.avatarUrl, + displayName: friend.displayName, + radius: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + friend.displayName, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + Text( + friend.displayHandle, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: colors.cyberCyan, + fontSize: 12, + ), + ), + ], + ), + ), + _buildFriendMenu(friend, colors), + ], + ), + ), + ); }, - itemBuilder: (context) => [ - const PopupMenuItem( - value: 'remove', - child: Row( - children: [ - Icon(Icons.person_remove_outlined, size: 18), - SizedBox(width: 8), - Text('Remove Friend'), - ], + ); + } + + return ListView.separated( + padding: const EdgeInsets.all(16.0), + itemCount: friends.length, + separatorBuilder: (context, index) => + Divider(color: colors.outlineVariant), + itemBuilder: (context, index) { + final friend = friends[index]; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: AvatarBadge( + avatarPreset: friend.avatarPreset, + photoUrl: friend.avatarUrl, + displayName: friend.displayName, + radius: 24, + ), + title: Text( + friend.displayName, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: Colors.white, ), ), - const PopupMenuItem( - value: 'block', - child: Row( - children: [ - Icon(Icons.block, size: 18, color: Colors.redAccent), - SizedBox(width: 8), - Text('Block Rider', style: TextStyle(color: Colors.redAccent)), - ], + subtitle: Text( + friend.displayHandle, + style: TextStyle( + color: colors.cyberCyan, + fontSize: 12, ), ), - ], - ), + trailing: _buildFriendMenu(friend, colors), + ); + }, ); }, ); @@ -522,10 +573,10 @@ class _FriendsScreenState extends ConsumerState padding: const EdgeInsets.all(16.0), children: [ // Incoming Section - const Text( + Text( 'INCOMING REQUESTS', style: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 0.8, @@ -533,13 +584,16 @@ class _FriendsScreenState extends ConsumerState ), const SizedBox(height: 8), incomingAsync.when( - loading: () => const Center( + loading: () => Center( child: Padding( - padding: EdgeInsets.all(16.0), - child: CircularProgressIndicator(), + padding: const EdgeInsets.all(16.0), + child: CircularProgressIndicator(color: colors.cyberCyan), ), ), - error: (err, _) => Text('Error: $err'), + error: (err, _) => Text( + 'Error: $err', + style: TextStyle(color: colors.raceRed), + ), data: (requests) { if (requests.isEmpty) { return const Padding( @@ -554,8 +608,12 @@ class _FriendsScreenState extends ConsumerState return Column( children: requests.map((req) { return Card( - color: Colors.white.withValues(alpha: 0.05), + color: colors.surfaceContainerHigh, margin: const EdgeInsets.only(bottom: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: colors.outlineVariant), + ), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( @@ -580,8 +638,8 @@ class _FriendsScreenState extends ConsumerState ), Text( '@${req.senderHandle}', - style: const TextStyle( - color: Color(0xFF00E5FF), + style: TextStyle( + color: colors.cyberCyan, fontSize: 11, ), ), @@ -589,17 +647,17 @@ class _FriendsScreenState extends ConsumerState ), ), IconButton( - icon: const Icon( + icon: Icon( Icons.check_circle_outline, - color: Colors.greenAccent, + color: colors.launchGreen, ), tooltip: 'Accept', onPressed: () => _acceptFriendRequest(req), ), IconButton( - icon: const Icon( + icon: Icon( Icons.cancel_outlined, - color: Colors.redAccent, + color: colors.raceRed, ), tooltip: 'Decline', onPressed: () => _declineFriendRequest(req), @@ -627,7 +685,10 @@ class _FriendsScreenState extends ConsumerState const SizedBox(height: 8), outgoingAsync.when( loading: () => const SizedBox.shrink(), - error: (err, _) => Text('Error: $err'), + error: (err, _) => Text( + 'Error: $err', + style: TextStyle(color: colors.raceRed), + ), data: (requests) { if (requests.isEmpty) { return const Padding( @@ -685,10 +746,13 @@ class _FriendsScreenState extends ConsumerState Expanded( child: TextField( controller: _searchController, - decoration: const InputDecoration( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( hintText: 'Enter @handle (e.g. speed_demon)', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + filled: true, + fillColor: colors.surfaceContainerHigh, ), onSubmitted: (_) => _performSearch(), ), @@ -696,7 +760,7 @@ class _FriendsScreenState extends ConsumerState const SizedBox(width: 8), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric( horizontal: 16, @@ -735,7 +799,11 @@ class _FriendsScreenState extends ConsumerState if (_searchResult != null) ...[ Card( - color: Colors.white.withValues(alpha: 0.06), + color: colors.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -764,8 +832,8 @@ class _FriendsScreenState extends ConsumerState ), Text( _searchResult!.displayHandle, - style: const TextStyle( - color: Color(0xFF00E5FF), + style: TextStyle( + color: colors.cyberCyan, fontWeight: FontWeight.w700, fontSize: 13, ), @@ -796,7 +864,7 @@ class _FriendsScreenState extends ConsumerState ), ), ], - const Divider(height: 24, color: Colors.white12), + Divider(height: 24, color: colors.outlineVariant), // Relationship Action Row( @@ -813,32 +881,36 @@ class _FriendsScreenState extends ConsumerState ) else if (_searchResult!.relationship == FriendshipStatus.friend) - const Chip( - avatar: Icon(Icons.check, size: 16), - label: Text('Friends'), - backgroundColor: Color(0xFF003840), + Chip( + avatar: Icon(Icons.check, + size: 16, color: colors.cyberCyan), + label: const Text('Friends'), + backgroundColor: + colors.cyberCyan.withValues(alpha: 0.15), labelStyle: TextStyle( - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), ) else if (_searchResult!.relationship == FriendshipStatus.requestOutgoing) ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: Colors.white12, + backgroundColor: + colors.surfaceContainerHighest, foregroundColor: Colors.white, ), - icon: const Icon(Icons.cancel_outlined, size: 16), + icon: const Icon(Icons.cancel_outlined, + size: 16), label: const Text('CANCEL REQUEST'), onPressed: () => _cancelFriendRequest(_searchResult!.uid), ) else if (_searchResult!.relationship == FriendshipStatus.requestIncoming) - const Text( + Text( 'Check Requests tab to respond', style: TextStyle( - color: Colors.amberAccent, + color: colors.warningAmber, fontSize: 12, ), ) @@ -853,7 +925,7 @@ class _FriendsScreenState extends ConsumerState else ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.person_add, size: 16), @@ -891,4 +963,41 @@ class _FriendsScreenState extends ConsumerState ), ); } + + Widget _buildFriendMenu( + FriendSummary friend, MotorSignalThemeColors colors) { + return PopupMenuButton( + icon: const Icon(Icons.more_vert, color: Colors.white60), + color: colors.surfaceContainerHighest, + onSelected: (val) { + if (val == 'remove') { + _removeFriend(friend); + } else if (val == 'block') { + _blockRider(friend.uid, friend.handle, friend.displayName); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'remove', + child: Row( + children: [ + Icon(Icons.person_remove_outlined, size: 18), + SizedBox(width: 8), + Text('Remove Friend'), + ], + ), + ), + PopupMenuItem( + value: 'block', + child: Row( + children: [ + Icon(Icons.block, size: 18, color: colors.raceRed), + const SizedBox(width: 8), + Text('Block Rider', style: TextStyle(color: colors.raceRed)), + ], + ), + ), + ], + ); + } } diff --git a/lib/features/social/rider_profile_screen.dart b/lib/features/social/rider_profile_screen.dart index 72f85af..59dcaea 100644 --- a/lib/features/social/rider_profile_screen.dart +++ b/lib/features/social/rider_profile_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import '../../core/theme/evolution_theme_config.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/responsive/viewport_size.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../services/auth/auth_providers.dart'; import '../../services/social/handle_validator.dart'; import '../../services/social/rider_profile_models.dart'; @@ -75,11 +77,14 @@ class _RiderProfileScreenState extends ConsumerState { .toLowerCase() .replaceAll(RegExp(r'[^a-z0-9_]'), '_'); if (candidate.length >= 3) { - _handleController.text = candidate.substring(0, candidate.length.clamp(0, 18)); + _handleController.text = + candidate.substring(0, candidate.length.clamp(0, 18)); } } else if (user.email != null) { - final candidate = user.email!.split('@').first.replaceAll(RegExp(r'[^a-z0-9_]'), '_'); - _handleController.text = candidate.substring(0, candidate.length.clamp(0, 18)); + final candidate = + user.email!.split('@').first.replaceAll(RegExp(r'[^a-z0-9_]'), '_'); + _handleController.text = + candidate.substring(0, candidate.length.clamp(0, 18)); } } _selectedAvatarPreset = PresetAvatar.availablePresets.first.id; @@ -142,21 +147,18 @@ class _RiderProfileScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final colors = context.motorColors; final authState = ref.watch(authNotifierProvider); final isAuthed = authState.isAuthenticated; - final themeMode = ref.watch(dashboardThemeModeProvider); - final isEvolution = themeMode == DashboardThemeMode.evolution; - - final bgColor = isEvolution ? Colors.black : Theme.of(context).scaffoldBackgroundColor; - final titleFont = isEvolution ? 'Sora' : null; if (!isAuthed) { return Scaffold( - backgroundColor: bgColor, + backgroundColor: colors.surfaceContainer, appBar: AppBar( - title: Text( + backgroundColor: colors.surfaceContainerHigh, + title: const Text( 'Rider Profile', - style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800), + style: TextStyle(fontWeight: FontWeight.w800), ), ), body: Center( @@ -165,16 +167,15 @@ class _RiderProfileScreenState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.person_pin_circle_outlined, size: 64, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), const SizedBox(height: 16), - Text( + const Text( 'MOTORCLOUD ACCOUNT REQUIRED', style: TextStyle( - fontFamily: titleFont, fontWeight: FontWeight.w800, fontSize: 16, color: Colors.white, @@ -190,7 +191,7 @@ class _RiderProfileScreenState extends ConsumerState { const SizedBox(height: 24), ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.login), @@ -207,11 +208,12 @@ class _RiderProfileScreenState extends ConsumerState { final profileAsync = ref.watch(myRiderProfileProvider); return Scaffold( - backgroundColor: bgColor, + backgroundColor: colors.surfaceContainer, appBar: AppBar( - title: Text( + backgroundColor: colors.surfaceContainerHigh, + title: const Text( 'Rider Profile', - style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800), + style: TextStyle(fontWeight: FontWeight.w800), ), actions: [ IconButton( @@ -222,8 +224,15 @@ class _RiderProfileScreenState extends ConsumerState { ], ), body: profileAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (err, _) => Center(child: Text('Error loading profile: $err')), + loading: () => Center( + child: CircularProgressIndicator(color: colors.cyberCyan), + ), + error: (err, _) => Center( + child: Text( + 'Error loading profile: $err', + style: TextStyle(color: colors.raceRed), + ), + ), data: (profile) { if (profile != null) { _populateFromProfile(profile); @@ -231,228 +240,439 @@ class _RiderProfileScreenState extends ConsumerState { _populateDefaults(); } - return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 100.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Header avatar preview - Center( - child: Column( - children: [ - AvatarBadge( - avatarPreset: _selectedAvatarPreset, - displayName: _displayNameController.text.isEmpty - ? 'Rider' - : _displayNameController.text, - radius: 40, - ), - const SizedBox(height: 8), - Text( - _handleController.text.isNotEmpty - ? '@${_handleController.text}' - : '@handle', - style: const TextStyle( - color: Color(0xFF00E5FF), - fontWeight: FontWeight.w800, - fontSize: 14, - fontFamily: 'Inter', - ), - ), - ], - ), - ), - const SizedBox(height: 20), + return ResponsiveLayout( + builder: (context, snapshot) { + final isWide = snapshot.sizeClass == WindowSizeClass.expanded || + (snapshot.isLandscape && snapshot.width >= 720); - // Messages - if (_errorMessage != null) ...[ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.redAccent.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.redAccent), - ), - child: Text( - _errorMessage!, - style: const TextStyle(color: Colors.redAccent, fontSize: 12), - ), - ), - const SizedBox(height: 16), - ], - if (_successMessage != null) ...[ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.greenAccent.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.greenAccent), - ), - child: Text( - _successMessage!, - style: const TextStyle(color: Colors.greenAccent, fontSize: 12), - ), - ), - const SizedBox(height: 16), - ], - - // Avatar preset selector - AvatarSelector( - selectedPresetId: _selectedAvatarPreset, - onSelected: (presetId) { - setState(() { - _selectedAvatarPreset = presetId; - }); - }, - ), - const SizedBox(height: 24), - - // Handle input - TextFormField( - controller: _handleController, - decoration: const InputDecoration( - labelText: 'Rider Handle', - hintText: 'e.g. canyon_runner', - prefixText: '@', - helperText: 'Unique, 3-20 characters, lowercase letters, numbers, and underscores', - border: OutlineInputBorder(), - ), - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 16), - - // Display Name input - TextFormField( - controller: _displayNameController, - decoration: const InputDecoration( - labelText: 'Display Name', - hintText: 'e.g. Alex Rivera', - border: OutlineInputBorder(), - ), - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 16), - - // Region input - TextFormField( - controller: _regionController, - decoration: const InputDecoration( - labelText: 'Riding Region (Optional)', - hintText: 'e.g. Blue Ridge Mountains, NC', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - - // Bio input - TextFormField( - controller: _bioController, - maxLines: 3, - decoration: const InputDecoration( - labelText: 'Rider Bio (Optional)', - hintText: 'Tell fellow riders about what you ride and your riding style...', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 24), - - // Privacy Settings Card - Card( - color: Colors.white.withValues(alpha: 0.05), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon(Icons.shield_outlined, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 8), - Text( - 'PRIVACY CONTROLS', - style: TextStyle( - color: Color(0xFF00E5FF), - fontWeight: FontWeight.w800, - fontSize: 12, - letterSpacing: 0.8, - ), - ), - ], - ), - const SizedBox(height: 12), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Discoverable by Handle', style: TextStyle(fontSize: 14)), - subtitle: const Text( - 'Allow other riders to find your profile by searching your @handle.', - style: TextStyle(fontSize: 11, color: Colors.white60), - ), - value: _isSearchableByHandle, - onChanged: (val) => setState(() => _isSearchableByHandle = val), - ), - const Divider(color: Colors.white12), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Public Profile', style: TextStyle(fontSize: 14)), - subtitle: const Text( - 'When off, non-friends only see your handle and avatar, hiding bio and region.', - style: TextStyle(fontSize: 11, color: Colors.white60), - ), - value: _isProfilePublic, - onChanged: (val) => setState(() => _isProfilePublic = val), - ), - const Divider(color: Colors.white12), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Allow Friend Requests', style: TextStyle(fontSize: 14)), - subtitle: const Text( - 'Allow other riders to send you friend requests.', - style: TextStyle(fontSize: 11, color: Colors.white60), - ), - value: _allowFriendRequests, - onChanged: (val) => setState(() => _allowFriendRequests = val), - ), - ], - ), - ), - ), - const SizedBox(height: 24), - - // Save Button - SizedBox( - height: 48, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: _isSaving ? null : _handleSave, - child: _isSaving - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, - ), - ) - : const Text( - 'SAVE PROFILE', - style: TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.8), - ), - ), - ), - ], - ), - ), + if (isWide) { + return _buildWideProfile(context, colors); + } + return _buildCompactProfile(context, colors); + }, ); }, ), ); } + + Widget _buildWideProfile( + BuildContext context, MotorSignalThemeColors colors) { + return Form( + key: _formKey, + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + if (_errorMessage != null) _buildErrorMessage(colors), + if (_successMessage != null) _buildSuccessMessage(colors), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left Column: Avatar & Handle Identity + Expanded( + flex: 5, + child: Card( + color: colors.surfaceContainerHigh, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Column( + children: [ + AvatarBadge( + avatarPreset: _selectedAvatarPreset, + displayName: + _displayNameController.text.isEmpty + ? 'Rider' + : _displayNameController.text, + radius: 44, + ), + const SizedBox(height: 8), + Text( + _handleController.text.isNotEmpty + ? '@${_handleController.text}' + : '@handle', + style: TextStyle( + color: colors.cyberCyan, + fontWeight: FontWeight.w800, + fontSize: 14, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + AvatarSelector( + selectedPresetId: _selectedAvatarPreset, + onSelected: (presetId) { + setState(() { + _selectedAvatarPreset = presetId; + }); + }, + ), + const SizedBox(height: 20), + TextFormField( + controller: _handleController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Rider Handle', + hintText: 'e.g. canyon_runner', + prefixText: '@', + helperText: + 'Unique, 3-20 chars: letters, numbers, underscores', + helperStyle: const TextStyle( + color: Colors.white38, fontSize: 11), + border: const OutlineInputBorder(), + filled: true, + fillColor: colors.surfaceContainer, + ), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 16), + TextFormField( + controller: _displayNameController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Display Name', + hintText: 'e.g. Alex Rivera', + border: const OutlineInputBorder(), + filled: true, + fillColor: colors.surfaceContainer, + ), + onChanged: (_) => setState(() {}), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 24), + + // Right Column: Bio, Region & Privacy + Expanded( + flex: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: colors.surfaceContainerHigh, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: _regionController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Riding Region (Optional)', + hintText: 'e.g. Blue Ridge Mountains, NC', + border: const OutlineInputBorder(), + filled: true, + fillColor: colors.surfaceContainer, + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: _bioController, + style: const TextStyle(color: Colors.white), + maxLines: 3, + decoration: InputDecoration( + labelText: 'Rider Bio (Optional)', + hintText: + 'Tell fellow riders about your bike and riding style...', + border: const OutlineInputBorder(), + filled: true, + fillColor: colors.surfaceContainer, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + _buildPrivacyCard(colors), + const SizedBox(height: 24), + _buildSaveButton(colors), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildCompactProfile( + BuildContext context, MotorSignalThemeColors colors) { + return Form( + key: _formKey, + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 48.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header avatar preview + Center( + child: Column( + children: [ + AvatarBadge( + avatarPreset: _selectedAvatarPreset, + displayName: _displayNameController.text.isEmpty + ? 'Rider' + : _displayNameController.text, + radius: 40, + ), + const SizedBox(height: 8), + Text( + _handleController.text.isNotEmpty + ? '@${_handleController.text}' + : '@handle', + style: TextStyle( + color: colors.cyberCyan, + fontWeight: FontWeight.w800, + fontSize: 14, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + if (_errorMessage != null) _buildErrorMessage(colors), + if (_successMessage != null) _buildSuccessMessage(colors), + + AvatarSelector( + selectedPresetId: _selectedAvatarPreset, + onSelected: (presetId) { + setState(() { + _selectedAvatarPreset = presetId; + }); + }, + ), + const SizedBox(height: 24), + + TextFormField( + controller: _handleController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Rider Handle', + hintText: 'e.g. canyon_runner', + prefixText: '@', + helperText: 'Unique, 3-20 characters: letters, numbers, underscores', + helperStyle: + const TextStyle(color: Colors.white38, fontSize: 11), + border: const OutlineInputBorder(), + filled: true, + fillColor: colors.surfaceContainerHigh, + ), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 16), + + TextFormField( + controller: _displayNameController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Display Name', + hintText: 'e.g. Alex Rivera', + border: const OutlineInputBorder(), + filled: true, + fillColor: colors.surfaceContainerHigh, + ), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 16), + + TextFormField( + controller: _regionController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Riding Region (Optional)', + hintText: 'e.g. Blue Ridge Mountains, NC', + border: const OutlineInputBorder(), + filled: true, + fillColor: colors.surfaceContainerHigh, + ), + ), + const SizedBox(height: 16), + + TextFormField( + controller: _bioController, + style: const TextStyle(color: Colors.white), + maxLines: 3, + decoration: InputDecoration( + labelText: 'Rider Bio (Optional)', + hintText: + 'Tell fellow riders about what you ride and your riding style...', + border: const OutlineInputBorder(), + filled: true, + fillColor: colors.surfaceContainerHigh, + ), + ), + const SizedBox(height: 24), + + _buildPrivacyCard(colors), + const SizedBox(height: 24), + + _buildSaveButton(colors), + ], + ), + ), + ); + } + + Widget _buildErrorMessage(MotorSignalThemeColors colors) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colors.raceRed.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: colors.raceRed), + ), + child: Text( + _errorMessage!, + style: TextStyle(color: colors.raceRed, fontSize: 12), + ), + ); + } + + Widget _buildSuccessMessage(MotorSignalThemeColors colors) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colors.launchGreen.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: colors.launchGreen), + ), + child: Text( + _successMessage!, + style: TextStyle(color: colors.launchGreen, fontSize: 12), + ), + ); + } + + Widget _buildPrivacyCard(MotorSignalThemeColors colors) { + return Card( + color: colors.surfaceContainerHigh, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.shield_outlined, + color: colors.cyberCyan, size: 18), + const SizedBox(width: 8), + Text( + 'PRIVACY CONTROLS', + style: TextStyle( + color: colors.cyberCyan, + fontWeight: FontWeight.w800, + fontSize: 12, + letterSpacing: 0.8, + ), + ), + ], + ), + const SizedBox(height: 12), + SwitchListTile( + contentPadding: EdgeInsets.zero, + activeThumbColor: colors.cyberCyan, + title: const Text('Discoverable by Handle', + style: TextStyle(fontSize: 14)), + subtitle: const Text( + 'Allow other riders to find your profile by searching your @handle.', + style: TextStyle(fontSize: 11, color: Colors.white60), + ), + value: _isSearchableByHandle, + onChanged: (val) => + setState(() => _isSearchableByHandle = val), + ), + Divider(color: colors.outlineVariant), + SwitchListTile( + contentPadding: EdgeInsets.zero, + activeThumbColor: colors.cyberCyan, + title: const Text('Public Profile', + style: TextStyle(fontSize: 14)), + subtitle: const Text( + 'When off, non-friends only see your handle and avatar, hiding bio and region.', + style: TextStyle(fontSize: 11, color: Colors.white60), + ), + value: _isProfilePublic, + onChanged: (val) => + setState(() => _isProfilePublic = val), + ), + Divider(color: colors.outlineVariant), + SwitchListTile( + contentPadding: EdgeInsets.zero, + activeThumbColor: colors.cyberCyan, + title: const Text('Allow Friend Requests', + style: TextStyle(fontSize: 14)), + subtitle: const Text( + 'Allow other riders to send you friend requests.', + style: TextStyle(fontSize: 11, color: Colors.white60), + ), + value: _allowFriendRequests, + onChanged: (val) => + setState(() => _allowFriendRequests = val), + ), + ], + ), + ), + ); + } + + Widget _buildSaveButton(MotorSignalThemeColors colors) { + return SizedBox( + height: 48, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: colors.cyberCyan, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: _isSaving ? null : _handleSave, + child: _isSaving + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.black, + ), + ) + : const Text( + 'SAVE PROFILE', + style: + TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.8), + ), + ), + ); + } } diff --git a/lib/features/social/widgets/avatar_badge.dart b/lib/features/social/widgets/avatar_badge.dart index 7b73fc7..eefeaa2 100644 --- a/lib/features/social/widgets/avatar_badge.dart +++ b/lib/features/social/widgets/avatar_badge.dart @@ -68,7 +68,7 @@ class AvatarBadge extends StatelessWidget { padding: const EdgeInsets.all(3), decoration: BoxDecoration( shape: BoxShape.circle, - border: Border.all(color: const Color(0xFF00E5FF), width: 2), + border: Border.all(color: Theme.of(context).colorScheme.primary, width: 2), ), child: inner, ); diff --git a/test/canonical_group_rides_social_responsive_test.dart b/test/canonical_group_rides_social_responsive_test.dart new file mode 100644 index 0000000..af38d1e --- /dev/null +++ b/test/canonical_group_rides_social_responsive_test.dart @@ -0,0 +1,539 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:motorcycle_dash/core/theme/theme.dart'; +import 'package:motorcycle_dash/features/group_rides/group_rides_hub_screen.dart'; +import 'package:motorcycle_dash/features/group_rides/create_group_ride_screen.dart'; +import 'package:motorcycle_dash/features/group_rides/group_ride_detail_screen.dart'; +import 'package:motorcycle_dash/features/group_rides/live_group_ride_screen.dart'; +import 'package:motorcycle_dash/features/social/rider_profile_screen.dart'; +import 'package:motorcycle_dash/features/social/friends_screen.dart'; +import 'package:motorcycle_dash/services/auth/auth_providers.dart'; +import 'package:motorcycle_dash/services/auth/auth_service.dart'; +import 'package:motorcycle_dash/services/auth/auth_state.dart'; +import 'package:motorcycle_dash/services/auth/auth_user.dart'; +import 'package:motorcycle_dash/services/group_rides/group_ride_models.dart'; +import 'package:motorcycle_dash/services/group_rides/group_ride_providers.dart'; +import 'package:motorcycle_dash/services/group_rides/live_broadcast_controller.dart'; +import 'package:motorcycle_dash/services/social/rider_profile_models.dart'; +import 'package:motorcycle_dash/services/social/social_providers.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:motorcycle_dash/features/records/records_service.dart'; + +class _FakeAuth implements AuthService { + final _states = StreamController.broadcast(); + AuthUser? user; + + _FakeAuth({this.user}); + + @override + Stream get authStateChanges => _states.stream; + + @override + AuthState get currentState => user == null + ? const AuthState.unauthenticated(isFirebaseConfigured: true) + : (user!.isAnonymous + ? AuthState.anonymous(user!, isFirebaseConfigured: true) + : AuthState.authenticated(user!, isFirebaseConfigured: true)); + + @override + AuthUser? get currentUser => user; + + @override + bool get isConfigured => true; + + @override + bool get isEmailVerified => true; + + @override + Future deleteCurrentUser() async {} + + @override + void dispose() => _states.close(); + + @override + Future getIdToken([bool forceRefresh = false]) async => 'token'; + + @override + Future reauthenticate({String? password}) async {} + + @override + Future reloadCurrentUser() async {} + + @override + Future sendEmailVerification() async {} + + @override + Future sendPasswordResetEmail(String email) async {} + + @override + Future signInAnonymously() async {} + + @override + Future signInWithEmailAndPassword(String email, String password) async {} + + @override + Future createUserWithEmailAndPassword(String email, String password, + {String? displayName}) async {} + + @override + Future signInWithGoogle() async {} + + @override + Future signOut() async {} +} + +class _FakeLiveBroadcastController extends StateNotifier + implements LiveBroadcastController { + _FakeLiveBroadcastController(super.initialState); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); + + @override + Future setOptIn(bool optIn) async {} + + @override + Future setStatus(LivePresenceStatus status) async {} + + @override + Future triggerRegroup() async {} + + @override + Future clearRegroup() async {} + + @override + void updateAppLifecycle(AppLifecycleState state) {} +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late SharedPreferences prefs; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + prefs = await SharedPreferences.getInstance(); + }); + + const testUser = AuthUser( + uid: 'uid_lead', + email: 'rider@motorsignal.app', + displayName: 'Canyon Lead', + isAnonymous: false, + ); + + final testProfile = RiderProfile( + uid: 'uid_lead', + handle: 'canyon_lead', + displayName: 'Canyon Lead', + bio: 'Twisty roads and track days', + region: 'Blue Ridge, NC', + avatarPreset: 'preset_neon_cyan', + privacySettings: const ProfilePrivacySettings( + isSearchableByHandle: true, + isProfilePublic: true, + allowFriendRequests: true, + ), + createdAt: DateTime(2026, 1, 1), + updatedAt: DateTime(2026, 1, 1), + ); + + final now = DateTime(2026, 9, 1, 12, 0); + + final testRide = GroupRide( + id: 'ride_101', + organizerUid: 'uid_lead', + organizerHandle: 'canyon_lead', + organizerDisplayName: 'Canyon Lead', + title: 'Blue Ridge Twisties Run', + description: 'Scenic group ride through Blue Ridge twisties.', + scheduledStartAt: DateTime(2026, 10, 15, 9, 0), + departureAt: DateTime(2026, 10, 15, 9, 30), + status: GroupRideStatus.underway, + joinPolicy: GroupRideJoinPolicy.open, + visibility: GroupRideVisibility.public, + meetingPoint: const GroupRideLocation( + latitude: 35.5951, + longitude: -82.5515, + displayName: 'Asheville Meetup', + locationType: GroupRideLocationType.meetup, + ), + destination: const GroupRideLocation( + latitude: 35.7951, + longitude: -82.2515, + displayName: 'Mount Mitchell Peak', + locationType: GroupRideLocationType.destination, + ), + participantCount: 3, + stops: const [ + GroupRideLocation( + latitude: 35.65, + longitude: -82.40, + displayName: 'Scenic Overlook Stop', + locationType: GroupRideLocationType.scenic, + ), + ], + createdAt: now, + updatedAt: now, + ); + + final List testParticipants = [ + GroupRideParticipant( + userId: 'uid_lead', + riderHandle: 'canyon_lead', + riderDisplayName: 'Canyon Lead', + role: GroupRideRole.lead, + status: ParticipantStatus.accepted, + createdAt: now, + updatedAt: now, + ), + GroupRideParticipant( + userId: 'uid_sweep', + riderHandle: 'tail_gunner', + riderDisplayName: 'Tail Gunner', + role: GroupRideRole.sweep, + status: ParticipantStatus.accepted, + createdAt: now, + updatedAt: now, + ), + GroupRideParticipant( + userId: 'uid_rider3', + riderHandle: 'moto_enthusiast', + riderDisplayName: 'Moto Enthusiast', + role: GroupRideRole.participant, + status: ParticipantStatus.accepted, + createdAt: now, + updatedAt: now, + ), + ]; + + final List testPresences = [ + LiveGroupPresence( + userId: 'uid_lead', + groupRideId: 'ride_101', + latitude: 35.5951, + longitude: -82.5515, + heading: 45.0, + speedMph: 45.0, + accuracyMeters: 5.0, + status: LivePresenceStatus.riding, + sharingEnabled: true, + updatedAt: now, + expiresAt: now.add(const Duration(seconds: 120)), + ), + LiveGroupPresence( + userId: 'uid_sweep', + groupRideId: 'ride_101', + latitude: 35.5900, + longitude: -82.5500, + heading: 40.0, + speedMph: 42.0, + accuracyMeters: 5.0, + status: LivePresenceStatus.riding, + sharingEnabled: true, + updatedAt: now.subtract(const Duration(seconds: 8)), + expiresAt: now.add(const Duration(seconds: 112)), + ), + ]; + + final List testFriends = [ + FriendSummary( + uid: 'uid_sweep', + handle: 'tail_gunner', + displayName: 'Tail Gunner', + avatarPreset: 'preset_amber_flame', + friendedAt: DateTime(2026, 8, 1), + ), + FriendSummary( + uid: 'uid_rider3', + handle: 'moto_enthusiast', + displayName: 'Moto Enthusiast', + avatarPreset: 'preset_electric_green', + friendedAt: DateTime(2026, 8, 10), + ), + ]; + + final List testIncomingRequests = [ + FriendRequest( + requestId: 'req_1', + senderUid: 'uid_guest', + senderHandle: 'speed_demon', + senderDisplayName: 'Speed Demon', + senderAvatarPreset: 'preset_hyper_purple', + receiverUid: 'uid_lead', + sentAt: DateTime(2026, 9, 4), + ), + ]; + + final viewports = { + 'Compact Phone (320x640)': const Size(320, 640), + 'Standard Phone (400x800)': const Size(400, 800), + 'Tall Phone (412x915)': const Size(412, 915), + 'Small Landscape (640x320)': const Size(640, 320), + 'Short Moto Mount Landscape (780x360)': const Size(780, 360), + 'Standard Landscape (800x360)': const Size(800, 360), + 'Wide Landscape (840x390)': const Size(840, 390), + 'Medium Tablet Portrait (768x1024)': const Size(768, 1024), + 'Expanded Tablet Landscape (1024x768)': const Size(1024, 768), + }; + + group('GroupRidesHubScreen Responsive Viewport Matrix', () { + for (final entry in viewports.entries) { + testWidgets('Renders cleanly without overflow on ${entry.key}', + (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + + final fakeAuth = _FakeAuth(user: testUser); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + authServiceProvider.overrideWithValue(fakeAuth), + currentUserProvider.overrideWithValue(testUser), + myRiderProfileProvider.overrideWith( + (ref) => Stream.value(testProfile), + ), + upcomingGroupRidesProvider.overrideWith( + (ref) => Stream.value([testRide]), + ), + ], + child: MaterialApp( + theme: AppTheme.darkTheme, + home: const GroupRidesHubScreen(), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('GROUP RIDES'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + } + }); + + group('CreateGroupRideScreen Responsive Matrix', () { + for (final entry in [ + const MapEntry('Compact (400x800)', Size(400, 800)), + const MapEntry('Short Landscape (800x360)', Size(800, 360)), + const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)), + ]) { + testWidgets('Renders cleanly on ${entry.key}', (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + + final fakeAuth = _FakeAuth(user: testUser); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + authServiceProvider.overrideWithValue(fakeAuth), + currentUserProvider.overrideWithValue(testUser), + myRiderProfileProvider.overrideWith( + (ref) => Stream.value(testProfile), + ), + ], + child: MaterialApp( + theme: AppTheme.darkTheme, + home: const CreateGroupRideScreen(), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('CREATE GROUP RIDE'), findsOneWidget); + expect(find.text('1. RIDE DETAILS'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + } + }); + + group('GroupRideDetailScreen Responsive Matrix', () { + for (final entry in [ + const MapEntry('Compact (400x800)', Size(400, 800)), + const MapEntry('Short Landscape (800x360)', Size(800, 360)), + const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)), + ]) { + testWidgets('Renders 2-column on wide and details cleanly on ${entry.key}', + (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + + final fakeAuth = _FakeAuth(user: testUser); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + authServiceProvider.overrideWithValue(fakeAuth), + currentUserProvider.overrideWithValue(testUser), + groupRideStreamProvider('ride_101').overrideWith( + (ref) => Stream.value(testRide), + ), + groupRideParticipantsProvider('ride_101').overrideWith( + (ref) => Stream.value(testParticipants), + ), + groupRideLivePresenceProvider('ride_101').overrideWith( + (ref) => Stream.value(testPresences), + ), + myRiderProfileProvider.overrideWith( + (ref) => Stream.value(testProfile), + ), + ], + child: MaterialApp( + theme: AppTheme.darkTheme, + home: const GroupRideDetailScreen(rideId: 'ride_101'), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('Blue Ridge Twisties Run'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + } + }); + + group('LiveGroupRideScreen Responsive Matrix', () { + for (final entry in [ + const MapEntry('Compact (400x800)', Size(400, 800)), + const MapEntry('Short Landscape (800x360)', Size(800, 360)), + const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)), + ]) { + testWidgets('Renders map, status bar, and controls on ${entry.key}', + (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + + final fakeAuth = _FakeAuth(user: testUser); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + authServiceProvider.overrideWithValue(fakeAuth), + currentUserProvider.overrideWithValue(testUser), + groupRideStreamProvider('ride_101').overrideWith( + (ref) => Stream.value(testRide), + ), + groupRideParticipantsProvider('ride_101').overrideWith( + (ref) => Stream.value(testParticipants), + ), + groupRideLivePresenceProvider('ride_101').overrideWith( + (ref) => Stream.value(testPresences), + ), + liveBroadcastControllerProvider('ride_101').overrideWith( + (ref) => _FakeLiveBroadcastController( + const LiveBroadcastState( + isOptedIn: true, + isBroadcasting: true, + status: LivePresenceStatus.riding, + ), + ), + ), + ], + child: MaterialApp( + theme: AppTheme.darkTheme, + home: const LiveGroupRideScreen(rideId: 'ride_101'), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.byKey(const ValueKey('live_ride_broadcast_switch')), + findsOneWidget); + expect(find.byKey(const ValueKey('live_ride_recenter_button')), + findsOneWidget); + expect(tester.takeException(), isNull); + }); + } + }); + + group('RiderProfileScreen & FriendsScreen Responsive Matrix', () { + for (final entry in [ + const MapEntry('Compact (400x800)', Size(400, 800)), + const MapEntry('Wide (1024x768)', Size(1024, 768)), + ]) { + testWidgets('RiderProfileScreen renders on ${entry.key}', (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + + final fakeAuth = _FakeAuth(user: testUser); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + authServiceProvider.overrideWithValue(fakeAuth), + currentUserProvider.overrideWithValue(testUser), + myRiderProfileProvider.overrideWith( + (ref) => Stream.value(testProfile), + ), + ], + child: MaterialApp( + theme: AppTheme.darkTheme, + home: const RiderProfileScreen(), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('Rider Profile'), findsOneWidget); + expect(find.text('PRIVACY CONTROLS'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + testWidgets('FriendsScreen renders on ${entry.key}', (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + + final fakeAuth = _FakeAuth(user: testUser); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + authServiceProvider.overrideWithValue(fakeAuth), + currentUserProvider.overrideWithValue(testUser), + myRiderProfileProvider.overrideWith( + (ref) => Stream.value(testProfile), + ), + myFriendsProvider.overrideWith( + (ref) => Stream.value(testFriends), + ), + myIncomingRequestsProvider.overrideWith( + (ref) => Stream.value(testIncomingRequests), + ), + myOutgoingRequestsProvider.overrideWith( + (ref) => Stream.value([]), + ), + ], + child: MaterialApp( + theme: AppTheme.darkTheme, + home: const FriendsScreen(), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('Friends & Riders'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + } + }); +}