import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/responsive/responsive_layout.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_typography.dart'; import '../../../core/theme/theme_extensions.dart'; import '../../../services/auth/auth_providers.dart'; import '../../../services/map/map_provider_interface.dart'; import '../../../services/social/social_feed_models.dart'; import '../../../services/social/social_feed_providers.dart'; import 'widgets/feed_comments_bottom_sheet.dart'; import 'widgets/feed_telemetry_inspector_modal.dart'; import 'widgets/share_session_modal.dart'; /// Community Social Feed & Kudos Screen (`SOC-01` / `MOB-19`). /// /// Implements the Stitch canonical Community Social Feed HUD with telemetry /// 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 ConsumerStatefulWidget { final bool showAppBar; const SocialFeedScreen({super.key, this.showAppBar = true}); @override ConsumerState createState() => _SocialFeedScreenState(); } class _SocialFeedScreenState extends ConsumerState { bool _isSearchExpanded = false; late final TextEditingController _searchController; @override void initState() { super.initState(); _searchController = TextEditingController( text: ref.read(feedSearchQueryProvider), ); } @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final colors = context.motorColors; final activeFilter = ref.watch(feedFilterStateProvider); final activeDiscipline = ref.watch(feedDisciplineFilterProvider); final searchQuery = ref.watch(feedSearchQueryProvider); 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, Multi-Stream Bar & Discipline Chips _buildTopFilterSection( context, activeFilter, activeDiscipline, searchQuery, colors, ), // Feed List Expanded( child: items.isEmpty ? _buildEmptyFeedState( context, activeFilter, activeDiscipline, searchQuery, colors, ) : ListView.separated( key: const ValueKey('social_feed_list_view'), padding: const EdgeInsets.fromLTRB(16, 10, 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 (!widget.showAppBar) { return Container( color: Theme.of(context).colorScheme.surface, child: content, ); } return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.surface, elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back_rounded, color: Colors.white), onPressed: () { if (context.canPop()) { context.pop(); } else { context.go('/social'); } }, ), title: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppColors.electricCyan.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.dynamic_feed_rounded, color: AppColors.electricCyan, size: 20, ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'Community Feed', overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 16, fontWeight: FontWeight.w700, letterSpacing: 0.2, color: Colors.white, ), ), Text( 'SQUAD & CANYON TELEMETRY', overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 9, fontWeight: FontWeight.w700, letterSpacing: 0.8, color: const Color(0xFF94A3B8), ), ), ], ), ), ], ), actions: [ IconButton( icon: Icon( _isSearchExpanded ? Icons.search_off_rounded : Icons.search_rounded, color: _isSearchExpanded ? AppColors.electricCyan : const Color(0xFF94A3B8), ), tooltip: 'Search Routes', onPressed: () { setState(() { _isSearchExpanded = !_isSearchExpanded; if (!_isSearchExpanded) { _searchController.clear(); ref.read(feedSearchQueryProvider.notifier).state = ''; } }); }, ), IconButton( icon: const Icon(Icons.refresh_rounded, color: Color(0xFF94A3B8)), tooltip: 'Refresh Feed', onPressed: () { ref.invalidate(socialFeedNotifierProvider); ref.invalidate(socialFeedStreamProvider); }, ), const SizedBox(width: 4), ], ), body: content, ); } Widget _buildTopFilterSection( BuildContext context, FeedFilter activeFilter, RidingDiscipline activeDiscipline, String searchQuery, MotorSignalThemeColors colors, ) { return Container( padding: const EdgeInsets.fromLTRB(16, 10, 16, 8), color: Theme.of(context).colorScheme.surface, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Row 1: Status LED & Search toggle Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ // Status LED pill Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: Theme.of(context).brightness == Brightness.light ? const Color(0xFFE0F7FA) : const Color(0xFF161B22), borderRadius: BorderRadius.circular(20), border: Border.all( color: AppColors.electricCyan.withValues( alpha: Theme.of(context).brightness == Brightness.light ? 0.6 : 0.25, ), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 8, height: 8, decoration: const BoxDecoration( color: AppColors.electricCyan, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: AppColors.electricCyan, blurRadius: 6, spreadRadius: 1, ), ], ), ), const SizedBox(width: 8), Text( 'SQUAD & CANYON FEED', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 0.6, color: Theme.of(context).brightness == Brightness.light ? const Color(0xFF006064) : const Color(0xFF9CF0FF), ), ), ], ), ), // Search toggle button IconButton( key: const ValueKey('feed_search_toggle_button'), icon: Icon( _isSearchExpanded ? Icons.close_rounded : Icons.search_rounded, color: AppColors.electricCyan, size: 20, ), tooltip: 'Search Feed', onPressed: () { setState(() { _isSearchExpanded = !_isSearchExpanded; if (!_isSearchExpanded) { _searchController.clear(); ref.read(feedSearchQueryProvider.notifier).state = ''; } }); }, ), ], ), // Optional Search Input Field if (_isSearchExpanded) ...[ const SizedBox(height: 8), Container( height: 42, decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(10), border: Border.all( color: AppColors.electricCyan.withValues(alpha: 0.3), ), ), child: TextField( controller: _searchController, autofocus: true, style: const TextStyle( fontFamily: AppTypography.fontBody, fontSize: 13, color: Colors.white, ), decoration: InputDecoration( hintText: 'Search rider, route, bike, or region...', hintStyle: const TextStyle( fontFamily: AppTypography.fontBody, fontSize: 13, color: Color(0xFF64748B), ), prefixIcon: const Icon( Icons.search_rounded, size: 18, color: Color(0xFF94A3B8), ), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.close_rounded, size: 16), color: const Color(0xFF94A3B8), onPressed: () { _searchController.clear(); ref.read(feedSearchQueryProvider.notifier).state = ''; setState(() {}); }, ) : null, border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(vertical: 10), ), onChanged: (val) { ref.read(feedSearchQueryProvider.notifier).state = val; setState(() {}); }, ), ), ], const SizedBox(height: 8), // Unified Single Streamlined Filter Row (Streams + Disciplines) SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildFilterTab( tab: FeedFilter.allActivity, title: 'All Activity', isSelected: activeFilter == FeedFilter.allActivity, ), const SizedBox(width: 4), _buildFilterTab( tab: FeedFilter.clubOnly, title: 'Club Only', isSelected: activeFilter == FeedFilter.clubOnly, ), const SizedBox(width: 4), _buildFilterTab( tab: FeedFilter.nearbyPacks, title: 'Nearby Packs', isSelected: activeFilter == FeedFilter.nearbyPacks, ), const SizedBox(width: 4), _buildFilterTab( tab: FeedFilter.forYou, title: 'For You', isSelected: activeFilter == FeedFilter.forYou, ), const SizedBox(width: 4), _buildFilterTab( tab: FeedFilter.following, title: 'Following', isSelected: activeFilter == FeedFilter.following, ), const SizedBox(width: 8), Container( width: 1, height: 20, color: colors.outlineVariant.withValues(alpha: 0.4), ), const SizedBox(width: 8), _buildDisciplineChip( discipline: RidingDiscipline.all, label: 'All Styles', isSelected: activeDiscipline == RidingDiscipline.all, ), const SizedBox(width: 6), _buildDisciplineChip( discipline: RidingDiscipline.canyonSport, label: '⚡ Canyon', isSelected: activeDiscipline == RidingDiscipline.canyonSport, ), const SizedBox(width: 6), _buildDisciplineChip( discipline: RidingDiscipline.advDualSport, label: '🌲 ADV & Dirt', isSelected: activeDiscipline == RidingDiscipline.advDualSport, ), const SizedBox(width: 6), _buildDisciplineChip( discipline: RidingDiscipline.touring, label: '🛣️ Touring', isSelected: activeDiscipline == RidingDiscipline.touring, ), const SizedBox(width: 6), _buildDisciplineChip( discipline: RidingDiscipline.trackDay, label: '🏁 Track Day', isSelected: activeDiscipline == RidingDiscipline.trackDay, ), const SizedBox(width: 6), _buildDisciplineChip( discipline: RidingDiscipline.cruiser, label: '🏍️ Cruiser', isSelected: activeDiscipline == RidingDiscipline.cruiser, ), ], ), ), // Filter Summary & Reset Bar (When disciplined or searched) if (activeDiscipline != RidingDiscipline.all || searchQuery.isNotEmpty) ...[ const SizedBox(height: 6), Row( children: [ const Icon( Icons.filter_alt_outlined, size: 13, color: AppColors.electricCyan, ), const SizedBox(width: 4), Text( 'Filtered by: ${activeDiscipline != RidingDiscipline.all ? activeDiscipline.displayName : ''}${activeDiscipline != RidingDiscipline.all && searchQuery.isNotEmpty ? ', ' : ''}${searchQuery.isNotEmpty ? '"$searchQuery"' : ''}', style: const TextStyle( fontFamily: AppTypography.fontBody, fontSize: 11, color: Color(0xFF94A3B8), ), ), const Spacer(), GestureDetector( onTap: () { ref.read(feedDisciplineFilterProvider.notifier).state = RidingDiscipline.all; ref.read(feedSearchQueryProvider.notifier).state = ''; _searchController.clear(); setState(() { _isSearchExpanded = false; }); }, child: const Text( 'Reset', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 11, fontWeight: FontWeight.w700, color: AppColors.electricCyan, ), ), ), ], ), ], ], ), ); } Widget _buildFilterTab({ required FeedFilter tab, required String title, required bool isSelected, }) { return InkWell( borderRadius: BorderRadius.circular(10), onTap: () { ref.read(feedFilterStateProvider.notifier).state = tab; }, child: AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.easeInOut, height: 36, padding: const EdgeInsets.symmetric(horizontal: 8), alignment: Alignment.center, decoration: BoxDecoration( color: isSelected ? AppColors.electricCyan : Colors.transparent, borderRadius: BorderRadius.circular(10), boxShadow: isSelected ? [ BoxShadow( color: AppColors.electricCyan.withValues(alpha: 0.35), blurRadius: 8, offset: const Offset(0, 2), ), ] : null, ), child: Text( title, style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 11.5, fontWeight: FontWeight.w700, letterSpacing: 0.1, color: isSelected ? const Color(0xFF00363D) : (Theme.of(context).brightness == Brightness.light ? const Color(0xFF334155) : const Color(0xFFCBD5E1)), ), ), ), ); } Widget _buildDisciplineChip({ required RidingDiscipline discipline, required String label, required bool isSelected, }) { final chipColor = _getDisciplineColor(discipline); final isLight = Theme.of(context).brightness == Brightness.light; return InkWell( borderRadius: BorderRadius.circular(8), onTap: () { ref.read(feedDisciplineFilterProvider.notifier).state = discipline; }, child: AnimatedContainer( duration: const Duration(milliseconds: 160), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: isSelected ? chipColor.withValues(alpha: 0.22) : (isLight ? const Color(0xFFF1F5F9) : const Color(0xFF161B22)), borderRadius: BorderRadius.circular(8), border: Border.all( color: isSelected ? chipColor.withValues(alpha: 0.7) : (isLight ? const Color(0xFFCBD5E1) : const Color(0xFF263238)), width: isSelected ? 1.2 : 1.0, ), ), child: Text( label, style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 11.5, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, color: isSelected ? (isLight ? const Color(0xFF0F172A) : Colors.white) : (isLight ? const Color(0xFF475569) : const Color(0xFF94A3B8)), ), ), ), ); } Widget _buildEmptyFeedState( BuildContext context, FeedFilter activeFilter, RidingDiscipline activeDiscipline, String searchQuery, MotorSignalThemeColors colors, ) { final hasCustomFilters = activeDiscipline != RidingDiscipline.all || searchQuery.isNotEmpty; return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.dynamic_feed_outlined, size: 56, color: const Color(0xFF64748B), ), const SizedBox(height: 16), Text( hasCustomFilters ? 'No matching ride sessions' : 'No posts in ${activeFilter.displayName}', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 16, fontWeight: FontWeight.w700, color: Theme.of(context).colorScheme.onSurface, ), ), const SizedBox(height: 6), Text( hasCustomFilters ? 'Try relaxing your style filters or search query to discover more community rides.' : 'Hit "Share Recent Session" below to publish telemetry, or check out group rides and clubs.', textAlign: TextAlign.center, style: TextStyle( fontFamily: AppTypography.fontBody, fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: [ OutlinedButton.icon( key: const ValueKey('empty_feed_group_rides_button'), icon: const Icon(Icons.two_wheeler_rounded, size: 16), label: const Text('Group Rides'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.electricCyan, side: BorderSide( color: AppColors.electricCyan.withValues(alpha: 0.5), ), ), onPressed: () => context.push('/group-rides'), ), OutlinedButton.icon( key: const ValueKey('empty_feed_clubs_button'), icon: const Icon(Icons.groups_rounded, size: 16), label: const Text('Clubs'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.electricCyan, side: BorderSide( color: AppColors.electricCyan.withValues(alpha: 0.5), ), ), onPressed: () => context.push('/social/clubs'), ), OutlinedButton.icon( key: const ValueKey('empty_feed_friends_button'), icon: const Icon(Icons.person_add_rounded, size: 16), label: const Text('Friends'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.electricCyan, side: BorderSide( color: AppColors.electricCyan.withValues(alpha: 0.5), ), ), onPressed: () => context.push('/social/friends'), ), ], ), if (hasCustomFilters) ...[ const SizedBox(height: 12), OutlinedButton.icon( icon: const Icon(Icons.refresh_rounded, size: 16), label: const Text('Reset All Filters'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.electricCyan, side: BorderSide( color: AppColors.electricCyan.withValues(alpha: 0.5), ), ), onPressed: () { ref.read(feedDisciplineFilterProvider.notifier).state = RidingDiscipline.all; ref.read(feedSearchQueryProvider.notifier).state = ''; _searchController.clear(); setState(() { _isSearchExpanded = false; }); }, ), ], ], ), ), ); } Widget _buildStickyShareButton(BuildContext context) { return Container( height: 52, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: const LinearGradient( colors: [Color(0xFF00E5FF), Color(0xFF00B0FF)], ), boxShadow: [ BoxShadow( color: const Color(0xFF00E5FF).withValues(alpha: 0.35), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: ElevatedButton( key: const ValueKey('feed_share_session_button'), style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent, foregroundColor: const Color(0xFF00272E), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), elevation: 0, ), onPressed: () => ShareSessionModal.show(context), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(Icons.add_circle_outline_rounded, size: 20), SizedBox(width: 8), Flexible( child: Text( 'SHARE RECENT SESSION', overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 13.5, fontWeight: FontWeight.w800, letterSpacing: 0.8, ), ), ), ], ), ), ); } } Color _getDisciplineColor(RidingDiscipline discipline) { switch (discipline) { case RidingDiscipline.canyonSport: return const Color(0xFFF59E0B); case RidingDiscipline.advDualSport: return const Color(0xFF10B981); case RidingDiscipline.touring: return const Color(0xFF38BDF8); case RidingDiscipline.trackDay: return const Color(0xFFF43F5E); case RidingDiscipline.cruiser: return const Color(0xFFA855F7); case RidingDiscipline.all: return AppColors.electricCyan; } } /// Authoritative Feed Item Card (`SocialFeedCard`). class _SocialFeedCard extends ConsumerStatefulWidget { final SocialFeedItem item; const _SocialFeedCard({super.key, required this.item}); @override ConsumerState<_SocialFeedCard> createState() => _SocialFeedCardState(); } class _SocialFeedCardState extends ConsumerState<_SocialFeedCard> with SingleTickerProviderStateMixin { late AnimationController _bumpAnimController; late Animation _bumpScaleAnimation; int? _optimisticBumpCount; bool? _optimisticIsBumped; @override void initState() { super.initState(); _bumpAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 160), ); _bumpScaleAnimation = Tween(begin: 1.0, end: 1.15).animate( CurvedAnimation(parent: _bumpAnimController, curve: Curves.easeOut), ); } @override void didUpdateWidget(covariant _SocialFeedCard oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.item.fistBumpCount != widget.item.fistBumpCount || oldWidget.item.fistBumpUserIds != widget.item.fistBumpUserIds) { _optimisticBumpCount = null; _optimisticIsBumped = null; } } @override void dispose() { _bumpAnimController.dispose(); super.dispose(); } String _formatRelativeTime(DateTime dt) { final now = DateTime.now(); final diff = now.difference(dt); if (diff.inMinutes < 1) return 'Just now'; if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; if (diff.inHours < 24) return '${diff.inHours}h ago'; return '${diff.inDays}d ago'; } String _formatDuration(int seconds) { final mins = seconds ~/ 60; if (mins < 60) { final secs = seconds % 60; return '${mins}m ${secs}s'; } final hrs = mins ~/ 60; final remMins = mins % 60; return '${hrs}h ${remMins}m'; } void _handleFistBump() { final user = ref.read(currentUserProvider); final uid = user?.uid ?? 'uid_rider'; final currentlyBumped = _optimisticIsBumped ?? widget.item.isFistBumpedBy(uid); final currentCount = _optimisticBumpCount ?? widget.item.fistBumpCount; setState(() { _optimisticIsBumped = !currentlyBumped; _optimisticBumpCount = currentlyBumped ? (currentCount - 1).clamp(0, 999999) : currentCount + 1; }); _bumpAnimController.forward().then((_) { if (mounted) _bumpAnimController.reverse(); }); ref.read(socialFeedServiceProvider).toggleFistBump( postId: widget.item.id, userUid: uid, ); } @override Widget build(BuildContext context) { final item = widget.item; final user = ref.watch(currentUserProvider); final isBumped = _optimisticIsBumped ?? item.isFistBumpedBy(user?.uid ?? 'uid_rider'); final bumpCount = _optimisticBumpCount ?? item.fistBumpCount; return Container( decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(18), border: Border.all(color: const Color(0xFF263238)), boxShadow: const [ BoxShadow( color: Colors.black45, blurRadius: 14, offset: Offset(0, 6), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Card Header: Author info, badge, discipline, club, overflow Padding( padding: const EdgeInsets.fromLTRB(16, 14, 12, 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Helmet Avatar with online indicator Stack( children: [ CircleAvatar( radius: 22, backgroundColor: const Color(0xFF222936), child: const Icon( Icons.sports_motorsports_rounded, color: AppColors.electricCyan, size: 26, ), ), Positioned( bottom: 0, right: 0, child: Container( width: 11, height: 11, decoration: BoxDecoration( color: AppColors.electricCyan, shape: BoxShape.circle, border: Border.all( color: const Color(0xFF161B22), width: 1.5), boxShadow: const [ BoxShadow( color: AppColors.electricCyan, blurRadius: 4, ), ], ), ), ), ], ), const SizedBox(width: 12), // Author handle, badge, discipline badge, club, timestamp Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 6, runSpacing: 4, children: [ Text( '@${item.authorHandle}', style: const TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 15, fontWeight: FontWeight.w700, color: Color(0xFFF1F5F9), ), ), if (item.isDemo) ...[ Container( key: const ValueKey('feed_demo_pill'), padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFF59E0B).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), border: Border.all( color: const Color(0xFFF59E0B).withValues(alpha: 0.4), ), ), child: const Text( 'DEMO SAMPLE', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 8.5, fontWeight: FontWeight.w800, letterSpacing: 0.5, color: Color(0xFFF59E0B), ), ), ), ], if (item.authorBadge != null) ...[ Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.electricCyan .withValues(alpha: 0.18), borderRadius: BorderRadius.circular(4), ), child: Text( item.authorBadge!, style: const TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 9, fontWeight: FontWeight.w800, letterSpacing: 0.6, color: AppColors.electricCyan, ), ), ), ], if (item.discipline != RidingDiscipline.all) ...[ Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _getDisciplineColor(item.discipline) .withValues(alpha: 0.18), borderRadius: BorderRadius.circular(4), border: Border.all( color: _getDisciplineColor(item.discipline) .withValues(alpha: 0.35), width: 0.8, ), ), child: Text( item.discipline.displayName.toUpperCase(), style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 8.5, fontWeight: FontWeight.w800, letterSpacing: 0.5, color: _getDisciplineColor(item.discipline), ), ), ), ], ], ), if (item.clubName != null) ...[ const SizedBox(height: 2), Text( item.clubName!, overflow: TextOverflow.ellipsis, style: const TextStyle( fontFamily: AppTypography.fontBody, fontSize: 12, color: Color(0xFF94A3B8), ), ), ], const SizedBox(height: 2), Text( '${_formatRelativeTime(item.timestamp)} • ${item.locationName}', style: const TextStyle( fontFamily: AppTypography.fontBody, fontSize: 12.5, fontWeight: FontWeight.w500, color: Color(0xFFCBD5E1), ), ), ], ), ), // Overflow options menu PopupMenuButton( icon: const Icon(Icons.more_vert, color: Color(0xFF849396), size: 20), color: const Color(0xFF181C21), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: const BorderSide(color: Color(0xFF272A30)), ), onSelected: (val) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Action: $val'), duration: const Duration(seconds: 1), ), ); }, itemBuilder: (ctx) => [ const PopupMenuItem( value: 'copy_link', child: Text('Copy Route Link', style: TextStyle(color: Colors.white, fontSize: 13)), ), const PopupMenuItem( value: 'export_gpx', child: Text('Export GPX File', style: TextStyle(color: Colors.white, fontSize: 13)), ), const PopupMenuItem( value: 'report', child: Text('Report Content', style: TextStyle(color: Colors.redAccent, fontSize: 13)), ), ], ), ], ), ), // Route Title & Motorcycle Tag Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.rideTitle, style: const TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: -0.3, color: Colors.white, ), ), const SizedBox(height: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF12161D), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFF263238)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.sports_motorsports_rounded, size: 14, color: AppColors.electricCyan, ), const SizedBox(width: 6), Flexible( child: Text( item.bikeModel, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 11.5, fontWeight: FontWeight.w600, color: Color(0xFFE2E8F0), ), ), ), if (item.regionName != null) ...[ const SizedBox(width: 6), Container( width: 3, height: 3, decoration: const BoxDecoration( color: Color(0xFF64748B), shape: BoxShape.circle, ), ), const SizedBox(width: 6), Text( item.regionName!, style: const TextStyle( fontFamily: AppTypography.fontBody, fontSize: 11, color: Color(0xFF94A3B8), ), ), ], ], ), ), ], ), ), const SizedBox(height: 12), // Telemetry Inset Strip (Stitch 2x2 Inset Grid) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFF0F1318), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF1E2633)), ), child: Column( children: [ Row( children: [ _buildMetricCell( label: 'DISTANCE', value: item.distanceMiles.toStringAsFixed(1), unit: 'MI', valueColor: Colors.white, ), const SizedBox(width: 6), _buildMetricCell( label: 'DURATION', value: _formatDuration(item.durationSeconds), unit: '', valueColor: Colors.white, ), ], ), const SizedBox(height: 6), Row( children: [ _buildMetricCell( label: 'MAX LEAN', value: '${item.maxLeanAngleDeg.toStringAsFixed(0)}°', unit: item.maxLeanSide, valueColor: AppColors.electricCyan, indicatorColor: AppColors.electricCyan, ), const SizedBox(width: 6), _buildMetricCell( label: 'PACE', value: item.paceFactor.split(' ').first, unit: item.paceFactor.split(' ').length > 1 ? item.paceFactor.split(' ').sublist(1).join(' ') : '', valueColor: const Color(0xFFB4C5FF), ), ], ), ], ), ), ), const SizedBox(height: 12), // Visual Snapshot Module (Route Vector Track or Scenic Photo) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: item.snapshotType == FeedSnapshotType.scenicPhoto ? _buildScenicPhotoModule(context, item) : _buildRouteTrackModule(context, item), ), const SizedBox(height: 12), // Kudos & Action Bar (Fist Bump, Comments, GPX, Inspect) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 14), child: Column( children: [ Row( children: [ // Fist Bump Interactive Button Expanded( flex: 3, child: ScaleTransition( scale: _bumpScaleAnimation, child: InkWell( key: ValueKey('fist_bump_button_${item.id}'), borderRadius: BorderRadius.circular(12), onTap: _handleFistBump, child: AnimatedContainer( duration: const Duration(milliseconds: 180), height: 48, decoration: BoxDecoration( color: isBumped ? AppColors.electricCyan : const Color(0xFF272A30), borderRadius: BorderRadius.circular(12), boxShadow: isBumped ? [ BoxShadow( color: AppColors.electricCyan .withValues(alpha: 0.35), blurRadius: 10, offset: const Offset(0, 2), ), ] : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('👊', style: TextStyle(fontSize: 18)), const SizedBox(width: 8), Flexible( child: Text( '$bumpCount Bumps', overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 13, fontWeight: FontWeight.w700, color: isBumped ? const Color(0xFF00363D) : Colors.white, ), ), ), ], ), ), ), ), ), const SizedBox(width: 8), // Comments Action Button Expanded( flex: 2, child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () => FeedCommentsBottomSheet.show(context, item), child: Container( height: 48, decoration: BoxDecoration( color: const Color(0xFF272A30), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.chat_bubble_outline_rounded, size: 18, color: AppColors.electricCyan, ), const SizedBox(width: 6), Text( '${item.comments.length}', style: const TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 13, fontWeight: FontWeight.w700, color: Colors.white, ), ), ], ), ), ), ), const SizedBox(width: 8), // GPX Export Button InkWell( borderRadius: BorderRadius.circular(12), onTap: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: const Color(0xFF1D2025), content: Row( children: [ const Icon(Icons.download_done_rounded, color: AppColors.electricCyan), const SizedBox(width: 10), Text( 'GPX track exported for ${item.rideTitle}', style: const TextStyle(color: Colors.white), ), ], ), ), ); }, child: Container( width: 48, height: 48, decoration: BoxDecoration( color: const Color(0xFF272A30), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.download_rounded, size: 20, color: Colors.white, ), ), ), ], ), const SizedBox(height: 8), // Telemetry Deep Dive Inspector Trigger InkWell( key: ValueKey('inspect_telemetry_btn_${item.id}'), borderRadius: BorderRadius.circular(12), onTap: () { FeedTelemetryInspectorModal.show(context, item); }, child: Container( height: 44, width: double.infinity, decoration: BoxDecoration( color: const Color(0xFF181C21), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppColors.electricCyan.withValues(alpha: 0.25), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon( Icons.bolt_rounded, size: 18, color: AppColors.electricCyan, ), SizedBox(width: 8), Flexible( child: Text( 'INSPECT TELEMETRY GRAPH', overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 0.8, color: AppColors.electricCyan, ), ), ), ], ), ), ), ], ), ), ], ), ); } Widget _buildMetricCell({ required String label, required String value, required String unit, required Color valueColor, Color? indicatorColor, }) { return Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF222936), width: 0.8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 10.5, fontWeight: FontWeight.w700, letterSpacing: 0.6, color: Color(0xFF94A3B8), ), ), ), if (indicatorColor != null) ...[ Container( width: 5, height: 5, decoration: BoxDecoration( color: indicatorColor, shape: BoxShape.circle, ), ), ], ], ), const SizedBox(height: 4), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Flexible( child: Text( value, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 15.5, fontWeight: FontWeight.w800, color: valueColor, ), ), ), if (unit.isNotEmpty) ...[ const SizedBox(width: 4), Text( unit, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 10, fontWeight: FontWeight.w700, color: Color(0xFFCBD5E1), ), ), ], ], ), ], ), ), ); } Widget _buildRouteTrackModule(BuildContext context, SocialFeedItem item) { return Container( height: 160, decoration: BoxDecoration( color: const Color(0xFF0B0E13), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF181C21)), ), child: Stack( children: [ // Route Vector Canvas Painter Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(14), child: CustomPaint( painter: _FeedRoutePainter( points: item.routePoints, maxLean: item.maxLeanAngleDeg, ), ), ), ), // Elevation Pill Overlay Positioned( bottom: 8, left: 10, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: const Color(0xFF1D2025).withValues(alpha: 0.85), borderRadius: BorderRadius.circular(6), border: Border.all( color: AppColors.electricCyan.withValues(alpha: 0.2), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.landscape_rounded, size: 13, color: AppColors.electricCyan, ), const SizedBox(width: 5), Text( 'ELV +${item.elevationFt} FT', style: const TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 10, fontWeight: FontWeight.w700, color: Color(0xFFE0E2EA), ), ), ], ), ), ), // GPS Verified Badge if (item.isGpsVerified) ...[ Positioned( top: 8, right: 10, child: Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: const Color(0xFF101419).withValues(alpha: 0.85), borderRadius: BorderRadius.circular(6), border: Border.all( color: AppColors.electricCyan.withValues(alpha: 0.2), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: const [ Icon(Icons.verified_rounded, size: 12, color: AppColors.electricCyan), SizedBox(width: 4), Text( 'GPS VERIFIED', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 9, fontWeight: FontWeight.w800, color: AppColors.electricCyan, ), ), ], ), ), ), ], ], ), ); } Widget _buildScenicPhotoModule(BuildContext context, SocialFeedItem item) { return Container( height: 180, decoration: BoxDecoration( color: const Color(0xFF0B0E13), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF181C21)), ), child: ClipRRect( borderRadius: BorderRadius.circular(14), child: Stack( fit: StackFit.expand, children: [ // Atmospheric gradient container mimicking canyon overlook Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFF1E293B), Color(0xFF0F172A), Color(0xFF020617), ], ), ), child: const Center( child: Icon( Icons.landscape_rounded, size: 64, color: Color(0xFF334155), ), ), ), // Bottom gradient vignette Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withValues(alpha: 0.75), ], ), ), ), ), // Lookout caption & GPS badge Positioned( left: 10, right: 10, bottom: 10, child: Wrap( alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.center, spacing: 6, runSpacing: 4, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF101419).withValues(alpha: 0.85), borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.photo_camera_rounded, size: 13, color: AppColors.electricCyan, ), const SizedBox(width: 5), Text( item.photoCaption ?? 'Scenic Overlook', style: const TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), if (item.isGpsVerified) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF101419).withValues(alpha: 0.85), borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: const [ Icon( Icons.verified_rounded, size: 13, color: AppColors.electricCyan, ), SizedBox(width: 4), Text( 'GPS VERIFIED', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 10, fontWeight: FontWeight.w800, color: AppColors.electricCyan, ), ), ], ), ), ], ), ), ], ), ), ); } } /// Custom vector route painter for the community social feed route preview. class _FeedRoutePainter extends CustomPainter { final List points; final double maxLean; const _FeedRoutePainter({ required this.points, required this.maxLean, }); @override void paint(Canvas canvas, Size size) { final w = size.width; final h = size.height; // 1. Grid lines matrix overlay final gridPaint = Paint() ..color = const Color(0xFF1D2025) ..strokeWidth = 0.8; const gridSize = 24.0; for (double x = 0; x < w; x += gridSize) { canvas.drawLine(Offset(x, 0), Offset(x, h), gridPaint); } for (double y = 0; y < h; y += gridSize) { canvas.drawLine(Offset(0, y), Offset(w, y), gridPaint); } // 2. High-contrast curved canyon path final path = Path(); final p0 = Offset(w * 0.1, h * 0.75); final p1 = Offset(w * 0.35, h * 0.25); final p2 = Offset(w * 0.55, h * 0.7); final p3 = Offset(w * 0.88, h * 0.3); path.moveTo(p0.dx, p0.dy); path.cubicTo( w * 0.2, h * 0.75, w * 0.25, h * 0.25, p1.dx, p1.dy, ); path.cubicTo( w * 0.42, h * 0.25, w * 0.45, h * 0.7, p2.dx, p2.dy, ); path.cubicTo( w * 0.65, h * 0.7, w * 0.72, h * 0.28, p3.dx, p3.dy, ); // Cyan glow blur final glowPaint = Paint() ..color = AppColors.electricCyan.withValues(alpha: 0.25) ..strokeWidth = 8 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; canvas.drawPath(path, glowPaint); // Active trail final trailPaint = Paint() ..color = const Color(0xFFC3F5FF) ..strokeWidth = 3.5 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; canvas.drawPath(path, trailPaint); // High G-Force Apex Segment Highlight (Amber Sector) final amberPath = Path(); amberPath.moveTo(p1.dx, p1.dy); amberPath.cubicTo( w * 0.42, h * 0.25, w * 0.45, h * 0.7, p2.dx, p2.dy, ); final amberPaint = Paint() ..color = const Color(0xFFF59E0B) ..strokeWidth = 4.5 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; canvas.drawPath(amberPath, amberPaint); // Start marker with dashed ring final startPaint = Paint()..color = AppColors.electricCyan; canvas.drawCircle(p0, 4.5, startPaint); final ringPaint = Paint() ..color = AppColors.electricCyan.withValues(alpha: 0.6) ..style = PaintingStyle.stroke ..strokeWidth = 1.5; canvas.drawCircle(p0, 8.0, ringPaint); // Apex markers with badges final apexPaint = Paint()..color = AppColors.electricCyan; canvas.drawCircle(p1, 4.0, apexPaint); final amberDotPaint = Paint()..color = const Color(0xFFF59E0B); canvas.drawCircle(p2, 4.0, amberDotPaint); // Finish marker final finishPaint = Paint()..color = const Color(0xFFBAC9CC); canvas.drawCircle(p3, 4.0, finishPaint); // Text labels for APEX and FINISH final textPainter = TextPainter(textDirection: TextDirection.ltr); textPainter.text = TextSpan( text: '${maxLean.toStringAsFixed(0)}° APEX', style: const TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 9, fontWeight: FontWeight.w800, color: Color(0xFFC3F5FF), ), ); textPainter.layout(); textPainter.paint(canvas, Offset(p1.dx - textPainter.width / 2, p1.dy - 16)); textPainter.text = const TextSpan( text: '48° SWEEP', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 9, fontWeight: FontWeight.w800, color: Color(0xFFF59E0B), ), ); textPainter.layout(); textPainter.paint(canvas, Offset(p2.dx - textPainter.width / 2, p2.dy + 8)); textPainter.text = const TextSpan( text: 'FINISH', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 9, fontWeight: FontWeight.w700, color: Color(0xFFBAC9CC), ), ); textPainter.layout(); textPainter.paint(canvas, Offset(p3.dx - textPainter.width / 2, p3.dy + 8)); } @override bool shouldRepaint(covariant _FeedRoutePainter oldDelegate) { return oldDelegate.maxLean != maxLean || oldDelegate.points != points; } }