fix(social): resolve community feed discoverability, tab routing, live comments, and responsive overflows
This commit is contained in:
parent
71cd994b2f
commit
d378db4c86
|
|
@ -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(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,15 +865,18 @@ 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(
|
||||||
'$bumpCount Bumps',
|
child: Text(
|
||||||
style: TextStyle(
|
'$bumpCount Bumps',
|
||||||
fontFamily: AppTypography.fontHeadline,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontSize: 13,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontFamily: AppTypography.fontHeadline,
|
||||||
color: isBumped
|
fontSize: 13,
|
||||||
? const Color(0xFF00363D)
|
fontWeight: FontWeight.w700,
|
||||||
: Colors.white,
|
color: isBumped
|
||||||
|
? const Color(0xFF00363D)
|
||||||
|
: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -991,14 +990,17 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
|
||||||
color: AppColors.electricCyan,
|
color: AppColors.electricCyan,
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Flexible(
|
||||||
'INSPECT TELEMETRY GRAPH',
|
child: Text(
|
||||||
style: TextStyle(
|
'INSPECT TELEMETRY GRAPH',
|
||||||
fontFamily: AppTypography.fontHeadline,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontSize: 11,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontFamily: AppTypography.fontHeadline,
|
||||||
letterSpacing: 0.8,
|
fontSize: 11,
|
||||||
color: AppColors.electricCyan,
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.8,
|
||||||
|
color: AppColors.electricCyan,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,162 +56,246 @@ class SocialHubScreen extends ConsumerWidget {
|
||||||
'Social',
|
'Social',
|
||||||
style: TextStyle(fontWeight: FontWeight.w700),
|
style: TextStyle(fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
),
|
actions: [
|
||||||
body: SafeArea(
|
Padding(
|
||||||
child: ResponsiveLayout(
|
padding: const EdgeInsets.only(right: 12),
|
||||||
builder: (context, responsive) {
|
child: Center(
|
||||||
final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
return SingleChildScrollView(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
color: const Color(0xFF181C21),
|
||||||
child: Center(
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: ConstrainedBox(
|
border: Border.all(
|
||||||
constraints: const BoxConstraints(maxWidth: 800),
|
color: colors.outlineVariant.withValues(alpha: 0.5),
|
||||||
child: Column(
|
width: 0.8,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
// Rider Identity Header Card
|
|
||||||
_buildRiderHeroCard(context, profileAsync.value, user, colors),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Section 1: Rider Network
|
|
||||||
Text(
|
|
||||||
'Rider Network',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: AppTypography.fontHeadline,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
color: colors.cyberCyan,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
|
|
||||||
if (isWide) ...[
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildNavigationCard(
|
|
||||||
context,
|
|
||||||
key: const ValueKey('social_card_friends'),
|
|
||||||
icon: Icons.people_outline,
|
|
||||||
title: 'Friends',
|
|
||||||
subtitle: 'Find fellow riders, manage requests & connections',
|
|
||||||
badgeCount: pendingCount,
|
|
||||||
colors: colors,
|
|
||||||
onTap: () => context.push('/social/friends'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 14),
|
|
||||||
Expanded(
|
|
||||||
child: _buildNavigationCard(
|
|
||||||
context,
|
|
||||||
key: const ValueKey('social_card_clubs'),
|
|
||||||
icon: Icons.shield_outlined,
|
|
||||||
title: 'Clubs',
|
|
||||||
subtitle: 'Discover motorcycle clubs, riding communities & member groups',
|
|
||||||
colors: colors,
|
|
||||||
onTap: () => context.push('/social/clubs'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
_buildNavigationCard(
|
|
||||||
context,
|
|
||||||
key: const ValueKey('social_card_friends'),
|
|
||||||
icon: Icons.people_outline,
|
|
||||||
title: 'Friends',
|
|
||||||
subtitle: 'Find fellow riders, manage requests & connections',
|
|
||||||
badgeCount: pendingCount,
|
|
||||||
colors: colors,
|
|
||||||
onTap: () => context.push('/social/friends'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildNavigationCard(
|
|
||||||
context,
|
|
||||||
key: const ValueKey('social_card_clubs'),
|
|
||||||
icon: Icons.shield_outlined,
|
|
||||||
title: 'Clubs',
|
|
||||||
subtitle: 'Discover motorcycle clubs, riding communities & member groups',
|
|
||||||
colors: colors,
|
|
||||||
onTap: () => context.push('/social/clubs'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Section 2: Motorcycle Activities
|
|
||||||
Text(
|
|
||||||
'Motorcycle Activities',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: AppTypography.fontHeadline,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
color: colors.cyberCyan,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
|
|
||||||
if (isWide) ...[
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildNavigationCard(
|
|
||||||
context,
|
|
||||||
key: const ValueKey('social_card_feed'),
|
|
||||||
icon: Icons.dynamic_feed_outlined,
|
|
||||||
title: 'Community Feed',
|
|
||||||
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
|
|
||||||
colors: colors,
|
|
||||||
onTap: () => context.push('/social/feed'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 14),
|
|
||||||
Expanded(
|
|
||||||
child: _buildNavigationCard(
|
|
||||||
context,
|
|
||||||
key: const ValueKey('social_card_group_rides'),
|
|
||||||
icon: Icons.groups_outlined,
|
|
||||||
title: 'Group Rides',
|
|
||||||
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
|
|
||||||
colors: colors,
|
|
||||||
onTap: () => context.push('/group-rides'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
_buildNavigationCard(
|
|
||||||
context,
|
|
||||||
key: const ValueKey('social_card_feed'),
|
|
||||||
icon: Icons.dynamic_feed_outlined,
|
|
||||||
title: 'Community Feed',
|
|
||||||
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
|
|
||||||
colors: colors,
|
|
||||||
onTap: () => context.push('/social/feed'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildNavigationCard(
|
|
||||||
context,
|
|
||||||
key: const ValueKey('social_card_group_rides'),
|
|
||||||
icon: Icons.groups_outlined,
|
|
||||||
title: 'Group Rides',
|
|
||||||
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
|
|
||||||
colors: colors,
|
|
||||||
onTap: () => context.push('/group-rides'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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(
|
||||||
|
builder: (context, responsive) {
|
||||||
|
final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 800),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Rider Identity Header Card
|
||||||
|
_buildRiderHeroCard(context, profile, user, colors),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Section 1: Rider Network
|
||||||
|
Text(
|
||||||
|
'Rider Network',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
color: colors.cyberCyan,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
if (isWide) ...[
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_friends'),
|
||||||
|
icon: Icons.people_outline,
|
||||||
|
title: 'Friends',
|
||||||
|
subtitle: 'Find fellow riders, manage requests & connections',
|
||||||
|
badgeCount: pendingCount,
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/social/friends'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: _buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_clubs'),
|
||||||
|
icon: Icons.shield_outlined,
|
||||||
|
title: 'Clubs',
|
||||||
|
subtitle: 'Discover motorcycle clubs, riding communities & member groups',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/social/clubs'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
_buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_friends'),
|
||||||
|
icon: Icons.people_outline,
|
||||||
|
title: 'Friends',
|
||||||
|
subtitle: 'Find fellow riders, manage requests & connections',
|
||||||
|
badgeCount: pendingCount,
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/social/friends'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_clubs'),
|
||||||
|
icon: Icons.shield_outlined,
|
||||||
|
title: 'Clubs',
|
||||||
|
subtitle: 'Discover motorcycle clubs, riding communities & member groups',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/social/clubs'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Section 2: Motorcycle Activities
|
||||||
|
Text(
|
||||||
|
'Motorcycle Activities',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: AppTypography.fontHeadline,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
color: colors.cyberCyan,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
if (isWide) ...[
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_feed'),
|
||||||
|
icon: Icons.dynamic_feed_outlined,
|
||||||
|
title: 'Community Feed',
|
||||||
|
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => setState(() => _currentTab = SocialHubTab.feed),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: _buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_group_rides'),
|
||||||
|
icon: Icons.groups_outlined,
|
||||||
|
title: 'Group Rides',
|
||||||
|
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/group-rides'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
_buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_feed'),
|
||||||
|
icon: Icons.dynamic_feed_outlined,
|
||||||
|
title: 'Community Feed',
|
||||||
|
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => setState(() => _currentTab = SocialHubTab.feed),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildNavigationCard(
|
||||||
|
context,
|
||||||
|
key: const ValueKey('social_card_group_rides'),
|
||||||
|
icon: Icons.groups_outlined,
|
||||||
|
title: 'Group Rides',
|
||||||
|
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
|
||||||
|
colors: colors,
|
||||||
|
onTap: () => context.push('/group-rides'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
firestore = FirebaseFirestore.instance;
|
if (Firebase.apps.isNotEmpty) {
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue