feat: unify group rides and social experience

This commit is contained in:
Developer 2026-09-05 04:08:53 -04:00
parent ce6641102d
commit 4beacfde03
8 changed files with 2779 additions and 1417 deletions

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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_models.dart';
import '../../services/group_rides/group_ride_providers.dart'; import '../../services/group_rides/group_ride_providers.dart';
import '../../services/social/social_providers.dart'; import '../../services/social/social_providers.dart';
@ -158,7 +161,8 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
if (_titleController.text.trim().isEmpty) { if (_titleController.text.trim().isEmpty) {
_titleController.text = route.title; _titleController.text = route.title;
} }
if (_descriptionController.text.trim().isEmpty && route.description.isNotEmpty) { if (_descriptionController.text.trim().isEmpty &&
route.description.isNotEmpty) {
_descriptionController.text = route.description; _descriptionController.text = route.description;
} }
@ -201,13 +205,13 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
}); });
} }
void _showSelectRouteSheet() { void _showSelectRouteSheet(MotorSignalThemeColors colors) {
final user = ref.read(currentUserProvider); final user = ref.read(currentUserProvider);
if (user == null) return; if (user == null) return;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
backgroundColor: const Color(0xFF13151A), backgroundColor: colors.surfaceContainerHigh,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
), ),
@ -237,15 +241,16 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
routesAsync.when( routesAsync.when(
loading: () => const Center( loading: () => Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: CircularProgressIndicator(color: Color(0xFF00E5FF)), child:
CircularProgressIndicator(color: colors.cyberCyan),
), ),
), ),
error: (err, _) => Text( error: (err, _) => Text(
'Failed to load routes: $err', 'Failed to load routes: $err',
style: const TextStyle(color: Colors.redAccent), style: TextStyle(color: colors.raceRed),
), ),
data: (routes) { data: (routes) {
if (routes.isEmpty) { if (routes.isEmpty) {
@ -269,14 +274,18 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
itemBuilder: (ctx, idx) { itemBuilder: (ctx, idx) {
final route = routes[idx]; final route = routes[idx];
return ListTile( return ListTile(
leading: const Icon(Icons.alt_route, color: Color(0xFF00E5FF)), leading: Icon(Icons.alt_route,
color: colors.cyberCyan),
title: Text( title: Text(
route.title, route.title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold),
), ),
subtitle: Text( subtitle: Text(
'${route.stops.length} stops • ${route.formattedDistanceMiles}', '${route.stops.length} stops • ${route.formattedDistanceMiles}',
style: const TextStyle(color: Colors.white54, fontSize: 12), style: const TextStyle(
color: Colors.white54, fontSize: 12),
), ),
onTap: () { onTap: () {
Navigator.pop(sheetCtx); Navigator.pop(sheetCtx);
@ -298,7 +307,7 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
); );
} }
void _addStopDialog() { void _addStopDialog(MotorSignalThemeColors colors) {
final nameCtrl = TextEditingController(); final nameCtrl = TextEditingController();
final notesCtrl = TextEditingController(); final notesCtrl = TextEditingController();
var stopType = GroupRideLocationType.fuel; var stopType = GroupRideLocationType.fuel;
@ -307,7 +316,11 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
context: context, context: context,
builder: (ctx) => StatefulBuilder( builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) => AlertDialog( 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', title: const Text('Add Intermediate Stop',
style: TextStyle(color: Colors.white, fontSize: 16)), style: TextStyle(color: Colors.white, fontSize: 16)),
content: Column( content: Column(
@ -316,17 +329,29 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
TextField( TextField(
controller: nameCtrl, controller: nameCtrl,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Stop Name (e.g. Shell Fuel / Overlook)', 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), const SizedBox(height: 10),
DropdownButtonFormField<GroupRideLocationType>( DropdownButtonFormField<GroupRideLocationType>(
initialValue: stopType, initialValue: stopType,
dropdownColor: const Color(0xFF1B1E28), dropdownColor: colors.surfaceContainerHigh,
style: const TextStyle(color: Colors.white), 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: [ items: [
GroupRideLocationType.fuel, GroupRideLocationType.fuel,
GroupRideLocationType.rest, GroupRideLocationType.rest,
@ -347,9 +372,14 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
TextField( TextField(
controller: notesCtrl, controller: notesCtrl,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Notes (optional)', 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<CreateGroupRideScreen> {
), ),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
), ),
onPressed: () { onPressed: () {
@ -389,10 +419,13 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.motorColors;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
appBar: AppBar( appBar: AppBar(
backgroundColor: const Color(0xFF13151A), backgroundColor: colors.surfaceContainerHigh,
elevation: 0,
title: const Text( title: const Text(
'CREATE GROUP RIDE', 'CREATE GROUP RIDE',
style: TextStyle( style: TextStyle(
@ -403,64 +436,165 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
), ),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.alt_route, color: Color(0xFF00E5FF)), icon: Icon(Icons.alt_route, color: colors.cyberCyan),
tooltip: 'Import Route', tooltip: 'Import Route',
onPressed: _showSelectRouteSheet, onPressed: () => _showSelectRouteSheet(colors),
), ),
TextButton( TextButton(
onPressed: _isSaving ? null : _submit, onPressed: _isSaving ? null : _submit,
child: _isSaving child: _isSaving
? const SizedBox( ? SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(
strokeWidth: 2,
color: colors.cyberCyan,
),
) )
: const Text( : Text(
'PUBLISH', 'PUBLISH',
style: TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
], ],
), ),
body: Form( body: ResponsiveLayout(
key: _formKey, builder: (context, snapshot) {
child: ListView( final isWide = snapshot.sizeClass == WindowSizeClass.expanded ||
padding: const EdgeInsets.all(20), (snapshot.isLandscape && snapshot.width >= 720);
children: [
if (_errorMessage != null) if (isWide) {
Container( return _buildWideForm(context, colors, snapshot);
margin: const EdgeInsets.only(bottom: 16), }
padding: const EdgeInsets.all(12), return _buildCompactForm(context, colors, snapshot);
decoration: BoxDecoration( },
color: Colors.redAccent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.redAccent),
), ),
child: Row( );
}
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: [ children: [
const Icon(Icons.error_outline, if (_errorMessage != null) _buildErrorBanner(colors),
color: Colors.redAccent, size: 20), Row(
const SizedBox(width: 10), crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column: Details, Schedule, Privacy
Expanded( Expanded(
child: Text( flex: 5,
_errorMessage!, child: Column(
style: const TextStyle( crossAxisAlignment: CrossAxisAlignment.start,
color: Colors.redAccent, fontSize: 12), children: [
_buildRideDetailsSection(colors),
const SizedBox(height: 24),
_buildScheduleSection(colors),
const SizedBox(height: 24),
_buildPrivacySection(colors),
],
),
),
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),
],
), ),
), ),
], ],
), ),
const SizedBox(height: 32),
],
), ),
),
);
}
_buildSectionHeader('1. RIDE DETAILS'), 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), const SizedBox(height: 10),
TextFormField( TextFormField(
controller: _titleController, controller: _titleController,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Ride Title', 'e.g. Blood Mountain Loop'), decoration: _inputDecoration(
'Ride Title',
'e.g. Blood Mountain Loop',
colors,
),
validator: (v) { validator: (v) {
if (v == null || v.trim().length < 3) { if (v == null || v.trim().length < 3) {
return 'Title must be at least 3 characters'; return 'Title must be at least 3 characters';
@ -476,22 +610,32 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
decoration: _inputDecoration( decoration: _inputDecoration(
'Description & Route Notes', 'Description & Route Notes',
'Pace, skill level, gear recommendations...', 'Pace, skill level, gear recommendations...',
colors,
), ),
), ),
],
);
}
const SizedBox(height: 24), Widget _buildScheduleSection(MotorSignalThemeColors colors) {
_buildSectionHeader('2. DATE & SCHEDULE'), return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('2. DATE & SCHEDULE', colors),
const SizedBox(height: 10), const SizedBox(height: 10),
Card( Card(
color: const Color(0xFF161922), color: colors.surfaceContainerHigh,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: colors.outlineVariant),
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
leading: leading: Icon(Icons.calendar_month, color: colors.cyberCyan),
const Icon(Icons.calendar_month, color: Color(0xFF00E5FF)),
title: const Text('Date', title: const Text('Date',
style: TextStyle(color: Colors.white70, fontSize: 13)), style: TextStyle(color: Colors.white70, fontSize: 13)),
subtitle: Text( subtitle: Text(
@ -515,7 +659,10 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
setState(() => _scheduledDate = picked); setState(() => _scheduledDate = picked);
} }
}, },
child: const Text('CHANGE'), child: Text(
'CHANGE',
style: TextStyle(color: colors.cyberCyan),
),
), ),
), ),
const Divider(color: Colors.white12), const Divider(color: Colors.white12),
@ -553,8 +700,8 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
color: Colors.white70, fontSize: 12)), color: Colors.white70, fontSize: 12)),
subtitle: Text( subtitle: Text(
_departureTime.format(context), _departureTime.format(context),
style: const TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@ -575,9 +722,15 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
), ),
), ),
), ),
],
);
}
const SizedBox(height: 24), Widget _buildMeetingPointSection(MotorSignalThemeColors colors) {
_buildSectionHeader('3. MEETING LOCATION'), return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('3. MEETING LOCATION', colors),
const SizedBox(height: 10), const SizedBox(height: 10),
TextFormField( TextFormField(
controller: _meetupNameController, controller: _meetupNameController,
@ -585,6 +738,7 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
decoration: _inputDecoration( decoration: _inputDecoration(
'Meeting Place Name', 'Meeting Place Name',
'e.g. Dahlonega Square / Shell Station', 'e.g. Dahlonega Square / Shell Station',
colors,
), ),
validator: (v) { validator: (v) {
if (v == null || v.trim().isEmpty) { if (v == null || v.trim().isEmpty) {
@ -600,6 +754,7 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
decoration: _inputDecoration( decoration: _inputDecoration(
'Address or Landmark', 'Address or Landmark',
'e.g. 123 Main St, Dahlonega, GA', 'e.g. 123 Main St, Dahlonega, GA',
colors,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -609,15 +764,22 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
decoration: _inputDecoration( decoration: _inputDecoration(
'Meeting Notes (optional)', 'Meeting Notes (optional)',
'e.g. Park on the north side near pumps', 'e.g. Park on the north side near pumps',
colors,
), ),
), ),
],
);
}
const SizedBox(height: 24), Widget _buildDestinationAndStopsSection(MotorSignalThemeColors colors) {
_buildSectionHeader('4. DESTINATION & STOPS'), return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('4. DESTINATION & STOPS', colors),
const SizedBox(height: 10), const SizedBox(height: 10),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
activeThumbColor: const Color(0xFF00E5FF), activeThumbColor: colors.cyberCyan,
title: const Text('Specific Destination', title: const Text('Specific Destination',
style: TextStyle(color: Colors.white)), style: TextStyle(color: Colors.white)),
value: _hasDestination, value: _hasDestination,
@ -631,6 +793,7 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
decoration: _inputDecoration( decoration: _inputDecoration(
'Destination Name', 'Destination Name',
'e.g. Brasstown Bald Summit', 'e.g. Brasstown Bald Summit',
colors,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -640,23 +803,23 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
decoration: _inputDecoration( decoration: _inputDecoration(
'Destination Address (optional)', 'Destination Address (optional)',
'e.g. Hiawassee, GA', 'e.g. Hiawassee, GA',
colors,
), ),
), ),
], ],
const SizedBox(height: 16), const SizedBox(height: 16),
// Route Pre-fill / Attachment // Route Pre-fill / Attachment
Container( Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF161922), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF2A2E3D)), border: Border.all(color: colors.outlineVariant),
), ),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.alt_route, color: Color(0xFF00E5FF), size: 22), Icon(Icons.alt_route, color: colors.cyberCyan, size: 22),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
@ -677,14 +840,16 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
_selectedRouteTitle != null _selectedRouteTitle != null
? '$_selectedRouteStopCount stops • ${(_selectedRouteDistanceMeters != null ? (_selectedRouteDistanceMeters! / 1609.34).toStringAsFixed(1) : 0)} mi' ? '$_selectedRouteStopCount stops • ${(_selectedRouteDistanceMeters != null ? (_selectedRouteDistanceMeters! / 1609.34).toStringAsFixed(1) : 0)} mi'
: 'Optional: import stops & coordinates', : 'Optional: import stops & coordinates',
style: const TextStyle(color: Colors.white54, fontSize: 11), style:
const TextStyle(color: Colors.white54, fontSize: 11),
), ),
], ],
), ),
), ),
if (_selectedRouteTitle != null) if (_selectedRouteTitle != null)
IconButton( IconButton(
icon: const Icon(Icons.close, color: Colors.white54, size: 18), icon:
const Icon(Icons.close, color: Colors.white54, size: 18),
tooltip: 'Clear route', tooltip: 'Clear route',
onPressed: () { onPressed: () {
setState(() { setState(() {
@ -697,17 +862,18 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
) )
else else
OutlinedButton( OutlinedButton(
onPressed: _showSelectRouteSheet, onPressed: () => _showSelectRouteSheet(colors),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF00E5FF)), side: BorderSide(color: colors.cyberCyan),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
minimumSize: Size.zero, minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
child: const Text( child: Text(
'IMPORT', 'IMPORT',
style: TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -716,21 +882,25 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
], ],
), ),
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text( const Flexible(
child: Text(
'Intermediate Stops', 'Intermediate Stops',
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
),
TextButton.icon( TextButton.icon(
onPressed: _addStopDialog, onPressed: () => _addStopDialog(colors),
icon: const Icon(Icons.add_location_alt, size: 16), icon: Icon(Icons.add_location_alt,
label: const Text('ADD STOP'), size: 16, color: colors.cyberCyan),
label: Text('ADD STOP',
style: TextStyle(color: colors.cyberCyan)),
), ),
], ],
), ),
@ -756,26 +926,28 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
for (int i = 0; i < _stops.length; i++) for (int i = 0; i < _stops.length; i++)
ListTile( ListTile(
key: ValueKey('stop_$i'), key: ValueKey('stop_$i'),
tileColor: const Color(0xFF161922), tileColor: colors.surfaceContainerHigh,
leading: CircleAvatar( leading: CircleAvatar(
radius: 12, radius: 12,
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.2), backgroundColor: colors.cyberCyan.withValues(alpha: 0.2),
child: Text( child: Text(
'${i + 1}', '${i + 1}',
style: const TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
title: Text( title: Text(
_stops[i].displayName, _stops[i].displayName,
style: const TextStyle(color: Colors.white, fontSize: 13), style:
const TextStyle(color: Colors.white, fontSize: 13),
), ),
subtitle: Text( subtitle: Text(
_stops[i].locationType.label, _stops[i].locationType.label,
style: const TextStyle(color: Colors.white54, fontSize: 11), style:
const TextStyle(color: Colors.white54, fontSize: 11),
), ),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.delete_outline, icon: const Icon(Icons.delete_outline,
@ -785,15 +957,21 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
), ),
], ],
), ),
],
);
}
const SizedBox(height: 24), Widget _buildPrivacySection(MotorSignalThemeColors colors) {
_buildSectionHeader('5. PRIVACY & PARTICIPATION'), return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('5. PRIVACY & PARTICIPATION', colors),
const SizedBox(height: 10), const SizedBox(height: 10),
DropdownButtonFormField<GroupRideVisibility>( DropdownButtonFormField<GroupRideVisibility>(
initialValue: _visibility, initialValue: _visibility,
dropdownColor: const Color(0xFF1B1E28), dropdownColor: colors.surfaceContainerHigh,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Visibility', ''), decoration: _inputDecoration('Visibility', '', colors),
items: GroupRideVisibility.values.map((v) { items: GroupRideVisibility.values.map((v) {
return DropdownMenuItem(value: v, child: Text(v.label)); return DropdownMenuItem(value: v, child: Text(v.label));
}).toList(), }).toList(),
@ -804,9 +982,9 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
DropdownButtonFormField<GroupRideJoinPolicy>( DropdownButtonFormField<GroupRideJoinPolicy>(
initialValue: _joinPolicy, initialValue: _joinPolicy,
dropdownColor: const Color(0xFF1B1E28), dropdownColor: colors.surfaceContainerHigh,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Join Policy', ''), decoration: _inputDecoration('Join Policy', '', colors),
items: GroupRideJoinPolicy.values.map((p) { items: GroupRideJoinPolicy.values.map((p) {
return DropdownMenuItem(value: p, child: Text(p.label)); return DropdownMenuItem(value: p, child: Text(p.label));
}).toList(), }).toList(),
@ -821,20 +999,24 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
decoration: _inputDecoration( decoration: _inputDecoration(
'Participant Limit (optional)', 'Participant Limit (optional)',
'Leave blank for no limit (min 2)', 'Leave blank for no limit (min 2)',
colors,
), ),
onChanged: (val) { onChanged: (val) {
final parsed = int.tryParse(val.trim()); final parsed = int.tryParse(val.trim());
setState(() => _participantLimit = parsed); setState(() => _participantLimit = parsed);
}, },
), ),
],
);
}
const SizedBox(height: 32), Widget _buildSubmitButton(MotorSignalThemeColors colors) {
SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: 48, height: 48,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
@ -844,26 +1026,21 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
child: _isSaving child: _isSaving
? const CircularProgressIndicator(color: Colors.black) ? const CircularProgressIndicator(color: Colors.black)
: const Text( : const Text(
'CREATE GROUP RIDE', 'PUBLISH GROUP RIDE',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
letterSpacing: 1.0, letterSpacing: 1.0,
), ),
), ),
), ),
),
const SizedBox(height: 32),
],
),
),
); );
} }
Widget _buildSectionHeader(String title) { Widget _buildSectionHeader(String title, MotorSignalThemeColors colors) {
return Text( return Text(
title, title,
style: const TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
letterSpacing: 1.0, letterSpacing: 1.0,
@ -871,21 +1048,29 @@ class _CreateGroupRideScreenState extends ConsumerState<CreateGroupRideScreen> {
); );
} }
InputDecoration _inputDecoration(String label, String hint) { InputDecoration _inputDecoration(
String label,
String hint,
MotorSignalThemeColors colors,
) {
return InputDecoration( return InputDecoration(
labelText: label, labelText: label,
hintText: hint, hintText: hint,
labelStyle: const TextStyle(color: Colors.white70, fontSize: 13), labelStyle: const TextStyle(color: Colors.white70, fontSize: 13),
hintStyle: const TextStyle(color: Colors.white24, fontSize: 12), hintStyle: const TextStyle(color: Colors.white24, fontSize: 12),
filled: true, filled: true,
fillColor: const Color(0xFF161922), fillColor: colors.surfaceContainerHigh,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), 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( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF00E5FF)), borderSide: BorderSide(color: colors.cyberCyan),
), ),
); );
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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/auth/auth_providers.dart';
import '../../services/group_rides/group_ride_models.dart'; import '../../services/group_rides/group_ride_models.dart';
import '../../services/group_rides/group_ride_providers.dart'; import '../../services/group_rides/group_ride_providers.dart';
@ -31,19 +34,20 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.motorColors;
final user = ref.watch(currentUserProvider); final user = ref.watch(currentUserProvider);
final isAuthed = user != null && !user.isAnonymous; final isAuthed = user != null && !user.isAnonymous;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
appBar: AppBar( appBar: AppBar(
backgroundColor: const Color(0xFF13151A), backgroundColor: colors.surfaceContainerHigh,
elevation: 0, elevation: 0,
title: const Row( title: Row(
children: [ children: [
Icon(Icons.two_wheeler, color: Color(0xFF00E5FF), size: 24), Icon(Icons.two_wheeler, color: colors.cyberCyan, size: 24),
SizedBox(width: 10), const SizedBox(width: 10),
Text( const Text(
'GROUP RIDES', 'GROUP RIDES',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
@ -55,9 +59,9 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
), ),
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
indicatorColor: const Color(0xFF00E5FF), indicatorColor: colors.cyberCyan,
indicatorWeight: 3, indicatorWeight: 3,
labelColor: const Color(0xFF00E5FF), labelColor: colors.cyberCyan,
unselectedLabelColor: Colors.white60, unselectedLabelColor: Colors.white60,
labelStyle: const TextStyle( labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -73,25 +77,30 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
if (isAuthed) if (isAuthed)
IconButton( IconButton(
key: const ValueKey('group_rides_create_appbar_button'), 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', tooltip: 'Create Group Ride',
onPressed: () => context.push('/group-rides/create'), onPressed: () => context.push('/group-rides/create'),
), ),
], ],
), ),
body: !isAuthed body: ResponsiveLayout(
? _buildAccountRequiredNotice(context) builder: (context, snapshot) {
: TabBarView( if (!isAuthed) {
return _buildAccountRequiredNotice(context, colors);
}
return TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
_buildUpcomingTab(context, ref, user.uid), _buildUpcomingTab(context, ref, user.uid, snapshot, colors),
_buildMyRidesTab(context, ref, user.uid), _buildMyRidesTab(context, ref, user.uid, snapshot, colors),
], ],
);
},
), ),
floatingActionButton: isAuthed floatingActionButton: isAuthed
? FloatingActionButton.extended( ? FloatingActionButton.extended(
key: const ValueKey('create_group_ride_fab'), key: const ValueKey('create_group_ride_fab'),
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
icon: const Icon(Icons.add_road), icon: const Icon(Icons.add_road),
label: const Text( label: const Text(
@ -104,9 +113,12 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
); );
} }
Widget _buildAccountRequiredNotice(BuildContext context) { Widget _buildAccountRequiredNotice(
BuildContext context,
MotorSignalThemeColors colors,
) {
return Center( return Center(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -115,12 +127,12 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, 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, Icons.groups_outlined,
size: 64, size: 64,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@ -145,7 +157,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 24, horizontal: 24,
@ -172,6 +184,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
String currentUid, String currentUid,
ResponsiveSnapshot snapshot,
MotorSignalThemeColors colors,
) { ) {
final ridesAsync = ref.watch(upcomingGroupRidesProvider); final ridesAsync = ref.watch(upcomingGroupRidesProvider);
@ -179,6 +193,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
data: (rides) { data: (rides) {
if (rides.isEmpty) { if (rides.isEmpty) {
return _buildEmptyState( return _buildEmptyState(
colors: colors,
icon: Icons.map_outlined, icon: Icons.map_outlined,
title: 'No Upcoming Group Rides', title: 'No Upcoming Group Rides',
message: message:
@ -188,22 +203,43 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
); );
} }
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( return ListView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: rides.length, itemCount: rides.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final ride = rides[index]; final ride = rides[index];
return _buildRideCard(context, ride, currentUid); return _buildRideCard(context, ride, currentUid, colors);
}, },
); );
}, },
loading: () => const Center( loading: () => Center(
child: CircularProgressIndicator(color: Color(0xFF00E5FF)), child: CircularProgressIndicator(color: colors.cyberCyan),
), ),
error: (e, _) => Center( error: (e, _) => Center(
child: Text( child: Text(
'Error loading rides: $e', 'Error loading rides: $e',
style: const TextStyle(color: Colors.redAccent), style: TextStyle(color: colors.raceRed),
), ),
), ),
); );
@ -213,6 +249,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
String currentUid, String currentUid,
ResponsiveSnapshot snapshot,
MotorSignalThemeColors colors,
) { ) {
final ridesAsync = ref.watch(upcomingGroupRidesProvider); final ridesAsync = ref.watch(upcomingGroupRidesProvider);
@ -224,6 +262,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
if (myRides.isEmpty) { if (myRides.isEmpty) {
return _buildEmptyState( return _buildEmptyState(
colors: colors,
icon: Icons.sports_motorsports_outlined, icon: Icons.sports_motorsports_outlined,
title: 'You Haven\'t Created Any Rides', title: 'You Haven\'t Created Any Rides',
message: message:
@ -233,28 +272,50 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
); );
} }
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( return ListView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: myRides.length, itemCount: myRides.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final ride = myRides[index]; final ride = myRides[index];
return _buildRideCard(context, ride, currentUid); return _buildRideCard(context, ride, currentUid, colors);
}, },
); );
}, },
loading: () => const Center( loading: () => Center(
child: CircularProgressIndicator(color: Color(0xFF00E5FF)), child: CircularProgressIndicator(color: colors.cyberCyan),
), ),
error: (e, _) => Center( error: (e, _) => Center(
child: Text( child: Text(
'Error: $e', 'Error: $e',
style: const TextStyle(color: Colors.redAccent), style: TextStyle(color: colors.raceRed),
), ),
), ),
); );
} }
Widget _buildEmptyState({ Widget _buildEmptyState({
required MotorSignalThemeColors colors,
required IconData icon, required IconData icon,
required String title, required String title,
required String message, required String message,
@ -286,8 +347,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const SizedBox(height: 20), const SizedBox(height: 20),
OutlinedButton( OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF00E5FF), foregroundColor: colors.cyberCyan,
side: const BorderSide(color: Color(0xFF00E5FF)), side: BorderSide(color: colors.cyberCyan),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
@ -305,18 +366,19 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
BuildContext context, BuildContext context,
GroupRide ride, GroupRide ride,
String currentUid, String currentUid,
MotorSignalThemeColors colors,
) { ) {
final isOrganizer = ride.organizerUid == currentUid; final isOrganizer = ride.organizerUid == currentUid;
return Card( return Card(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
color: const Color(0xFF161922), color: colors.surfaceContainerHigh,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
side: BorderSide( side: BorderSide(
color: isOrganizer color: isOrganizer
? const Color(0xFF00E5FF).withValues(alpha: 0.4) ? colors.cyberCyan.withValues(alpha: 0.4)
: Colors.white12, : colors.outlineVariant,
), ),
), ),
child: InkWell( child: InkWell(
@ -337,8 +399,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: ride.isCanceled color: ride.isCanceled
? Colors.redAccent.withValues(alpha: 0.2) ? colors.raceRed.withValues(alpha: 0.2)
: const Color(0xFF00E5FF).withValues(alpha: 0.15), : colors.cyberCyan.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Text( child: Text(
@ -347,8 +409,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: ride.isCanceled color: ride.isCanceled
? Colors.redAccent ? colors.raceRed
: const Color(0xFF00E5FF), : colors.cyberCyan,
), ),
), ),
), ),
@ -360,15 +422,15 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.2), color: colors.warningAmber.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: const Text( child: Text(
'ORGANIZER', 'ORGANIZER',
style: TextStyle( style: TextStyle(
fontSize: 9, fontSize: 9,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.amber, color: colors.warningAmber,
), ),
), ),
), ),
@ -396,6 +458,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
ride.title, ride.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
fontSize: 17, fontSize: 17,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -405,20 +469,23 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
const Icon( Icon(
Icons.schedule, Icons.schedule,
size: 14, size: 14,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Expanded(
child: Text(
'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}', 'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}',
overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.white70, color: Colors.white70,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
),
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@ -446,10 +513,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
children: [ children: [
const Icon( Icon(
Icons.flag_outlined, Icons.flag_outlined,
size: 14, size: 14,
color: Color(0xFFFFB300), color: colors.warningAmber,
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(

View File

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:go_router/go_router.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/auth/auth_providers.dart';
import '../../services/group_rides/group_ride_models.dart'; import '../../services/group_rides/group_ride_models.dart';
import '../../services/group_rides/group_ride_providers.dart'; import '../../services/group_rides/group_ride_providers.dart';
@ -48,6 +50,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.motorColors;
final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId)); final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId));
final participantsAsync = ref.watch( final participantsAsync = ref.watch(
groupRideParticipantsProvider(widget.rideId), groupRideParticipantsProvider(widget.rideId),
@ -68,9 +71,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
data: (ride) { data: (ride) {
if (ride == null) { if (ride == null) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
appBar: AppBar( appBar: AppBar(
backgroundColor: const Color(0xFF13151A), backgroundColor: colors.surfaceContainerHigh,
title: const Text('LIVE GROUP RIDE'), title: const Text('LIVE GROUP RIDE'),
), ),
body: const Center( body: const Center(
@ -88,9 +91,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
// Check if ride was closed // Check if ride was closed
if (ride.isCompleted || ride.isCanceled) { if (ride.isCompleted || ride.isCanceled) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
appBar: AppBar( appBar: AppBar(
backgroundColor: const Color(0xFF13151A), backgroundColor: colors.surfaceContainerHigh,
title: Text( title: Text(
ride.title.toUpperCase(), ride.title.toUpperCase(),
style: const TextStyle( style: const TextStyle(
@ -109,8 +112,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
Icon( Icon(
ride.isCompleted ? Icons.flag : Icons.cancel, ride.isCompleted ? Icons.flag : Icons.cancel,
color: ride.isCompleted color: ride.isCompleted
? Colors.greenAccent ? colors.launchGreen
: Colors.redAccent, : colors.raceRed,
size: 48, size: 48,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -128,7 +131,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
), ),
onPressed: () => context.pop(), onPressed: () => context.pop(),
@ -202,7 +205,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
points: attachedRoute.previewPoints points: attachedRoute.previewPoints
.map((p) => AppLatLng(p.latitude, p.longitude)) .map((p) => AppLatLng(p.latitude, p.longitude))
.toList(), .toList(),
color: const Color(0xFF00E5FF).withValues(alpha: 0.85), color: colors.cyberCyan.withValues(alpha: 0.85),
width: 3.5, width: 3.5,
), ),
); );
@ -217,10 +220,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
currentUid: currentUid, currentUid: currentUid,
leadUid: leadParticipant?.userId, leadUid: leadParticipant?.userId,
attachedRoute: attachedRoute, attachedRoute: attachedRoute,
colors: colors,
); );
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
appBar: _buildAppBar( appBar: _buildAppBar(
context: context, context: context,
ride: ride, ride: ride,
@ -229,8 +233,15 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
participants: acceptedParticipants, participants: acceptedParticipants,
presences: presences, presences: presences,
leadPresence: leadPresence, leadPresence: leadPresence,
colors: colors,
), ),
body: Stack( body: ResponsiveLayout(
builder: (context, snapshot) {
final isShort = snapshot.height < 450;
final isLandscapeWide =
snapshot.isLandscape && snapshot.width >= 720;
return Stack(
children: [ children: [
// 1. Unified Map // 1. Unified Map
Positioned.fill( Positioned.fill(
@ -249,7 +260,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
Positioned( Positioned(
top: 8, top: 8,
left: 8, left: 8,
right: 8, right: isLandscapeWide ? snapshot.width * 0.35 : 8,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -261,18 +272,21 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
isLeadOrOrganizer: isLeadOrOrganizer:
isOrganizer || isOrganizer ||
(leadParticipant != null && (leadParticipant != null &&
leadParticipant.userId == currentUid), leadParticipant.userId ==
currentUid),
controller: broadcastController, controller: broadcastController,
colors: colors,
), ),
if (!broadcastState.isMasterAllowed) if (!broadcastState.isMasterAllowed)
_buildMasterPrivacyBanner(context, ref), _buildMasterPrivacyBanner(
context, ref, colors),
], ],
), ),
), ),
// 3. Quick Status Bar & Broadcast Floating Control // 3. Quick Status Bar & Broadcast Floating Control
Positioned( Positioned(
bottom: 12, bottom: isShort ? 6 : 12,
left: 8, left: 8,
right: 8, right: 8,
child: SafeArea( child: SafeArea(
@ -283,11 +297,14 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
// Quick Status Selector // Quick Status Selector
_buildQuickStatusBar( _buildQuickStatusBar(
currentStatus: broadcastState.status, currentStatus: broadcastState.status,
isBroadcasting: broadcastState.isBroadcasting, isBroadcasting:
broadcastState.isBroadcasting,
onStatusSelected: (s) => onStatusSelected: (s) =>
broadcastController.setStatus(s), broadcastController.setStatus(s),
colors: colors,
isShort: isShort,
), ),
const SizedBox(height: 8), SizedBox(height: isShort ? 6 : 8),
// Bottom Control Strip (Opt-in toggle, Roster opener, Recenter) // Bottom Control Strip (Opt-in toggle, Roster opener, Recenter)
_buildBottomControlStrip( _buildBottomControlStrip(
@ -299,67 +316,70 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
presences: presences, presences: presences,
leadPresence: leadPresence, leadPresence: leadPresence,
currentUid: currentUid, currentUid: currentUid,
colors: colors,
isShort: isShort,
), ),
], ],
), ),
), ),
), ),
], ],
);
},
), ),
); );
}, },
loading: () => const Scaffold( loading: () => Scaffold(
backgroundColor: Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
body: Center( body: Center(
child: CircularProgressIndicator(color: Color(0xFF00E5FF)), child: CircularProgressIndicator(color: colors.cyberCyan),
), ),
), ),
error: (e, _) => Scaffold( error: (e, _) => Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
body: Center( body: Center(
child: Text( child: Text(
'Error loading live presence: $e', 'Error loading live presence: $e',
style: const TextStyle(color: Colors.redAccent), style: TextStyle(color: colors.raceRed),
), ),
), ),
), ),
); );
}, },
loading: () => const Scaffold( loading: () => Scaffold(
backgroundColor: Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
body: Center( body: Center(
child: CircularProgressIndicator(color: Color(0xFF00E5FF)), child: CircularProgressIndicator(color: colors.cyberCyan),
), ),
), ),
error: (e, _) => Scaffold( error: (e, _) => Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
body: Center( body: Center(
child: Text( child: Text(
'Error loading participants: $e', 'Error loading participants: $e',
style: const TextStyle(color: Colors.redAccent), style: TextStyle(color: colors.raceRed),
), ),
), ),
), ),
); );
}, },
loading: () => const Scaffold( loading: () => Scaffold(
backgroundColor: Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
body: Center( body: Center(
child: CircularProgressIndicator(color: Color(0xFF00E5FF)), child: CircularProgressIndicator(color: colors.cyberCyan),
), ),
), ),
error: (e, _) => Scaffold( error: (e, _) => Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: colors.surfaceContainer,
body: Center( body: Center(
child: Text( child: Text(
'Error loading ride: $e', 'Error loading ride: $e',
style: const TextStyle(color: Colors.redAccent), style: TextStyle(color: colors.raceRed),
), ),
), ),
), ),
); );
} }
PreferredSizeWidget _buildAppBar({ PreferredSizeWidget _buildAppBar({
required BuildContext context, required BuildContext context,
required GroupRide ride, required GroupRide ride,
@ -368,9 +388,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
required List<GroupRideParticipant> participants, required List<GroupRideParticipant> participants,
required List<LiveGroupPresence> presences, required List<LiveGroupPresence> presences,
required LiveGroupPresence? leadPresence, required LiveGroupPresence? leadPresence,
required MotorSignalThemeColors colors,
}) { }) {
return AppBar( return AppBar(
backgroundColor: const Color(0xFF13151A), backgroundColor: colors.surfaceContainerHigh,
elevation: 3, elevation: 3,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
@ -399,8 +420,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: ride.status == GroupRideStatus.underway color: ride.status == GroupRideStatus.underway
? const Color(0xFF00E5FF).withValues(alpha: 0.2) ? colors.cyberCyan.withValues(alpha: 0.2)
: Colors.amber.withValues(alpha: 0.2), : colors.warningAmber.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(
@ -409,8 +430,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
fontSize: 9, fontSize: 9,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: ride.status == GroupRideStatus.underway color: ride.status == GroupRideStatus.underway
? const Color(0xFF00E5FF) ? colors.cyberCyan
: Colors.amber, : colors.warningAmber,
), ),
), ),
), ),
@ -425,15 +446,15 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
actions: [ actions: [
if (isOrganizer) if (isOrganizer)
IconButton( IconButton(
icon: const Icon( icon: Icon(
Icons.stop_circle_outlined, Icons.stop_circle_outlined,
color: Colors.redAccent, color: colors.raceRed,
), ),
tooltip: 'End Ride', tooltip: 'End Ride',
onPressed: () => _confirmEndRide(context, ride, currentUid), onPressed: () => _confirmEndRide(context, ride, currentUid, colors),
), ),
IconButton( IconButton(
icon: const Icon(Icons.people_outline, color: Color(0xFF00E5FF)), icon: Icon(Icons.people_outline, color: colors.cyberCyan),
tooltip: 'Roster', tooltip: 'Roster',
onPressed: () => _showRosterSheet( onPressed: () => _showRosterSheet(
context: context, context: context,
@ -441,6 +462,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
presences: presences, presences: presences,
leadPresence: leadPresence, leadPresence: leadPresence,
currentUid: currentUid, currentUid: currentUid,
colors: colors,
), ),
), ),
], ],
@ -453,6 +475,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
required List<GroupRideParticipant> participants, required List<GroupRideParticipant> participants,
required bool isLeadOrOrganizer, required bool isLeadOrOrganizer,
required LiveBroadcastController controller, required LiveBroadcastController controller,
required MotorSignalThemeColors colors,
}) { }) {
String callerName = caller.userId; String callerName = caller.userId;
try { try {
@ -466,7 +489,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
margin: const EdgeInsets.only(bottom: 6), margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.92), color: colors.warningAmber.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
boxShadow: const [ boxShadow: const [
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)), BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)),
@ -536,18 +559,19 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
); );
} }
Widget _buildMasterPrivacyBanner(BuildContext context, WidgetRef ref) { Widget _buildMasterPrivacyBanner(
BuildContext context, WidgetRef ref, MotorSignalThemeColors colors) {
return Container( return Container(
margin: const EdgeInsets.only(bottom: 6), margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1E222D), color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8), 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( child: Row(
children: [ 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 SizedBox(width: 8),
const Expanded( const Expanded(
child: Text( child: Text(
@ -558,7 +582,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
const SizedBox(width: 6), const SizedBox(width: 6),
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: Colors.amber, backgroundColor: colors.warningAmber,
foregroundColor: Colors.black, foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
minimumSize: Size.zero, minimumSize: Size.zero,
@ -581,6 +605,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
required LivePresenceStatus currentStatus, required LivePresenceStatus currentStatus,
required bool isBroadcasting, required bool isBroadcasting,
required ValueChanged<LivePresenceStatus> onStatusSelected, required ValueChanged<LivePresenceStatus> onStatusSelected,
required MotorSignalThemeColors colors,
bool isShort = false,
}) { }) {
final statuses = [ final statuses = [
(LivePresenceStatus.riding, 'RIDING', Icons.two_wheeler), (LivePresenceStatus.riding, 'RIDING', Icons.two_wheeler),
@ -592,11 +618,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
]; ];
return Container( return Container(
height: 46, height: isShort ? 40 : 46,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF161922).withValues(alpha: 0.95), color: colors.surfaceContainerHigh.withValues(alpha: 0.95),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12), border: Border.all(color: colors.outlineVariant),
boxShadow: const [ boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)), BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)),
], ],
@ -610,13 +636,13 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
final (status, label, icon) = statuses[index]; final (status, label, icon) = statuses[index];
final isSelected = currentStatus == status; final isSelected = currentStatus == status;
Color activeColor = const Color(0xFF00E5FF); Color activeColor = colors.cyberCyan;
if (status == LivePresenceStatus.needFuel) if (status == LivePresenceStatus.needFuel)
activeColor = Colors.orangeAccent; activeColor = Colors.orangeAccent;
if (status == LivePresenceStatus.regrouping) if (status == LivePresenceStatus.regrouping)
activeColor = Colors.amber; activeColor = colors.warningAmber;
if (status == LivePresenceStatus.arrived) if (status == LivePresenceStatus.arrived)
activeColor = Colors.greenAccent; activeColor = colors.launchGreen;
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
@ -670,13 +696,15 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
required List<LiveGroupPresence> presences, required List<LiveGroupPresence> presences,
required LiveGroupPresence? leadPresence, required LiveGroupPresence? leadPresence,
required String? currentUid, required String? currentUid,
required MotorSignalThemeColors colors,
bool isShort = false,
}) { }) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: EdgeInsets.symmetric(horizontal: 8, vertical: isShort ? 4 : 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF161922).withValues(alpha: 0.95), color: colors.surfaceContainerHigh.withValues(alpha: 0.95),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12), border: Border.all(color: colors.outlineVariant),
boxShadow: const [ boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)), BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)),
], ],
@ -692,7 +720,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
? Icons.radar ? Icons.radar
: Icons.location_off, : Icons.location_off,
color: broadcastState.isBroadcasting color: broadcastState.isBroadcasting
? const Color(0xFF00E5FF) ? colors.cyberCyan
: Colors.white38, : Colors.white38,
size: 20, size: 20,
), ),
@ -714,8 +742,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
style: TextStyle( style: TextStyle(
color: broadcastState.isBroadcasting color: broadcastState.isBroadcasting
? (broadcastState.isPaused ? (broadcastState.isPaused
? Colors.amber ? colors.warningAmber
: const Color(0xFF00E5FF)) : colors.cyberCyan)
: Colors.white60, : Colors.white60,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 11, fontSize: 11,
@ -743,7 +771,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
key: const ValueKey('live_ride_broadcast_switch'), key: const ValueKey('live_ride_broadcast_switch'),
value: broadcastState.isOptedIn, value: broadcastState.isOptedIn,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
activeThumbColor: const Color(0xFF00E5FF), activeThumbColor: colors.cyberCyan,
onChanged: (val) => broadcastController.setOptIn(val), onChanged: (val) => broadcastController.setOptIn(val),
), ),
], ],
@ -752,18 +780,18 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
const SizedBox(width: 8), const SizedBox(width: 8),
// Recenter Map Button // Recenter Map Button (min 48x48 tap target)
IconButton( IconButton(
key: const ValueKey('live_ride_recenter_button'), key: const ValueKey('live_ride_recenter_button'),
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.white10, backgroundColor: colors.surfaceContainer,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
minimumSize: const Size(36, 36), minimumSize: const Size(48, 48),
), ),
icon: const Icon( icon: const Icon(
Icons.my_location, Icons.my_location,
color: Colors.white70, color: Colors.white70,
size: 18, size: 20,
), ),
tooltip: 'Center on Me', tooltip: 'Center on Me',
onPressed: () { onPressed: () {
@ -792,13 +820,13 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
ElevatedButton.icon( ElevatedButton.icon(
key: const ValueKey('live_ride_roster_button'), key: const ValueKey('live_ride_roster_button'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15), backgroundColor: colors.cyberCyan.withValues(alpha: 0.15),
foregroundColor: const Color(0xFF00E5FF), foregroundColor: colors.cyberCyan,
side: const BorderSide(color: Color(0xFF00E5FF)), side: BorderSide(color: colors.cyberCyan),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), 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( label: Text(
'${participants.length}', '${participants.length}',
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
@ -809,6 +837,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
presences: presences, presences: presences,
leadPresence: leadPresence, leadPresence: leadPresence,
currentUid: currentUid, currentUid: currentUid,
colors: colors,
), ),
), ),
], ],
@ -822,10 +851,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
required List<LiveGroupPresence> presences, required List<LiveGroupPresence> presences,
required LiveGroupPresence? leadPresence, required LiveGroupPresence? leadPresence,
required String? currentUid, required String? currentUid,
required MotorSignalThemeColors colors,
}) { }) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
backgroundColor: const Color(0xFF161922), backgroundColor: colors.surfaceContainerHigh,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
), ),
@ -835,12 +865,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Icons.people, color: Color(0xFF00E5FF), size: 18), Icon(Icons.people, color: colors.cyberCyan, size: 18),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text( Text(
'LIVE RIDER ROSTER', 'LIVE RIDER ROSTER',
style: TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
letterSpacing: 1.0, letterSpacing: 1.0,
fontSize: 13, fontSize: 13,
@ -853,13 +883,14 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
), ),
], ],
), ),
const Divider(height: 20, color: Colors.white12), Divider(height: 20, color: colors.outlineVariant),
for (final p in participants) ...[ for (final p in participants) ...[
_buildRosterItem( _buildRosterItem(
participant: p, participant: p,
presence: _findPresence(presences, p.userId), presence: _findPresence(presences, p.userId),
leadPresence: leadPresence, leadPresence: leadPresence,
isCaller: p.userId == currentUid, isCaller: p.userId == currentUid,
colors: colors,
onCenter: (pos) { onCenter: (pos) {
Navigator.pop(ctx); Navigator.pop(ctx);
setState(() { setState(() {
@ -881,6 +912,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
required LiveGroupPresence? presence, required LiveGroupPresence? presence,
required LiveGroupPresence? leadPresence, required LiveGroupPresence? leadPresence,
required bool isCaller, required bool isCaller,
required MotorSignalThemeColors colors,
required ValueChanged<AppLatLng> onCenter, required ValueChanged<AppLatLng> onCenter,
}) { }) {
// Freshness // Freshness
@ -910,12 +942,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
return Container( return Container(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1B1E28), color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: isCaller color: isCaller
? const Color(0xFF00E5FF).withValues(alpha: 0.5) ? colors.cyberCyan.withValues(alpha: 0.5)
: Colors.white10, : colors.outlineVariant,
), ),
), ),
child: Row( child: Row(
@ -925,7 +957,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 18, radius: 18,
backgroundColor: Colors.white10, backgroundColor: colors.surfaceContainerHigh,
child: Text( child: Text(
participant.riderDisplayName.isNotEmpty participant.riderDisplayName.isNotEmpty
? participant.riderDisplayName[0].toUpperCase() ? participant.riderDisplayName[0].toUpperCase()
@ -937,12 +969,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
), ),
), ),
if (isLive) if (isLive)
const Positioned( Positioned(
bottom: 0, bottom: 0,
right: 0, right: 0,
child: CircleAvatar( child: CircleAvatar(
radius: 5, radius: 5,
backgroundColor: Colors.greenAccent, backgroundColor: colors.launchGreen,
), ),
), ),
], ],
@ -965,7 +997,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: isCaller color: isCaller
? const Color(0xFF00E5FF) ? colors.cyberCyan
: Colors.white, : Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 13, fontSize: 13,
@ -982,6 +1014,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
decoration: BoxDecoration( decoration: BoxDecoration(
color: _roleColor( color: _roleColor(
participant.role, participant.role,
colors,
).withValues(alpha: 0.2), ).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
), ),
@ -990,7 +1023,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
style: TextStyle( style: TextStyle(
fontSize: 9, fontSize: 9,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _roleColor(participant.role), color: _roleColor(participant.role, colors),
), ),
), ),
), ),
@ -1007,7 +1040,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: _statusColor(presence.status), color: _statusColor(presence.status, colors),
), ),
) )
else else
@ -1041,18 +1074,18 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
isLive ? 'LIVE' : '${presence.secondsAgo}s ago', isLive ? 'LIVE' : '${presence.secondsAgo}s ago',
style: TextStyle( style: TextStyle(
color: isLive color: isLive
? Colors.greenAccent ? colors.launchGreen
: (isRecent ? Colors.amber : Colors.white38), : (isRecent ? colors.warningAmber : Colors.white38),
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
IconButton( IconButton(
icon: const Icon( icon: Icon(
Icons.gps_fixed, Icons.gps_fixed,
size: 16, size: 16,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
), ),
constraints: const BoxConstraints(minWidth: 32, minHeight: 32), constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@ -1070,12 +1103,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
); );
} }
Color _roleColor(GroupRideRole role) { Color _roleColor(GroupRideRole role, MotorSignalThemeColors colors) {
switch (role) { switch (role) {
case GroupRideRole.organizer: case GroupRideRole.organizer:
return const Color(0xFF00E5FF); return colors.cyberCyan;
case GroupRideRole.lead: case GroupRideRole.lead:
return const Color(0xFFFFD700); return colors.warningAmber;
case GroupRideRole.sweep: case GroupRideRole.sweep:
return Colors.orangeAccent; return Colors.orangeAccent;
case GroupRideRole.participant: case GroupRideRole.participant:
@ -1083,20 +1116,20 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
} }
} }
Color _statusColor(LivePresenceStatus status) { Color _statusColor(LivePresenceStatus status, MotorSignalThemeColors colors) {
switch (status) { switch (status) {
case LivePresenceStatus.riding: case LivePresenceStatus.riding:
return Colors.greenAccent; return colors.launchGreen;
case LivePresenceStatus.stopped: case LivePresenceStatus.stopped:
return Colors.white70; return Colors.white70;
case LivePresenceStatus.needFuel: case LivePresenceStatus.needFuel:
return Colors.orangeAccent; return Colors.orangeAccent;
case LivePresenceStatus.regrouping: case LivePresenceStatus.regrouping:
return Colors.amber; return colors.warningAmber;
case LivePresenceStatus.delayed: case LivePresenceStatus.delayed:
return Colors.redAccent; return colors.raceRed;
case LivePresenceStatus.arrived: case LivePresenceStatus.arrived:
return const Color(0xFF00E5FF); return colors.cyberCyan;
case LivePresenceStatus.offline: case LivePresenceStatus.offline:
return Colors.white38; return Colors.white38;
} }
@ -1136,6 +1169,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
required String? currentUid, required String? currentUid,
required String? leadUid, required String? leadUid,
MotorSignalRoute? attachedRoute, MotorSignalRoute? attachedRoute,
required MotorSignalThemeColors colors,
}) { }) {
final markers = <AppMapMarker>[]; final markers = <AppMapMarker>[];
@ -1152,10 +1186,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
: 'Meetup', : 'Meetup',
iconWidget: Container( iconWidget: Container(
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.greenAccent, color: colors.launchGreen,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: const [
BoxShadow( BoxShadow(
color: Colors.black45, color: Colors.black45,
blurRadius: 4, blurRadius: 4,
@ -1186,10 +1220,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
label: stop.displayName, label: stop.displayName,
iconWidget: Container( iconWidget: Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: const [
BoxShadow( BoxShadow(
color: Colors.black45, color: Colors.black45,
blurRadius: 4, blurRadius: 4,
@ -1276,10 +1310,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
: 'Destination', : 'Destination',
iconWidget: Container( iconWidget: Container(
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.redAccent, color: colors.raceRed,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: const [
BoxShadow( BoxShadow(
color: Colors.black45, color: Colors.black45,
blurRadius: 4, blurRadius: 4,
@ -1310,11 +1344,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
final isCaller = presence.userId == currentUid; final isCaller = presence.userId == currentUid;
// Color scheme // Color scheme
Color markerColor = const Color(0xFF00E5FF); Color markerColor = colors.cyberCyan;
if (isLead) markerColor = const Color(0xFFFFD700); // Gold for lead if (isLead) markerColor = colors.warningAmber; // Gold for lead
if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep
if (presence.status == LivePresenceStatus.regrouping) if (presence.status == LivePresenceStatus.regrouping)
markerColor = Colors.amber; markerColor = colors.warningAmber;
if (presence.status == LivePresenceStatus.needFuel) if (presence.status == LivePresenceStatus.needFuel)
markerColor = Colors.deepOrangeAccent; markerColor = Colors.deepOrangeAccent;
@ -1367,12 +1401,13 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
BuildContext context, BuildContext context,
GroupRide ride, GroupRide ride,
String? currentUid, String? currentUid,
MotorSignalThemeColors colors,
) { ) {
if (currentUid == null) return; if (currentUid == null) return;
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF161922), backgroundColor: colors.surfaceContainerHigh,
title: const Text( title: const Text(
'End Group Ride?', 'End Group Ride?',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
@ -1391,7 +1426,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
), ),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent, backgroundColor: colors.raceRed,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
onPressed: () async { onPressed: () async {

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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/auth/auth_providers.dart';
import '../../services/social/rider_profile_models.dart'; import '../../services/social/rider_profile_models.dart';
import '../../services/social/social_providers.dart'; import '../../services/social/social_providers.dart';
@ -273,23 +275,18 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.motorColors;
final authState = ref.watch(authNotifierProvider); final authState = ref.watch(authNotifierProvider);
final isAuthed = authState.isAuthenticated; 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) { if (!isAuthed) {
return Scaffold( return Scaffold(
backgroundColor: bgColor, backgroundColor: colors.surfaceContainer,
appBar: AppBar( appBar: AppBar(
title: Text( backgroundColor: colors.surfaceContainerHigh,
title: const Text(
'Friends & Riders', 'Friends & Riders',
style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800), style: TextStyle(fontWeight: FontWeight.w800),
), ),
), ),
body: Center( body: Center(
@ -298,16 +295,15 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon( Icon(
Icons.group_outlined, Icons.group_outlined,
size: 64, size: 64,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( const Text(
'MOTORCLOUD ACCOUNT REQUIRED', 'MOTORCLOUD ACCOUNT REQUIRED',
style: TextStyle( style: TextStyle(
fontFamily: titleFont,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 16, fontSize: 16,
color: Colors.white, color: Colors.white,
@ -323,7 +319,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
), ),
icon: const Icon(Icons.login), icon: const Icon(Icons.login),
@ -345,11 +341,12 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
final friendsCount = friendsAsync.value?.length ?? 0; final friendsCount = friendsAsync.value?.length ?? 0;
return Scaffold( return Scaffold(
backgroundColor: bgColor, backgroundColor: colors.surfaceContainer,
appBar: AppBar( appBar: AppBar(
title: Text( backgroundColor: colors.surfaceContainerHigh,
title: const Text(
'Friends & Riders', 'Friends & Riders',
style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800), style: TextStyle(fontWeight: FontWeight.w800),
), ),
actions: [ actions: [
IconButton( IconButton(
@ -360,8 +357,10 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
], ],
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
indicatorColor: const Color(0xFF00E5FF), isScrollable: true,
labelColor: const Color(0xFF00E5FF), tabAlignment: TabAlignment.start,
indicatorColor: colors.cyberCyan,
labelColor: colors.cyberCyan,
unselectedLabelColor: Colors.white60, unselectedLabelColor: Colors.white60,
tabs: [ tabs: [
Tab(text: 'FRIENDS ($friendsCount)'), Tab(text: 'FRIENDS ($friendsCount)'),
@ -378,7 +377,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.amberAccent, color: colors.warningAmber,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Text( child: Text(
@ -403,8 +402,15 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
children: [ children: [
// TAB 1: FRIENDS LIST // TAB 1: FRIENDS LIST
friendsAsync.when( friendsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => Center(
error: (err, _) => Center(child: Text('Error: $err')), child: CircularProgressIndicator(color: colors.cyberCyan),
),
error: (err, _) => Center(
child: Text(
'Error: $err',
style: TextStyle(color: colors.raceRed),
),
),
data: (friends) { data: (friends) {
if (friends.isEmpty) { if (friends.isEmpty) {
return Center( return Center(
@ -437,7 +443,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
), ),
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
@ -450,11 +456,85 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
); );
} }
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,
),
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),
],
),
),
);
},
);
}
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
itemCount: friends.length, itemCount: friends.length,
separatorBuilder: (context, index) => separatorBuilder: (context, index) =>
const Divider(color: Colors.white10), Divider(color: colors.outlineVariant),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final friend = friends[index]; final friend = friends[index];
return ListTile( return ListTile(
@ -474,43 +554,14 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
), ),
subtitle: Text( subtitle: Text(
friend.displayHandle, friend.displayHandle,
style: const TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontSize: 12, fontSize: 12,
), ),
), ),
trailing: PopupMenuButton<String>( trailing: _buildFriendMenu(friend, colors),
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);
}
}, },
itemBuilder: (context) => [
const PopupMenuItem(
value: 'remove',
child: Row(
children: [
Icon(Icons.person_remove_outlined, size: 18),
SizedBox(width: 8),
Text('Remove Friend'),
],
),
),
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)),
],
),
),
],
),
); );
}, },
); );
@ -522,10 +573,10 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
children: [ children: [
// Incoming Section // Incoming Section
const Text( Text(
'INCOMING REQUESTS', 'INCOMING REQUESTS',
style: TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
letterSpacing: 0.8, letterSpacing: 0.8,
@ -533,13 +584,16 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
incomingAsync.when( incomingAsync.when(
loading: () => const Center( loading: () => Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: CircularProgressIndicator(), child: CircularProgressIndicator(color: colors.cyberCyan),
), ),
), ),
error: (err, _) => Text('Error: $err'), error: (err, _) => Text(
'Error: $err',
style: TextStyle(color: colors.raceRed),
),
data: (requests) { data: (requests) {
if (requests.isEmpty) { if (requests.isEmpty) {
return const Padding( return const Padding(
@ -554,8 +608,12 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
return Column( return Column(
children: requests.map((req) { children: requests.map((req) {
return Card( return Card(
color: Colors.white.withValues(alpha: 0.05), color: colors.surfaceContainerHigh,
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: colors.outlineVariant),
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Row( child: Row(
@ -580,8 +638,8 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
), ),
Text( Text(
'@${req.senderHandle}', '@${req.senderHandle}',
style: const TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontSize: 11, fontSize: 11,
), ),
), ),
@ -589,17 +647,17 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
), ),
), ),
IconButton( IconButton(
icon: const Icon( icon: Icon(
Icons.check_circle_outline, Icons.check_circle_outline,
color: Colors.greenAccent, color: colors.launchGreen,
), ),
tooltip: 'Accept', tooltip: 'Accept',
onPressed: () => _acceptFriendRequest(req), onPressed: () => _acceptFriendRequest(req),
), ),
IconButton( IconButton(
icon: const Icon( icon: Icon(
Icons.cancel_outlined, Icons.cancel_outlined,
color: Colors.redAccent, color: colors.raceRed,
), ),
tooltip: 'Decline', tooltip: 'Decline',
onPressed: () => _declineFriendRequest(req), onPressed: () => _declineFriendRequest(req),
@ -627,7 +685,10 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
const SizedBox(height: 8), const SizedBox(height: 8),
outgoingAsync.when( outgoingAsync.when(
loading: () => const SizedBox.shrink(), loading: () => const SizedBox.shrink(),
error: (err, _) => Text('Error: $err'), error: (err, _) => Text(
'Error: $err',
style: TextStyle(color: colors.raceRed),
),
data: (requests) { data: (requests) {
if (requests.isEmpty) { if (requests.isEmpty) {
return const Padding( return const Padding(
@ -685,10 +746,13 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
Expanded( Expanded(
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
decoration: const InputDecoration( style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'Enter @handle (e.g. speed_demon)', hintText: 'Enter @handle (e.g. speed_demon)',
prefixIcon: Icon(Icons.search), prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(), border: const OutlineInputBorder(),
filled: true,
fillColor: colors.surfaceContainerHigh,
), ),
onSubmitted: (_) => _performSearch(), onSubmitted: (_) => _performSearch(),
), ),
@ -696,7 +760,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
const SizedBox(width: 8), const SizedBox(width: 8),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
@ -735,7 +799,11 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
if (_searchResult != null) ...[ if (_searchResult != null) ...[
Card( Card(
color: Colors.white.withValues(alpha: 0.06), color: colors.surfaceContainerHigh,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: colors.outlineVariant),
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
@ -764,8 +832,8 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
), ),
Text( Text(
_searchResult!.displayHandle, _searchResult!.displayHandle,
style: const TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 13, fontSize: 13,
), ),
@ -796,7 +864,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
), ),
), ),
], ],
const Divider(height: 24, color: Colors.white12), Divider(height: 24, color: colors.outlineVariant),
// Relationship Action // Relationship Action
Row( Row(
@ -813,32 +881,36 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
) )
else if (_searchResult!.relationship == else if (_searchResult!.relationship ==
FriendshipStatus.friend) FriendshipStatus.friend)
const Chip( Chip(
avatar: Icon(Icons.check, size: 16), avatar: Icon(Icons.check,
label: Text('Friends'), size: 16, color: colors.cyberCyan),
backgroundColor: Color(0xFF003840), label: const Text('Friends'),
backgroundColor:
colors.cyberCyan.withValues(alpha: 0.15),
labelStyle: TextStyle( labelStyle: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
), ),
) )
else if (_searchResult!.relationship == else if (_searchResult!.relationship ==
FriendshipStatus.requestOutgoing) FriendshipStatus.requestOutgoing)
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.white12, backgroundColor:
colors.surfaceContainerHighest,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
icon: const Icon(Icons.cancel_outlined, size: 16), icon: const Icon(Icons.cancel_outlined,
size: 16),
label: const Text('CANCEL REQUEST'), label: const Text('CANCEL REQUEST'),
onPressed: () => onPressed: () =>
_cancelFriendRequest(_searchResult!.uid), _cancelFriendRequest(_searchResult!.uid),
) )
else if (_searchResult!.relationship == else if (_searchResult!.relationship ==
FriendshipStatus.requestIncoming) FriendshipStatus.requestIncoming)
const Text( Text(
'Check Requests tab to respond', 'Check Requests tab to respond',
style: TextStyle( style: TextStyle(
color: Colors.amberAccent, color: colors.warningAmber,
fontSize: 12, fontSize: 12,
), ),
) )
@ -853,7 +925,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
else else
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
), ),
icon: const Icon(Icons.person_add, size: 16), icon: const Icon(Icons.person_add, size: 16),
@ -891,4 +963,41 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
), ),
); );
} }
Widget _buildFriendMenu(
FriendSummary friend, MotorSignalThemeColors colors) {
return PopupMenuButton<String>(
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)),
],
),
),
],
);
}
} }

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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/auth/auth_providers.dart';
import '../../services/social/handle_validator.dart'; import '../../services/social/handle_validator.dart';
import '../../services/social/rider_profile_models.dart'; import '../../services/social/rider_profile_models.dart';
@ -75,11 +77,14 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
.toLowerCase() .toLowerCase()
.replaceAll(RegExp(r'[^a-z0-9_]'), '_'); .replaceAll(RegExp(r'[^a-z0-9_]'), '_');
if (candidate.length >= 3) { 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) { } else if (user.email != null) {
final candidate = user.email!.split('@').first.replaceAll(RegExp(r'[^a-z0-9_]'), '_'); final candidate =
_handleController.text = candidate.substring(0, candidate.length.clamp(0, 18)); 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; _selectedAvatarPreset = PresetAvatar.availablePresets.first.id;
@ -142,21 +147,18 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.motorColors;
final authState = ref.watch(authNotifierProvider); final authState = ref.watch(authNotifierProvider);
final isAuthed = authState.isAuthenticated; 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) { if (!isAuthed) {
return Scaffold( return Scaffold(
backgroundColor: bgColor, backgroundColor: colors.surfaceContainer,
appBar: AppBar( appBar: AppBar(
title: Text( backgroundColor: colors.surfaceContainerHigh,
title: const Text(
'Rider Profile', 'Rider Profile',
style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800), style: TextStyle(fontWeight: FontWeight.w800),
), ),
), ),
body: Center( body: Center(
@ -165,16 +167,15 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon( Icon(
Icons.person_pin_circle_outlined, Icons.person_pin_circle_outlined,
size: 64, size: 64,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( const Text(
'MOTORCLOUD ACCOUNT REQUIRED', 'MOTORCLOUD ACCOUNT REQUIRED',
style: TextStyle( style: TextStyle(
fontFamily: titleFont,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 16, fontSize: 16,
color: Colors.white, color: Colors.white,
@ -190,7 +191,7 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
), ),
icon: const Icon(Icons.login), icon: const Icon(Icons.login),
@ -207,11 +208,12 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
final profileAsync = ref.watch(myRiderProfileProvider); final profileAsync = ref.watch(myRiderProfileProvider);
return Scaffold( return Scaffold(
backgroundColor: bgColor, backgroundColor: colors.surfaceContainer,
appBar: AppBar( appBar: AppBar(
title: Text( backgroundColor: colors.surfaceContainerHigh,
title: const Text(
'Rider Profile', 'Rider Profile',
style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800), style: TextStyle(fontWeight: FontWeight.w800),
), ),
actions: [ actions: [
IconButton( IconButton(
@ -222,8 +224,15 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
], ],
), ),
body: profileAsync.when( body: profileAsync.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => Center(
error: (err, _) => Center(child: Text('Error loading profile: $err')), child: CircularProgressIndicator(color: colors.cyberCyan),
),
error: (err, _) => Center(
child: Text(
'Error loading profile: $err',
style: TextStyle(color: colors.raceRed),
),
),
data: (profile) { data: (profile) {
if (profile != null) { if (profile != null) {
_populateFromProfile(profile); _populateFromProfile(profile);
@ -231,10 +240,190 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
_populateDefaults(); _populateDefaults();
} }
return SingleChildScrollView( return ResponsiveLayout(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 100.0), builder: (context, snapshot) {
child: Form( final isWide = snapshot.sizeClass == WindowSizeClass.expanded ||
(snapshot.isLandscape && snapshot.width >= 720);
if (isWide) {
return _buildWideProfile(context, colors);
}
return _buildCompactProfile(context, colors);
},
);
},
),
);
}
Widget _buildWideProfile(
BuildContext context, MotorSignalThemeColors colors) {
return Form(
key: _formKey, 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@ -254,11 +443,10 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
_handleController.text.isNotEmpty _handleController.text.isNotEmpty
? '@${_handleController.text}' ? '@${_handleController.text}'
: '@handle', : '@handle',
style: const TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 14, fontSize: 14,
fontFamily: 'Inter',
), ),
), ),
], ],
@ -266,39 +454,9 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Messages if (_errorMessage != null) _buildErrorMessage(colors),
if (_errorMessage != null) ...[ if (_successMessage != null) _buildSuccessMessage(colors),
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( AvatarSelector(
selectedPresetId: _selectedAvatarPreset, selectedPresetId: _selectedAvatarPreset,
onSelected: (presetId) { onSelected: (presetId) {
@ -309,71 +467,130 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Handle input
TextFormField( TextFormField(
controller: _handleController, controller: _handleController,
decoration: const InputDecoration( style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'Rider Handle', labelText: 'Rider Handle',
hintText: 'e.g. canyon_runner', hintText: 'e.g. canyon_runner',
prefixText: '@', prefixText: '@',
helperText: 'Unique, 3-20 characters, lowercase letters, numbers, and underscores', helperText: 'Unique, 3-20 characters: letters, numbers, underscores',
border: OutlineInputBorder(), helperStyle:
const TextStyle(color: Colors.white38, fontSize: 11),
border: const OutlineInputBorder(),
filled: true,
fillColor: colors.surfaceContainerHigh,
), ),
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Display Name input
TextFormField( TextFormField(
controller: _displayNameController, controller: _displayNameController,
decoration: const InputDecoration( style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'Display Name', labelText: 'Display Name',
hintText: 'e.g. Alex Rivera', hintText: 'e.g. Alex Rivera',
border: OutlineInputBorder(), border: const OutlineInputBorder(),
filled: true,
fillColor: colors.surfaceContainerHigh,
), ),
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Region input
TextFormField( TextFormField(
controller: _regionController, controller: _regionController,
decoration: const InputDecoration( style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'Riding Region (Optional)', labelText: 'Riding Region (Optional)',
hintText: 'e.g. Blue Ridge Mountains, NC', hintText: 'e.g. Blue Ridge Mountains, NC',
border: OutlineInputBorder(), border: const OutlineInputBorder(),
filled: true,
fillColor: colors.surfaceContainerHigh,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Bio input
TextFormField( TextFormField(
controller: _bioController, controller: _bioController,
style: const TextStyle(color: Colors.white),
maxLines: 3, maxLines: 3,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Rider Bio (Optional)', labelText: 'Rider Bio (Optional)',
hintText: 'Tell fellow riders about what you ride and your riding style...', hintText:
border: OutlineInputBorder(), 'Tell fellow riders about what you ride and your riding style...',
border: const OutlineInputBorder(),
filled: true,
fillColor: colors.surfaceContainerHigh,
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Privacy Settings Card _buildPrivacyCard(colors),
Card( const SizedBox(height: 24),
color: Colors.white.withValues(alpha: 0.05),
_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( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Row( Row(
children: [ children: [
Icon(Icons.shield_outlined, color: Color(0xFF00E5FF), size: 18), Icon(Icons.shield_outlined,
SizedBox(width: 8), color: colors.cyberCyan, size: 18),
const SizedBox(width: 8),
Text( Text(
'PRIVACY CONTROLS', 'PRIVACY CONTROLS',
style: TextStyle( style: TextStyle(
color: Color(0xFF00E5FF), color: colors.cyberCyan,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 12, fontSize: 12,
letterSpacing: 0.8, letterSpacing: 0.8,
@ -384,48 +601,57 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: const Text('Discoverable by Handle', style: TextStyle(fontSize: 14)), activeThumbColor: colors.cyberCyan,
title: const Text('Discoverable by Handle',
style: TextStyle(fontSize: 14)),
subtitle: const Text( subtitle: const Text(
'Allow other riders to find your profile by searching your @handle.', 'Allow other riders to find your profile by searching your @handle.',
style: TextStyle(fontSize: 11, color: Colors.white60), style: TextStyle(fontSize: 11, color: Colors.white60),
), ),
value: _isSearchableByHandle, value: _isSearchableByHandle,
onChanged: (val) => setState(() => _isSearchableByHandle = val), onChanged: (val) =>
setState(() => _isSearchableByHandle = val),
), ),
const Divider(color: Colors.white12), Divider(color: colors.outlineVariant),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: const Text('Public Profile', style: TextStyle(fontSize: 14)), activeThumbColor: colors.cyberCyan,
title: const Text('Public Profile',
style: TextStyle(fontSize: 14)),
subtitle: const Text( subtitle: const Text(
'When off, non-friends only see your handle and avatar, hiding bio and region.', 'When off, non-friends only see your handle and avatar, hiding bio and region.',
style: TextStyle(fontSize: 11, color: Colors.white60), style: TextStyle(fontSize: 11, color: Colors.white60),
), ),
value: _isProfilePublic, value: _isProfilePublic,
onChanged: (val) => setState(() => _isProfilePublic = val), onChanged: (val) =>
setState(() => _isProfilePublic = val),
), ),
const Divider(color: Colors.white12), Divider(color: colors.outlineVariant),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: const Text('Allow Friend Requests', style: TextStyle(fontSize: 14)), activeThumbColor: colors.cyberCyan,
title: const Text('Allow Friend Requests',
style: TextStyle(fontSize: 14)),
subtitle: const Text( subtitle: const Text(
'Allow other riders to send you friend requests.', 'Allow other riders to send you friend requests.',
style: TextStyle(fontSize: 11, color: Colors.white60), style: TextStyle(fontSize: 11, color: Colors.white60),
), ),
value: _allowFriendRequests, value: _allowFriendRequests,
onChanged: (val) => setState(() => _allowFriendRequests = val), onChanged: (val) =>
setState(() => _allowFriendRequests = val),
), ),
], ],
), ),
), ),
), );
const SizedBox(height: 24), }
// Save Button Widget _buildSaveButton(MotorSignalThemeColors colors) {
SizedBox( return SizedBox(
height: 48, height: 48,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@ -443,16 +669,10 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
) )
: const Text( : const Text(
'SAVE PROFILE', 'SAVE PROFILE',
style: TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.8), style:
TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.8),
), ),
), ),
),
],
),
),
);
},
),
); );
} }
} }

View File

@ -68,7 +68,7 @@ class AvatarBadge extends StatelessWidget {
padding: const EdgeInsets.all(3), padding: const EdgeInsets.all(3),
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, 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, child: inner,
); );

View File

@ -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<AuthState>.broadcast();
AuthUser? user;
_FakeAuth({this.user});
@override
Stream<AuthState> 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<void> deleteCurrentUser() async {}
@override
void dispose() => _states.close();
@override
Future<String?> getIdToken([bool forceRefresh = false]) async => 'token';
@override
Future<void> reauthenticate({String? password}) async {}
@override
Future<void> reloadCurrentUser() async {}
@override
Future<void> sendEmailVerification() async {}
@override
Future<void> sendPasswordResetEmail(String email) async {}
@override
Future<void> signInAnonymously() async {}
@override
Future<void> signInWithEmailAndPassword(String email, String password) async {}
@override
Future<void> createUserWithEmailAndPassword(String email, String password,
{String? displayName}) async {}
@override
Future<void> signInWithGoogle() async {}
@override
Future<void> signOut() async {}
}
class _FakeLiveBroadcastController extends StateNotifier<LiveBroadcastState>
implements LiveBroadcastController {
_FakeLiveBroadcastController(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
@override
Future<void> setOptIn(bool optIn) async {}
@override
Future<void> setStatus(LivePresenceStatus status) async {}
@override
Future<void> triggerRegroup() async {}
@override
Future<void> 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<GroupRideParticipant> 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<LiveGroupPresence> 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<FriendSummary> 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<FriendRequest> 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 = <String, Size>{
'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(<FriendRequest>[]),
),
],
child: MaterialApp(
theme: AppTheme.darkTheme,
home: const FriendsScreen(),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Friends & Riders'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
});
}