1494 lines
54 KiB
Dart
1494 lines
54 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../../core/responsive/responsive_layout.dart';
|
|
import '../../../core/theme/app_typography.dart';
|
|
import '../../../core/theme/theme_extensions.dart';
|
|
import '../../../services/auth/auth_providers.dart';
|
|
import '../../../services/clubs/club_models.dart';
|
|
import '../../../services/clubs/club_providers.dart';
|
|
import '../../../services/clubs/club_service.dart';
|
|
import 'club_membership_controller.dart';
|
|
import 'club_membership_presentation.dart';
|
|
import 'widgets/club_monogram.dart';
|
|
import 'widgets/club_roster_tab.dart';
|
|
import 'widgets/club_wall_tab.dart';
|
|
import 'widgets/officer_badge.dart';
|
|
|
|
/// Navigation tabs on the ClubDetailScreen.
|
|
enum ClubDetailTab { overview, wall, roster }
|
|
|
|
/// Authoritative detail and membership lifecycle screen for a MotorSignal Club.
|
|
/// Enables riders to discover, join, request admission, withdraw requests,
|
|
/// accept/decline invitations, explore the Club Wall, and review the full Member Roster.
|
|
class ClubDetailScreen extends ConsumerStatefulWidget {
|
|
final String clubId;
|
|
|
|
const ClubDetailScreen({
|
|
super.key,
|
|
required this.clubId,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<ClubDetailScreen> createState() => _ClubDetailScreenState();
|
|
}
|
|
|
|
class _ClubDetailScreenState extends ConsumerState<ClubDetailScreen> {
|
|
ClubDetailTab _selectedTab = ClubDetailTab.overview;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = context.motorColors;
|
|
final theme = Theme.of(context);
|
|
final user = ref.watch(currentUserProvider);
|
|
final isAuthenticated = user != null && !user.isAnonymous;
|
|
final currentUserId = user?.uid;
|
|
|
|
final clubAsync = ref.watch(clubDetailProvider(widget.clubId));
|
|
final memberAsync = ref.watch(currentMemberProvider(widget.clubId));
|
|
final actionState = ref.watch(clubMembershipControllerProvider(widget.clubId));
|
|
final isMutating = actionState.isSubmitting;
|
|
|
|
// Listen for feedback snackbars
|
|
ref.listen<ClubMembershipActionState>(clubMembershipControllerProvider(widget.clubId), (prev, next) {
|
|
if (next.error != null && next.error != prev?.error) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(next.error!),
|
|
backgroundColor: colors.raceRed,
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
if (next.success != null && next.success != prev?.success) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(next.success!),
|
|
backgroundColor: colors.launchGreen,
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'Club',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
body: SafeArea(
|
|
child: clubAsync.when(
|
|
data: (club) {
|
|
if (club == null) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.shield_outlined, size: 48, color: colors.warningAmber),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Club Not Found',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'This club may have been archived or does not exist.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 13,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () => context.go('/social/clubs'),
|
|
icon: const Icon(Icons.arrow_back, size: 16),
|
|
label: const Text('Back to Clubs'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: colors.surfaceContainerHigh,
|
|
foregroundColor: colors.cyberCyan,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
side: BorderSide(color: colors.outlineVariant),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (club.isArchived) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.archive_outlined, size: 48, color: colors.warningAmber),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Club Archived',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'This club has been archived by leadership and is no longer active.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 13,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () => context.go('/social/clubs'),
|
|
icon: const Icon(Icons.arrow_back, size: 16),
|
|
label: const Text('Back to Clubs'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: colors.surfaceContainerHigh,
|
|
foregroundColor: colors.cyberCyan,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
side: BorderSide(color: colors.outlineVariant),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
final myMembership = memberAsync.valueOrNull;
|
|
|
|
return ResponsiveLayout(
|
|
builder: (context, responsive) {
|
|
return Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 750),
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(18.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Club Hero Identity Card
|
|
_buildHeroCard(context, club, myMembership, colors, theme),
|
|
const SizedBox(height: 14),
|
|
|
|
// Segmented Navigation Tab Bar
|
|
_buildTabBar(colors, theme),
|
|
const SizedBox(height: 16),
|
|
|
|
if (_selectedTab == ClubDetailTab.overview) ...[
|
|
// Interactive Membership Lifecycle Action Card
|
|
_buildMembershipActionCard(
|
|
context,
|
|
ref,
|
|
club,
|
|
memberAsync,
|
|
currentUserId,
|
|
isAuthenticated,
|
|
isMutating,
|
|
colors,
|
|
theme,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Quick Navigation Teasers (Club Wall & Roster)
|
|
_buildQuickShortcutsCard(context, club, colors, theme),
|
|
const SizedBox(height: 18),
|
|
|
|
// Club Rides Card
|
|
_buildClubRidesCard(
|
|
context,
|
|
ref,
|
|
club,
|
|
myMembership,
|
|
colors,
|
|
theme,
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// Description Card
|
|
_buildDescriptionCard(context, club, colors, theme),
|
|
const SizedBox(height: 18),
|
|
|
|
// Metadata Card (Region, Focus, Dates)
|
|
_buildMetadataCard(context, club, colors, theme),
|
|
] else if (_selectedTab == ClubDetailTab.wall) ...[
|
|
ClubWallTab(club: club, member: myMembership),
|
|
] else if (_selectedTab == ClubDetailTab.roster) ...[
|
|
ClubRosterTab(club: club, myMembership: myMembership),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
loading: () => Center(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(colors.cyberCyan),
|
|
),
|
|
),
|
|
error: (err, _) => Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.shield_outlined, size: 36, color: colors.warningAmber),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'Club Unavailable',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Unable to display this club at this time.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 13,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () => context.go('/social/clubs'),
|
|
icon: const Icon(Icons.arrow_back, size: 16),
|
|
label: const Text('Back to Clubs'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: colors.surfaceContainerHigh,
|
|
foregroundColor: colors.cyberCyan,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
side: BorderSide(color: colors.outlineVariant),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeroCard(
|
|
BuildContext context,
|
|
Club club,
|
|
ClubMember? member,
|
|
MotorSignalThemeColors colors,
|
|
ThemeData theme,
|
|
) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(20.0),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
ClubMonogram(name: club.name, size: 68, fontSize: 26),
|
|
const SizedBox(height: 14),
|
|
Text(
|
|
club.name,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'@${club.slug}',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontTelemetry,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: colors.cyberCyan,
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 6,
|
|
alignment: WrapAlignment.center,
|
|
children: [
|
|
_buildPill(
|
|
icon: Icons.people_outline,
|
|
label: '${club.memberCount} ${club.memberCount == 1 ? 'member' : 'members'}',
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
_buildPill(
|
|
icon: club.admissionPolicy == ClubAdmissionPolicy.open
|
|
? Icons.group_add_outlined
|
|
: (club.admissionPolicy == ClubAdmissionPolicy.request
|
|
? Icons.how_to_reg_outlined
|
|
: Icons.mail_outline),
|
|
label: club.admissionPolicy.label,
|
|
colors: colors,
|
|
theme: theme,
|
|
tintColor: colors.cyberCyan,
|
|
),
|
|
_buildPill(
|
|
icon: club.visibility == ClubVisibility.public
|
|
? Icons.public
|
|
: Icons.visibility_off_outlined,
|
|
label: club.visibility.label,
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
if (member != null &&
|
|
member.isActive &&
|
|
(member.role == ClubRole.owner || member.role == ClubRole.admin))
|
|
_buildPill(
|
|
icon: Icons.verified_user_outlined,
|
|
label: 'ROLE: ${member.role.label.toUpperCase()}',
|
|
colors: colors,
|
|
theme: theme,
|
|
tintColor: colors.launchGreen,
|
|
),
|
|
if (member != null && member.isActive && member.displayRank != null)
|
|
OfficerBadge(
|
|
rank: member.displayRank,
|
|
role: member.role,
|
|
fontSize: 11,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTabBar(MotorSignalThemeColors colors, ThemeData theme) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
|
),
|
|
padding: const EdgeInsets.all(4),
|
|
child: Row(
|
|
children: [
|
|
_buildTabButton(
|
|
tab: ClubDetailTab.overview,
|
|
label: 'OVERVIEW',
|
|
icon: Icons.info_outline,
|
|
colors: colors,
|
|
),
|
|
_buildTabButton(
|
|
tab: ClubDetailTab.wall,
|
|
label: 'CLUB WALL',
|
|
icon: Icons.forum_outlined,
|
|
colors: colors,
|
|
),
|
|
_buildTabButton(
|
|
tab: ClubDetailTab.roster,
|
|
label: 'ROSTER',
|
|
icon: Icons.groups_outlined,
|
|
colors: colors,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTabButton({
|
|
required ClubDetailTab tab,
|
|
required String label,
|
|
required IconData icon,
|
|
required MotorSignalThemeColors colors,
|
|
}) {
|
|
final isSelected = _selectedTab == tab;
|
|
|
|
return Expanded(
|
|
child: InkWell(
|
|
onTap: () => setState(() => _selectedTab = tab),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 9),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? colors.cyberCyan.withValues(alpha: 0.15) : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: isSelected
|
|
? Border.all(color: colors.cyberCyan.withValues(alpha: 0.6), width: 0.8)
|
|
: null,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 14,
|
|
color: isSelected ? colors.cyberCyan : Colors.white60,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 11,
|
|
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600,
|
|
letterSpacing: 0.3,
|
|
color: isSelected ? colors.cyberCyan : Colors.white60,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickShortcutsCard(
|
|
BuildContext context,
|
|
Club club,
|
|
MotorSignalThemeColors colors,
|
|
ThemeData theme,
|
|
) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () => setState(() => _selectedTab = ClubDetailTab.wall),
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 32,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color: colors.cyberCyan.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(Icons.forum_outlined, size: 16, color: colors.cyberCyan),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Club Wall',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 12.5,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
Text(
|
|
'Announcements & chat',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 10.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, size: 16, color: colors.cyberCyan),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () => setState(() => _selectedTab = ClubDetailTab.roster),
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 32,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFFB800).withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(Icons.groups_outlined, size: 16, color: Color(0xFFFFB800)),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Roster',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 12.5,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
Text(
|
|
'${club.memberCount} squad riders',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 10.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, size: 16, color: colors.cyberCyan),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMembershipActionCard(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
Club club,
|
|
AsyncValue<ClubMember?> memberAsync,
|
|
String? currentUserId,
|
|
bool isAuthenticated,
|
|
bool isMutating,
|
|
MotorSignalThemeColors colors,
|
|
ThemeData theme,
|
|
) {
|
|
if (isAuthenticated && memberAsync.isLoading) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(18.0),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(colors.cyberCyan),
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Checking Membership Status...',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'Verifying status with club registry.',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 12,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (isAuthenticated && memberAsync.hasError) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(18.0),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: colors.warningAmber.withValues(alpha: 0.6), width: 0.8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.warning_amber_rounded, size: 18, color: colors.warningAmber),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'Membership Status Unavailable',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'Could not load your membership record. Check connection and retry.',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 12,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
OutlinedButton.icon(
|
|
onPressed: () => ref.invalidate(currentMemberProvider(club.id)),
|
|
icon: const Icon(Icons.refresh, size: 16),
|
|
label: const Text('Retry'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: colors.cyberCyan,
|
|
side: BorderSide(color: colors.cyberCyan.withValues(alpha: 0.6)),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
final myMembership = memberAsync.valueOrNull;
|
|
final presentation = ClubMembershipPresentation.resolve(
|
|
club: club,
|
|
member: myMembership,
|
|
currentUserId: currentUserId,
|
|
isAuthenticated: isAuthenticated,
|
|
);
|
|
|
|
Color badgeBgColor;
|
|
Color badgeTextColor;
|
|
|
|
switch (presentation.badgeType) {
|
|
case PresentationBadgeType.green:
|
|
badgeBgColor = colors.launchGreen.withValues(alpha: 0.15);
|
|
badgeTextColor = colors.launchGreen;
|
|
break;
|
|
case PresentationBadgeType.amber:
|
|
badgeBgColor = colors.warningAmber.withValues(alpha: 0.15);
|
|
badgeTextColor = colors.warningAmber;
|
|
break;
|
|
case PresentationBadgeType.cyan:
|
|
badgeBgColor = colors.cyberCyan.withValues(alpha: 0.15);
|
|
badgeTextColor = colors.cyberCyan;
|
|
break;
|
|
case PresentationBadgeType.neutral:
|
|
badgeBgColor = colors.surfaceContainerHighest.withValues(alpha: 0.5);
|
|
badgeTextColor = theme.colorScheme.onSurfaceVariant;
|
|
break;
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(18.0),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(
|
|
color: presentation.badgeType == PresentationBadgeType.amber
|
|
? colors.warningAmber.withValues(alpha: 0.6)
|
|
: colors.outlineVariant,
|
|
width: 0.8,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: badgeBgColor,
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: badgeTextColor.withValues(alpha: 0.6), width: 0.8),
|
|
),
|
|
child: Text(
|
|
presentation.statusBadge,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: badgeTextColor,
|
|
letterSpacing: 0.4,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
presentation.statusHeadline,
|
|
style: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
presentation.statusDescription,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 12.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
height: 1.35,
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
if (isMutating)
|
|
Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: SizedBox(
|
|
height: 24,
|
|
width: 24,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(colors.cyberCyan),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
else ...[
|
|
if (presentation.state == ClubMembershipPresentationState.invited)
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () {
|
|
ref
|
|
.read(clubMembershipControllerProvider(club.id).notifier)
|
|
.declineInvitation(club.id);
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: colors.raceRed,
|
|
side: BorderSide(color: colors.raceRed.withValues(alpha: 0.6)),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
),
|
|
child: const Text(
|
|
'Decline',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: FilledButton.icon(
|
|
onPressed: () {
|
|
ref
|
|
.read(clubMembershipControllerProvider(club.id).notifier)
|
|
.acceptInvitation(club.id);
|
|
},
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: colors.launchGreen,
|
|
foregroundColor: Colors.black,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
),
|
|
icon: const Icon(Icons.check, size: 18),
|
|
label: const Text(
|
|
'Accept',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
else if (presentation.primaryAction != null)
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: _buildActionButton(
|
|
context,
|
|
ref,
|
|
club,
|
|
presentation,
|
|
colors,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActionButton(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
Club club,
|
|
ClubMembershipPresentation presentation,
|
|
MotorSignalThemeColors colors,
|
|
) {
|
|
final action = presentation.primaryAction!;
|
|
final label = presentation.primaryActionLabel ?? '';
|
|
final icon = presentation.primaryActionIcon;
|
|
|
|
if (action == ClubMembershipAction.signIn) {
|
|
return FilledButton.icon(
|
|
onPressed: () => context.push('/account'),
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: colors.cyberCyan,
|
|
foregroundColor: Colors.black,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
),
|
|
icon: Icon(icon, size: 18),
|
|
label: Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (action == ClubMembershipAction.leaveClub) {
|
|
return OutlinedButton.icon(
|
|
onPressed: () => _confirmLeaveClub(context, ref, club, colors),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: colors.raceRed,
|
|
side: BorderSide(color: colors.raceRed.withValues(alpha: 0.6)),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
),
|
|
icon: Icon(icon, size: 18),
|
|
label: Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (action == ClubMembershipAction.withdrawRequest) {
|
|
return OutlinedButton.icon(
|
|
onPressed: () {
|
|
ref.read(clubMembershipControllerProvider(club.id).notifier).withdrawJoinRequest(club.id);
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: colors.warningAmber,
|
|
side: BorderSide(color: colors.warningAmber.withValues(alpha: 0.6)),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
),
|
|
icon: Icon(icon, size: 18),
|
|
label: Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Default: FilledButton (Join Club or Request to Join)
|
|
return FilledButton.icon(
|
|
onPressed: () {
|
|
if (action == ClubMembershipAction.joinOpen) {
|
|
ref.read(clubMembershipControllerProvider(club.id).notifier).joinOpenClub(club.id);
|
|
} else if (action == ClubMembershipAction.requestToJoin) {
|
|
ref.read(clubMembershipControllerProvider(club.id).notifier).requestToJoinClub(club.id);
|
|
}
|
|
},
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: colors.cyberCyan,
|
|
foregroundColor: Colors.black,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
),
|
|
icon: Icon(icon, size: 18),
|
|
label: Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _confirmLeaveClub(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
Club club,
|
|
MotorSignalThemeColors colors,
|
|
) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogCtx) => AlertDialog(
|
|
scrollable: true,
|
|
title: Text('Leave ${club.name}?'),
|
|
content: const Text(
|
|
'Are you sure you want to leave this club? You will lose access to member-only rides and updates.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(dialogCtx).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
FilledButton(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: colors.raceRed,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(dialogCtx).pop();
|
|
ref.read(clubMembershipControllerProvider(club.id).notifier).leaveClub(club.id);
|
|
},
|
|
child: const Text('Leave Club'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDescriptionCard(
|
|
BuildContext context,
|
|
Club club,
|
|
MotorSignalThemeColors colors,
|
|
ThemeData theme,
|
|
) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(18.0),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'About the Club',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.2,
|
|
color: colors.cyberCyan,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
club.description.isNotEmpty
|
|
? club.description
|
|
: 'No club description provided by leadership.',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 13.5,
|
|
color: club.description.isNotEmpty
|
|
? theme.colorScheme.onSurface
|
|
: theme.colorScheme.onSurfaceVariant,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMetadataCard(
|
|
BuildContext context,
|
|
Club club,
|
|
MotorSignalThemeColors colors,
|
|
ThemeData theme,
|
|
) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(18.0),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Club Details',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.2,
|
|
color: colors.cyberCyan,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildDetailRow(
|
|
icon: Icons.location_on_outlined,
|
|
title: 'Home Region',
|
|
value: club.homeRegion?.trim().isNotEmpty == true ? club.homeRegion! : 'Global / Not Specified',
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
const Divider(height: 18),
|
|
_buildDetailRow(
|
|
icon: Icons.two_wheeler_outlined,
|
|
title: 'Motorcycle Focus',
|
|
value: club.motorcycleFocus?.trim().isNotEmpty == true
|
|
? club.motorcycleFocus!
|
|
: 'All Motorcycles & Disciplines',
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
const Divider(height: 18),
|
|
_buildDetailRow(
|
|
icon: Icons.calendar_today_outlined,
|
|
title: 'Established',
|
|
value: '${club.createdAt.year}-${club.createdAt.month.toString().padLeft(2, '0')}-${club.createdAt.day.toString().padLeft(2, '0')}',
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailRow({
|
|
required IconData icon,
|
|
required String title,
|
|
required String value,
|
|
required MotorSignalThemeColors colors,
|
|
required ThemeData theme,
|
|
}) {
|
|
return Row(
|
|
children: [
|
|
Icon(icon, size: 18, color: colors.cyberCyan),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 11.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 13.5,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPill({
|
|
required IconData icon,
|
|
required String label,
|
|
required MotorSignalThemeColors colors,
|
|
required ThemeData theme,
|
|
Color? tintColor,
|
|
}) {
|
|
final effectiveColor = tintColor ?? theme.colorScheme.onSurfaceVariant;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainerHighest.withValues(alpha: 0.6),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: colors.outlineVariant.withValues(alpha: 0.6), width: 0.6),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 13, color: effectiveColor),
|
|
const SizedBox(width: 5),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 11.5,
|
|
fontWeight: FontWeight.w600,
|
|
color: effectiveColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildClubRidesCard(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
Club club,
|
|
ClubMember? member,
|
|
MotorSignalThemeColors colors,
|
|
ThemeData theme,
|
|
) {
|
|
final isActiveMember = member != null && member.status.isActive;
|
|
final canPlanRides =
|
|
isActiveMember && member.role.isAtLeast(ClubRole.admin);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(18.0),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'CLUB RIDES',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.2,
|
|
color: colors.cyberCyan,
|
|
),
|
|
),
|
|
if (canPlanRides)
|
|
ElevatedButton.icon(
|
|
onPressed: () => context.push('/group-rides/create?clubId=${club.id}'),
|
|
icon: const Icon(Icons.add_road, size: 14),
|
|
label: const Text('Plan Club Ride'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: colors.cyberCyan,
|
|
foregroundColor: Colors.black,
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
textStyle: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 11.5,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (!isActiveMember)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
child: Text(
|
|
'Club rides are visible to active members only.',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 12.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
)
|
|
else
|
|
_buildClubDiscoveryList(context, ref, club.id, colors, theme),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildClubDiscoveryList(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
String clubId,
|
|
MotorSignalThemeColors colors,
|
|
ThemeData theme,
|
|
) {
|
|
final ridesAsync = ref.watch(clubDiscoveryRidesProvider(clubId));
|
|
|
|
return ridesAsync.when(
|
|
loading: () => const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 24.0),
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
),
|
|
),
|
|
error: (err, _) {
|
|
String message = 'Unable to load club rides at this time.';
|
|
if (err is ClubOperationException) {
|
|
switch (err.code) {
|
|
case 'unauthenticated':
|
|
message = 'Please sign in to view club rides.';
|
|
break;
|
|
case 'permission_denied':
|
|
message = 'Active club membership is required to view club rides.';
|
|
break;
|
|
case 'timeout':
|
|
case 'network_error':
|
|
message = 'Network connection issue. Please check your connection and try again.';
|
|
break;
|
|
default:
|
|
message = 'Unable to load club rides at this time.';
|
|
}
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
message,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 12.5,
|
|
color: theme.colorScheme.error,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
OutlinedButton.icon(
|
|
onPressed: () => ref.invalidate(clubDiscoveryRidesProvider(clubId)),
|
|
icon: const Icon(Icons.refresh, size: 14),
|
|
label: const Text('Retry'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: colors.cyberCyan,
|
|
side: BorderSide(color: colors.cyberCyan.withValues(alpha: 0.5)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
textStyle: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
data: (rides) {
|
|
if (rides.isEmpty) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.two_wheeler, size: 18, color: theme.colorScheme.onSurfaceVariant),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'No upcoming club rides scheduled.',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 12.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
final dateFormat = DateFormat('EEE, MMM d • h:mm a');
|
|
|
|
return Column(
|
|
children: rides.map((ride) {
|
|
final startLabel = dateFormat.format(ride.scheduledStartAt.toLocal());
|
|
final locationLabel = ride.meetingLocationLabel ?? 'Meeting area shared with participants';
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 10.0),
|
|
child: Material(
|
|
color: colors.surfaceContainerHighest.withValues(alpha: 0.4),
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: InkWell(
|
|
onTap: () => context.push('/group-rides/${ride.rideId}'),
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12.0),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: colors.outlineVariant.withValues(alpha: 0.4),
|
|
width: 0.8,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
ride.title,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 13.5,
|
|
fontWeight: FontWeight.w700,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: colors.cyberCyan.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(
|
|
color: colors.cyberCyan.withValues(alpha: 0.3),
|
|
width: 0.5,
|
|
),
|
|
),
|
|
child: Text(
|
|
ride.joinPolicy == 'open'
|
|
? 'OPEN'
|
|
: (ride.joinPolicy == 'requestToJoin' ? 'REQUEST TO JOIN' : 'INVITE ONLY'),
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w700,
|
|
color: colors.cyberCyan,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.calendar_today, size: 12, color: colors.cyberCyan),
|
|
const SizedBox(width: 5),
|
|
Text(
|
|
startLabel,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 11.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.location_on_outlined, size: 12, color: theme.colorScheme.onSurfaceVariant),
|
|
const SizedBox(width: 5),
|
|
Expanded(
|
|
child: Text(
|
|
locationLabel,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 11.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 4,
|
|
children: [
|
|
_buildPill(
|
|
icon: Icons.group,
|
|
label: '${ride.participantCount}${ride.participantLimit != null ? ' / ${ride.participantLimit}' : ''} riders',
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
if (ride.paceClassification != null && ride.paceClassification!.isNotEmpty)
|
|
_buildPill(
|
|
icon: Icons.speed,
|
|
label: ride.paceClassification!,
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
if (ride.organizerDisplayName != null || ride.organizerHandle != null)
|
|
_buildPill(
|
|
icon: Icons.person_pin,
|
|
label: ride.organizerDisplayName ?? ride.organizerHandle!,
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|