fix(social): resolve community feed discoverability, tab routing, live comments, and responsive overflows

This commit is contained in:
Developer 2026-09-30 09:49:02 -04:00
parent 71cd994b2f
commit d378db4c86
11 changed files with 606 additions and 253 deletions

View File

@ -139,7 +139,13 @@ final appRouter = GoRouter(
routes: [ routes: [
GoRoute( GoRoute(
path: '/social', path: '/social',
builder: (context, state) => const SocialHubScreen(), builder: (context, state) {
final tabParam = state.uri.queryParameters['tab'];
final initialTab = tabParam == 'hub' || tabParam == 'network'
? SocialHubTab.hub
: SocialHubTab.feed;
return SocialHubScreen(initialTab: initialTab);
},
routes: [ routes: [
GoRoute( GoRoute(
path: 'profile', path: 'profile',
@ -293,5 +299,13 @@ final appRouter = GoRouter(
path: '/launch', path: '/launch',
builder: (context, state) => const PerformanceLaunchScreen(), builder: (context, state) => const PerformanceLaunchScreen(),
), ),
GoRoute(
path: '/social/feed',
builder: (context, state) => const SocialFeedScreen(),
),
GoRoute(
path: '/feed',
builder: (context, state) => const SocialFeedScreen(),
),
], ],
); );

View File

@ -20,6 +20,7 @@ import 'ride_action_button.dart';
import 'ride_status_chip.dart'; import 'ride_status_chip.dart';
import 'speed_readout.dart'; import 'speed_readout.dart';
import '../../../services/alerts/widgets/quick_swarm_report_modal.dart'; import '../../../services/alerts/widgets/quick_swarm_report_modal.dart';
import '../../social/feed/widgets/quick_squad_feed_button.dart';
/// The single authoritative responsive layout for the MotorSignal dashboard. /// The single authoritative responsive layout for the MotorSignal dashboard.
/// ///
@ -82,6 +83,8 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
SizedBox(width: 4), SizedBox(width: 4),
QuickSwarmReportButton(compact: true), QuickSwarmReportButton(compact: true),
SizedBox(width: 4), SizedBox(width: 4),
QuickSquadFeedButton(compact: true),
SizedBox(width: 4),
GPSStatusIndicator(), GPSStatusIndicator(),
SizedBox(width: 8), SizedBox(width: 8),
HeadingIndicator(), HeadingIndicator(),

View File

@ -18,13 +18,76 @@ import 'widgets/share_session_modal.dart';
/// snapshot cards, custom route vector track visualization, apex angle markers, /// snapshot cards, custom route vector track visualization, apex angle markers,
/// glove-friendly fist bump kudos reactions, comment drawers, and session publishing. /// glove-friendly fist bump kudos reactions, comment drawers, and session publishing.
class SocialFeedScreen extends ConsumerWidget { class SocialFeedScreen extends ConsumerWidget {
const SocialFeedScreen({super.key}); final bool showAppBar;
const SocialFeedScreen({super.key, this.showAppBar = true});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colors = context.motorColors; final colors = context.motorColors;
final activeFilter = ref.watch(feedFilterStateProvider); final activeFilter = ref.watch(feedFilterStateProvider);
final feedAsync = ref.watch(socialFeedStreamProvider); final items = ref.watch(socialFeedNotifierProvider);
final content = SafeArea(
child: ResponsiveLayout(
builder: (context, responsive) {
return Stack(
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Top Context Header & Filter Bar
_buildTopFilterSection(context, ref, activeFilter, colors),
// Feed List
Expanded(
child: items.isEmpty
? _buildEmptyFeedState(context, activeFilter, colors)
: ListView.separated(
key: const ValueKey('social_feed_list_view'),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 100),
itemCount: items.length,
separatorBuilder: (_, _) => const SizedBox(height: 16),
itemBuilder: (context, index) {
return _SocialFeedCard(
key: ValueKey('feed_card_${items[index].id}'),
item: items[index],
);
},
),
),
],
),
),
),
// Sticky Glove-Friendly Share Session Action Bar
Positioned(
left: 16,
right: 16,
bottom: 12,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: _buildStickyShareButton(context),
),
),
),
],
);
},
),
);
if (!showAppBar) {
return Container(
color: const Color(0xFF101419),
child: content,
);
}
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF101419), backgroundColor: const Color(0xFF101419),
@ -93,81 +156,14 @@ class SocialFeedScreen extends ConsumerWidget {
icon: const Icon(Icons.refresh_rounded, color: Color(0xFF849396)), icon: const Icon(Icons.refresh_rounded, color: Color(0xFF849396)),
tooltip: 'Refresh Feed', tooltip: 'Refresh Feed',
onPressed: () { onPressed: () {
ref.invalidate(socialFeedNotifierProvider);
ref.invalidate(socialFeedStreamProvider); ref.invalidate(socialFeedStreamProvider);
}, },
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
), ),
body: SafeArea( body: content,
child: ResponsiveLayout(
builder: (context, responsive) {
return Stack(
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Top Context Header & Filter Bar
_buildTopFilterSection(context, ref, activeFilter, colors),
// Feed List
Expanded(
child: feedAsync.when(
data: (items) {
if (items.isEmpty) {
return _buildEmptyFeedState(context, activeFilter, colors);
}
return ListView.separated(
key: const ValueKey('social_feed_list_view'),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 80),
itemCount: items.length,
separatorBuilder: (_, _) => const SizedBox(height: 16),
itemBuilder: (context, index) {
return _SocialFeedCard(
key: ValueKey('feed_card_${items[index].id}'),
item: items[index],
);
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(
color: AppColors.electricCyan,
),
),
error: (err, _) => Center(
child: Text(
'Error loading feed: $err',
style: const TextStyle(color: Colors.redAccent),
),
),
),
),
],
),
),
),
// Sticky Glove-Friendly Share Session Action Bar
Positioned(
left: 16,
right: 16,
bottom: 12,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: _buildStickyShareButton(context),
),
),
),
],
);
},
),
),
); );
} }
@ -869,8 +865,10 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
children: [ children: [
const Text('👊', style: TextStyle(fontSize: 18)), const Text('👊', style: TextStyle(fontSize: 18)),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Flexible(
child: Text(
'$bumpCount Bumps', '$bumpCount Bumps',
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontHeadline, fontFamily: AppTypography.fontHeadline,
fontSize: 13, fontSize: 13,
@ -880,6 +878,7 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
: Colors.white, : Colors.white,
), ),
), ),
),
], ],
), ),
), ),
@ -991,8 +990,10 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
color: AppColors.electricCyan, color: AppColors.electricCyan,
), ),
SizedBox(width: 8), SizedBox(width: 8),
Text( Flexible(
child: Text(
'INSPECT TELEMETRY GRAPH', 'INSPECT TELEMETRY GRAPH',
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontHeadline, fontFamily: AppTypography.fontHeadline,
fontSize: 11, fontSize: 11,
@ -1001,6 +1002,7 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
color: AppColors.electricCyan, color: AppColors.electricCyan,
), ),
), ),
),
], ],
), ),
), ),

View File

@ -35,6 +35,13 @@ class _FeedCommentsBottomSheetState
extends ConsumerState<FeedCommentsBottomSheet> { extends ConsumerState<FeedCommentsBottomSheet> {
final TextEditingController _commentController = TextEditingController(); final TextEditingController _commentController = TextEditingController();
bool _isSubmitting = false; bool _isSubmitting = false;
late List<FeedPostComment> _localComments;
@override
void initState() {
super.initState();
_localComments = List<FeedPostComment>.from(widget.feedItem.comments);
}
@override @override
void dispose() { void dispose() {
@ -78,6 +85,9 @@ class _FeedCommentsBottomSheetState
_commentController.clear(); _commentController.clear();
if (mounted) { if (mounted) {
setState(() {
_localComments.add(comment);
});
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
} }
} finally { } finally {
@ -91,7 +101,14 @@ class _FeedCommentsBottomSheetState
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.motorColors; final colors = context.motorColors;
final bottomInset = MediaQuery.of(context).viewInsets.bottom; final bottomInset = MediaQuery.of(context).viewInsets.bottom;
final comments = widget.feedItem.comments; final feedItems = ref.watch(socialFeedNotifierProvider);
final livePost = feedItems.cast<SocialFeedItem?>().firstWhere(
(p) => p?.id == widget.feedItem.id,
orElse: () => null,
);
final comments = livePost != null && livePost.comments.isNotEmpty
? livePost.comments
: _localComments;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_typography.dart';
/// Glove-friendly quick launcher button for the Squad & Canyon Community Feed.
///
/// Designed to sit directly in the Dashboard status bar for instantaneous
/// 1-tap access to live rider activity, route snapshots, and kudos.
class QuickSquadFeedButton extends StatelessWidget {
final bool compact;
const QuickSquadFeedButton({super.key, this.compact = false});
@override
Widget build(BuildContext context) {
if (compact) {
return IconButton(
key: const ValueKey('dash_quick_feed_button'),
icon: const Icon(
Icons.dynamic_feed_rounded,
color: AppColors.electricCyan,
size: 19,
),
tooltip: 'Squad & Canyon Feed',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
onPressed: () => context.push('/social/feed'),
);
}
return InkWell(
key: const ValueKey('dash_quick_feed_button'),
borderRadius: BorderRadius.circular(16),
onTap: () => context.push('/social/feed'),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF181C21),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.electricCyan.withValues(alpha: 0.3),
width: 0.8,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 7,
height: 7,
decoration: const BoxDecoration(
color: AppColors.electricCyan,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColors.electricCyan,
blurRadius: 4,
),
],
),
),
const SizedBox(width: 5),
const Text(
'FEED',
style: TextStyle(
fontFamily: AppTypography.fontTelemetry,
fontSize: 10,
fontWeight: FontWeight.w800,
letterSpacing: 0.6,
color: Color(0xFFC3F5FF),
),
),
],
),
),
);
}
}

View File

@ -87,9 +87,16 @@ class _ShareSessionModalState extends ConsumerState<ShareSessionModal> {
authorDisplayName: authorDisplayName, authorDisplayName: authorDisplayName,
authorHandle: authorHandle, authorHandle: authorHandle,
authorBadge: 'PRO', authorBadge: 'PRO',
locationName: _locationController.text.trim(), clubName: 'Bay Area Twisties Club',
rideTitle: _titleController.text.trim(), locationName: _locationController.text.trim().isNotEmpty
bikeModel: _bikeController.text.trim(), ? _locationController.text.trim()
: 'Skyline Blvd & Highway 9',
rideTitle: _titleController.text.trim().isNotEmpty
? _titleController.text.trim()
: 'Morning Canyon Carve',
bikeModel: _bikeController.text.trim().isNotEmpty
? _bikeController.text.trim()
: '2024 Ducati Streetfighter V4',
distanceMiles: dist, distanceMiles: dist,
durationSeconds: durMins * 60, durationSeconds: durMins * 60,
maxLeanAngleDeg: lean, maxLeanAngleDeg: lean,
@ -106,6 +113,9 @@ class _ShareSessionModalState extends ConsumerState<ShareSessionModal> {
], ],
); );
// Reset filter to All Activity so published session is immediately displayed
ref.read(feedFilterStateProvider.notifier).state = FeedFilter.allActivity;
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

View File

@ -8,15 +8,42 @@ import '../../services/auth/auth_providers.dart';
import '../../services/social/social_providers.dart'; import '../../services/social/social_providers.dart';
import 'widgets/avatar_badge.dart'; import 'widgets/avatar_badge.dart';
import 'feed/social_feed_screen.dart';
enum SocialHubTab { hub, feed }
/// The authoritative landing screen for MotorSignal Social & Community. /// The authoritative landing screen for MotorSignal Social & Community.
/// ///
/// Unifies Rider Profile, Friends, and Group Rides under a dedicated, /// Unifies the Community Social Feed & Kudos with Rider Profile, Friends,
/// restrained automotive social home completely decoupled from Settings. /// and Group Rides under a dedicated, restrained automotive social home.
class SocialHubScreen extends ConsumerWidget { class SocialHubScreen extends ConsumerStatefulWidget {
const SocialHubScreen({super.key}); final SocialHubTab initialTab;
const SocialHubScreen({super.key, this.initialTab = SocialHubTab.hub});
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<SocialHubScreen> createState() => _SocialHubScreenState();
}
class _SocialHubScreenState extends ConsumerState<SocialHubScreen> {
late SocialHubTab _currentTab;
@override
void initState() {
super.initState();
_currentTab = widget.initialTab;
}
@override
void didUpdateWidget(covariant SocialHubScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialTab != widget.initialTab) {
_currentTab = widget.initialTab;
}
}
@override
Widget build(BuildContext context) {
final colors = context.motorColors; final colors = context.motorColors;
final user = ref.watch(currentUserProvider); final user = ref.watch(currentUserProvider);
final profileAsync = ref.watch(myRiderProfileProvider); final profileAsync = ref.watch(myRiderProfileProvider);
@ -29,8 +56,93 @@ class SocialHubScreen extends ConsumerWidget {
'Social', 'Social',
style: TextStyle(fontWeight: FontWeight.w700), style: TextStyle(fontWeight: FontWeight.w700),
), ),
actions: [
Padding(
padding: const EdgeInsets.only(right: 12),
child: Center(
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: const Color(0xFF181C21),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: colors.outlineVariant.withValues(alpha: 0.5),
width: 0.8,
), ),
body: SafeArea( ),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildTabToggle(
key: const ValueKey('social_tab_feed'),
label: 'Feed',
isSelected: _currentTab == SocialHubTab.feed,
colors: colors,
onTap: () => setState(() => _currentTab = SocialHubTab.feed),
),
_buildTabToggle(
key: const ValueKey('social_tab_hub'),
label: 'Hub',
isSelected: _currentTab == SocialHubTab.hub,
colors: colors,
onTap: () => setState(() => _currentTab = SocialHubTab.hub),
),
],
),
),
),
),
],
),
body: IndexedStack(
index: _currentTab == SocialHubTab.hub ? 0 : 1,
children: [
_buildHubBody(context, colors, user, profileAsync.value, pendingCount),
const SocialFeedScreen(showAppBar: false),
],
),
);
}
Widget _buildTabToggle({
required Key key,
required String label,
required bool isSelected,
required MotorSignalThemeColors colors,
required VoidCallback onTap,
}) {
return InkWell(
key: key,
borderRadius: BorderRadius.circular(8),
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? colors.cyberCyan : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 12,
fontWeight: FontWeight.w700,
color: isSelected ? const Color(0xFF00363D) : const Color(0xFFBAC9CC),
),
),
),
);
}
Widget _buildHubBody(
BuildContext context,
MotorSignalThemeColors colors,
dynamic user,
dynamic profile,
int pendingCount,
) {
return SafeArea(
child: ResponsiveLayout( child: ResponsiveLayout(
builder: (context, responsive) { builder: (context, responsive) {
final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape); final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
@ -44,7 +156,7 @@ class SocialHubScreen extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Rider Identity Header Card // Rider Identity Header Card
_buildRiderHeroCard(context, profileAsync.value, user, colors), _buildRiderHeroCard(context, profile, user, colors),
const SizedBox(height: 20), const SizedBox(height: 20),
// Section 1: Rider Network // Section 1: Rider Network
@ -139,7 +251,7 @@ class SocialHubScreen extends ConsumerWidget {
title: 'Community Feed', title: 'Community Feed',
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos', subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
colors: colors, colors: colors,
onTap: () => context.push('/social/feed'), onTap: () => setState(() => _currentTab = SocialHubTab.feed),
), ),
), ),
const SizedBox(width: 14), const SizedBox(width: 14),
@ -164,7 +276,7 @@ class SocialHubScreen extends ConsumerWidget {
title: 'Community Feed', title: 'Community Feed',
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos', subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
colors: colors, colors: colors,
onTap: () => context.push('/social/feed'), onTap: () => setState(() => _currentTab = SocialHubTab.feed),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildNavigationCard( _buildNavigationCard(
@ -185,7 +297,6 @@ class SocialHubScreen extends ConsumerWidget {
); );
}, },
), ),
),
); );
} }

View File

@ -1,4 +1,6 @@
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'social_feed_models.dart'; import 'social_feed_models.dart';
import 'social_feed_service.dart'; import 'social_feed_service.dart';
@ -7,7 +9,9 @@ import 'social_feed_service.dart';
final socialFeedServiceProvider = Provider<SocialFeedService>((ref) { final socialFeedServiceProvider = Provider<SocialFeedService>((ref) {
FirebaseFirestore? firestore; FirebaseFirestore? firestore;
try { try {
if (Firebase.apps.isNotEmpty) {
firestore = FirebaseFirestore.instance; firestore = FirebaseFirestore.instance;
}
} catch (_) { } catch (_) {
firestore = null; firestore = null;
} }
@ -23,10 +27,40 @@ final feedFilterStateProvider = StateProvider<FeedFilter>((ref) {
return FeedFilter.allActivity; return FeedFilter.allActivity;
}); });
/// Stream provider for the filtered community social feed. /// StateNotifier providing instant synchronous initial feed items and reacting to changes.
class SocialFeedNotifier extends StateNotifier<List<SocialFeedItem>> {
final SocialFeedService _service;
final FeedFilter _filter;
StreamSubscription<List<SocialFeedItem>>? _sub;
SocialFeedNotifier(this._service, this._filter)
: super(_service.getFilteredItems(filter: _filter)) {
_sub = _service.getFeedStream(filter: _filter).listen((items) {
state = items;
});
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
}
/// Primary feed provider with zero-latency synchronous initial data.
final socialFeedNotifierProvider =
StateNotifierProvider.autoDispose<SocialFeedNotifier, List<SocialFeedItem>>(
(ref) {
final service = ref.watch(socialFeedServiceProvider);
final filter = ref.watch(feedFilterStateProvider);
return SocialFeedNotifier(service, filter);
});
/// Stream provider for the filtered community social feed (preserved for backwards compatibility).
final socialFeedStreamProvider = final socialFeedStreamProvider =
StreamProvider.autoDispose<List<SocialFeedItem>>((ref) { StreamProvider.autoDispose<List<SocialFeedItem>>((ref) {
final service = ref.watch(socialFeedServiceProvider); final service = ref.watch(socialFeedServiceProvider);
final filter = ref.watch(feedFilterStateProvider); final filter = ref.watch(feedFilterStateProvider);
return service.getFeedStream(filter: filter); return service.getFeedStream(filter: filter);
}); });

View File

@ -124,6 +124,15 @@ class SocialFeedService {
_localFeedController.add(List.unmodifiable(_memoryFeed)); _localFeedController.add(List.unmodifiable(_memoryFeed));
} }
List<SocialFeedItem> get currentFeed => List.unmodifiable(_memoryFeed);
List<SocialFeedItem> getFilteredItems({
FeedFilter filter = FeedFilter.allActivity,
String? userClubName,
}) {
return _filterItems(_memoryFeed, filter, userClubName);
}
Stream<List<SocialFeedItem>> getFeedStream({ Stream<List<SocialFeedItem>> getFeedStream({
FeedFilter filter = FeedFilter.allActivity, FeedFilter filter = FeedFilter.allActivity,
String? userClubName, String? userClubName,

View File

@ -1,4 +1,4 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:motorcycle_dash/core/router.dart'; import 'package:motorcycle_dash/core/router.dart';
void main() { void main() {
@ -30,6 +30,12 @@ void main() {
final matchFriends = config.findMatch(Uri.parse('/social/friends')); final matchFriends = config.findMatch(Uri.parse('/social/friends'));
expect(matchFriends.matches.isNotEmpty, isTrue); expect(matchFriends.matches.isNotEmpty, isTrue);
final matchSocialFeed = config.findMatch(Uri.parse('/social/feed'));
expect(matchSocialFeed.matches.isNotEmpty, isTrue);
final matchFeed = config.findMatch(Uri.parse('/feed'));
expect(matchFeed.matches.isNotEmpty, isTrue);
}); });
}); });
} }

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:motorcycle_dash/core/router.dart';
import 'package:motorcycle_dash/core/theme/theme_extensions.dart';
import 'package:motorcycle_dash/features/records/records_service.dart';
import 'package:motorcycle_dash/features/social/feed/social_feed_screen.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
testWidgets('Navigating to /social and tapping Community Feed opens SocialFeedScreen', (tester) async {
await tester.binding.setSurfaceSize(const Size(400, 800));
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
],
child: MaterialApp.router(
theme: ThemeData.dark().copyWith(
extensions: const [
MotorSignalThemeColors.dark,
],
),
routerConfig: appRouter,
),
),
);
await tester.pumpAndSettle();
// 1. We start on Dashboard (index 0)
expect(find.text('Dash'), findsWidgets);
// 2. Tap Social in bottom navigation bar
final socialTab = find.byKey(const ValueKey('nav_social_icon'));
expect(socialTab, findsOneWidget);
await tester.tap(socialTab);
await tester.pumpAndSettle();
// 3. We land directly on Community Feed!
expect(find.byType(SocialFeedScreen), findsOneWidget);
expect(find.text('SQUAD & CANYON FEED'), findsOneWidget);
// 4. Tap 'Hub' toggle in AppBar to view Rider Hub
final hubToggle = find.byKey(const ValueKey('social_tab_hub'));
expect(hubToggle, findsOneWidget);
await tester.tap(hubToggle);
await tester.pumpAndSettle();
expect(find.text('Rider Network'), findsOneWidget);
expect(find.byKey(const ValueKey('social_card_feed')), findsOneWidget);
// 5. Tapping 'Feed' toggle switches back to Community Feed
final feedToggle = find.byKey(const ValueKey('social_tab_feed'));
await tester.tap(feedToggle);
await tester.pumpAndSettle();
expect(find.text('SQUAD & CANYON FEED'), findsOneWidget);
});
}