299 lines
10 KiB
Dart
299 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../../../core/theme/app_typography.dart';
|
|
import '../../../../core/theme/theme_extensions.dart';
|
|
import '../../../../services/clubs/club_models.dart';
|
|
import 'club_monogram.dart';
|
|
|
|
/// Automotive styled card representing a MotorSignal Club in list views,
|
|
/// discovery grids, and membership directories.
|
|
class ClubCard extends StatelessWidget {
|
|
final Club club;
|
|
final ClubRole? role;
|
|
final VoidCallback? onTap;
|
|
final bool inGrid;
|
|
|
|
const ClubCard({
|
|
super.key,
|
|
required this.club,
|
|
this.role,
|
|
this.onTap,
|
|
this.inGrid = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = context.motorColors;
|
|
final theme = Theme.of(context);
|
|
|
|
return Container(
|
|
margin: inGrid ? EdgeInsets.zero : const EdgeInsets.symmetric(vertical: 6.0),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: colors.outlineVariant, width: 0.8),
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(14),
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(14.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header row: Monogram + Name/Slug + Optional Role Badge
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
ClubMonogram(name: club.name, size: 44),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
club.name,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
if (role != null && (role == ClubRole.owner || role == ClubRole.admin)) ...[
|
|
const SizedBox(width: 8),
|
|
_buildRolePill(context, role!, colors),
|
|
],
|
|
],
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'@${club.slug}',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontTelemetry,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: colors.cyberCyan,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Icon(
|
|
Icons.chevron_right,
|
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
|
size: 20,
|
|
),
|
|
],
|
|
),
|
|
|
|
// Optional description preview if present
|
|
if (club.description.isNotEmpty) ...[
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
club.description,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontBody,
|
|
fontSize: 12.5,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
height: 1.3,
|
|
),
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Badges row: Member count, admission policy, region, focus
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 6,
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
children: [
|
|
// Member count badge
|
|
_buildPill(
|
|
icon: Icons.people_outline,
|
|
label: '${club.memberCount} ${club.memberCount == 1 ? 'member' : 'members'}',
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
|
|
// Admission policy badge
|
|
if (role == null)
|
|
_buildAdmissionBadge(context, club.admissionPolicy, colors),
|
|
|
|
// Region badge if available
|
|
if (club.homeRegion != null && club.homeRegion!.trim().isNotEmpty)
|
|
_buildPill(
|
|
icon: Icons.location_on_outlined,
|
|
label: club.homeRegion!.trim(),
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
|
|
// Motorcycle focus badge if available
|
|
if (club.motorcycleFocus != null && club.motorcycleFocus!.trim().isNotEmpty)
|
|
_buildPill(
|
|
icon: Icons.two_wheeler_outlined,
|
|
label: club.motorcycleFocus!.trim(),
|
|
colors: colors,
|
|
theme: theme,
|
|
),
|
|
|
|
// Non-public visibility badge
|
|
if (club.visibility != ClubVisibility.public)
|
|
_buildPill(
|
|
icon: club.visibility == ClubVisibility.private
|
|
? Icons.lock_outline
|
|
: Icons.visibility_off_outlined,
|
|
label: club.visibility.label,
|
|
colors: colors,
|
|
theme: theme,
|
|
tintColor: colors.warningAmber,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRolePill(BuildContext context, ClubRole role, MotorSignalThemeColors colors) {
|
|
Color badgeColor;
|
|
Color textColor = Colors.black;
|
|
|
|
switch (role) {
|
|
case ClubRole.owner:
|
|
badgeColor = colors.warningAmber;
|
|
break;
|
|
case ClubRole.admin:
|
|
badgeColor = colors.cyberCyan;
|
|
break;
|
|
case ClubRole.member:
|
|
badgeColor = colors.outlineVariant;
|
|
textColor = Theme.of(context).colorScheme.onSurface;
|
|
break;
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: badgeColor,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
role.label.toUpperCase(),
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontTelemetry,
|
|
fontSize: 9.5,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: 0.4,
|
|
color: textColor,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAdmissionBadge(
|
|
BuildContext context,
|
|
ClubAdmissionPolicy policy,
|
|
MotorSignalThemeColors colors,
|
|
) {
|
|
Color color;
|
|
IconData icon;
|
|
|
|
switch (policy) {
|
|
case ClubAdmissionPolicy.open:
|
|
color = colors.launchGreen;
|
|
icon = Icons.group_add_outlined;
|
|
break;
|
|
case ClubAdmissionPolicy.request:
|
|
color = colors.cyberCyan;
|
|
icon = Icons.how_to_reg_outlined;
|
|
break;
|
|
case ClubAdmissionPolicy.inviteOnly:
|
|
color = colors.outline;
|
|
icon = Icons.mail_outline;
|
|
break;
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: color.withValues(alpha: 0.35), width: 0.8),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 12, color: color),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
policy.label,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainerHighest.withValues(alpha: 0.5),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: colors.outlineVariant.withValues(alpha: 0.5), width: 0.6),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 12, color: effectiveColor),
|
|
const SizedBox(width: 4),
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 160),
|
|
child: Text(
|
|
label,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontHeadline,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: effectiveColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|