1026 lines
35 KiB
Dart
1026 lines
35 KiB
Dart
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_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 'club_membership_controller.dart';
|
|
import 'club_membership_presentation.dart';
|
|
import 'widgets/club_monogram.dart';
|
|
|
|
/// Authoritative detail and membership lifecycle screen for a MotorSignal Club.
|
|
/// Enables riders to discover, join, request admission, withdraw requests,
|
|
/// accept/decline invitations, and leave active memberships.
|
|
class ClubDetailScreen extends ConsumerWidget {
|
|
final String clubId;
|
|
|
|
const ClubDetailScreen({
|
|
super.key,
|
|
required this.clubId,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
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(clubId));
|
|
final memberAsync = ref.watch(currentMemberProvider(clubId));
|
|
final actionState = ref.watch(clubMembershipControllerProvider(clubId));
|
|
final isMutating = actionState.isSubmitting;
|
|
|
|
// Listen for feedback snackbars
|
|
ref.listen<ClubMembershipActionState>(clubMembershipControllerProvider(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: 16),
|
|
|
|
// Interactive Membership Lifecycle Action Card
|
|
_buildMembershipActionCard(
|
|
context,
|
|
ref,
|
|
club,
|
|
memberAsync,
|
|
currentUserId,
|
|
isAuthenticated,
|
|
isMutating,
|
|
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),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
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.lock_open_outlined
|
|
: Icons.how_to_reg_outlined,
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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('/settings/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),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
child: Text(
|
|
isActiveMember
|
|
? 'Club ride discovery is coming soon.'
|
|
: 'Club rides are visible to active members only.',
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 12.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|