import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:trainhub_flutter/core/constants/ui_constants.dart'; import 'package:trainhub_flutter/core/theme/app_colors.dart'; import 'package:trainhub_flutter/data/services/grading_service.dart'; import 'package:trainhub_flutter/injection.dart'; /// Belt grading preparation: the ITF colored-belt syllabus as a checklist. /// Pick the belt you're testing for, tick off requirements as they're ready. @RoutePage() class GradingPage extends StatefulWidget { const GradingPage({super.key}); @override State createState() => _GradingPageState(); } class _GradingPageState extends State { late final GradingService _service = getIt(); @override void initState() { super.initState(); _service.addListener(_onChanged); _service.ensureLoaded(); } @override void dispose() { _service.removeListener(_onChanged); super.dispose(); } void _onChanged() { if (mounted) setState(() {}); } @override Widget build(BuildContext context) { final currentId = _service.currentBeltId; final current = kBeltSyllabus.where((b) => b.id == currentId).firstOrNull; return ListView( padding: const EdgeInsets.all(UIConstants.pagePadding), children: [ Text( 'GRADING PREP', style: GoogleFonts.chakraPetch( fontSize: 22, fontWeight: FontWeight.w700, letterSpacing: 1, color: AppColors.textPrimary, ), ), const SizedBox(height: UIConstants.spacing4), Text( current == null ? 'Tap a belt and set it as your current goal.' : 'Preparing for ${current.gup} ยท ${current.name}', style: GoogleFonts.inter( fontSize: 13, color: AppColors.textSecondary, ), ), if (current != null) ...[ const SizedBox(height: UIConstants.spacing16), _ReadinessBanner(belt: current, service: _service), ], const SizedBox(height: UIConstants.spacing24), for (final belt in kBeltSyllabus) ...[ _BeltCard( belt: belt, service: _service, isCurrent: belt.id == currentId, ), const SizedBox(height: UIConstants.spacing12), ], ], ); } } // --------------------------------------------------------------------------- // Readiness banner for the current belt // --------------------------------------------------------------------------- class _ReadinessBanner extends StatelessWidget { const _ReadinessBanner({required this.belt, required this.service}); final BeltLevel belt; final GradingService service; @override Widget build(BuildContext context) { final progress = service.progress(belt); final percent = (progress * 100).round(); final ready = progress >= 1.0; return Container( padding: const EdgeInsets.all(UIConstants.cardPadding), decoration: BoxDecoration( color: AppColors.surfaceContainer, borderRadius: UIConstants.cardBorderRadius, border: Border.all( color: ready ? AppColors.success : AppColors.accentBorder, ), ), child: Row( children: [ _BeltChip(belt: belt, size: 40), const SizedBox(width: UIConstants.spacing16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( ready ? 'READY FOR GRADING' : 'GRADING READINESS', style: GoogleFonts.jetBrainsMono( fontSize: 10, fontWeight: FontWeight.w600, letterSpacing: 1.5, color: ready ? AppColors.success : AppColors.accent, ), ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(3), child: LinearProgressIndicator( value: progress, minHeight: 6, backgroundColor: AppColors.surfaceContainerHigh, valueColor: AlwaysStoppedAnimation( ready ? AppColors.success : AppColors.accent, ), ), ), ], ), ), const SizedBox(width: UIConstants.spacing16), Text( '$percent%', style: GoogleFonts.jetBrainsMono( fontSize: 22, fontWeight: FontWeight.w700, color: ready ? AppColors.success : AppColors.textPrimary, ), ), ], ), ); } } // --------------------------------------------------------------------------- // Belt card with expandable requirement checklist // --------------------------------------------------------------------------- class _BeltCard extends StatelessWidget { const _BeltCard({ required this.belt, required this.service, required this.isCurrent, }); final BeltLevel belt; final GradingService service; final bool isCurrent; @override Widget build(BuildContext context) { final checked = service.checkedCount(belt.id); final total = belt.requirements.length; return Container( decoration: BoxDecoration( color: AppColors.surfaceContainer, borderRadius: UIConstants.cardBorderRadius, border: Border.all( color: isCurrent ? AppColors.accentBorder : AppColors.border, ), ), child: Theme( // Remove the ExpansionTile divider lines. data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( initiallyExpanded: isCurrent, tilePadding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing16, vertical: 4, ), leading: _BeltChip(belt: belt, size: 34), title: Row( children: [ Text( belt.gup, style: GoogleFonts.chakraPetch( fontSize: 14, fontWeight: FontWeight.w700, letterSpacing: 0.8, color: AppColors.textPrimary, ), ), const SizedBox(width: UIConstants.spacing12), Expanded( child: Text( belt.name, style: GoogleFonts.inter( fontSize: 12, color: AppColors.textSecondary, ), ), ), if (isCurrent) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: AppColors.accentMuted, borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColors.accentBorder), ), child: Text( 'CURRENT', style: GoogleFonts.jetBrainsMono( fontSize: 8, fontWeight: FontWeight.w700, letterSpacing: 1, color: AppColors.accent, ), ), ), ], ), subtitle: Padding( padding: const EdgeInsets.only(top: 4), child: Text( '$checked / $total requirements', style: GoogleFonts.jetBrainsMono( fontSize: 10, color: checked == total ? AppColors.success : AppColors.textMuted, ), ), ), children: [ for (var i = 0; i < belt.requirements.length; i++) CheckboxListTile( dense: true, controlAffinity: ListTileControlAffinity.leading, contentPadding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing16, ), value: service.isChecked(belt.id, i), onChanged: (_) => service.toggle(belt.id, i), title: Text( belt.requirements[i], style: GoogleFonts.inter( fontSize: 13, color: service.isChecked(belt.id, i) ? AppColors.textMuted : AppColors.textPrimary, decoration: service.isChecked(belt.id, i) ? TextDecoration.lineThrough : null, ), ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), child: Align( alignment: Alignment.centerLeft, child: OutlinedButton( onPressed: () => service.setCurrentBelt(isCurrent ? null : belt.id), child: Text( isCurrent ? 'UNSET CURRENT' : 'SET AS CURRENT GOAL', ), ), ), ), ], ), ), ); } } // --------------------------------------------------------------------------- // Belt color chip // --------------------------------------------------------------------------- class _BeltChip extends StatelessWidget { const _BeltChip({required this.belt, required this.size}); final BeltLevel belt; final double size; @override Widget build(BuildContext context) { return Container( width: size, height: size * 0.55, decoration: BoxDecoration( color: belt.baseColor, borderRadius: BorderRadius.circular(4), border: Border.all(color: AppColors.border), ), child: belt.stripeColor == null ? null : Center( child: Container(height: size * 0.14, color: belt.stripeColor), ), ); } }