Compare commits

..

No commits in common. "9d49cef27111f723c19c5ae4220a5383102621d8" and "d12b01b4ab720e9ab3fe19d3d1d6a0c884d94ec5" have entirely different histories.

23 changed files with 1114 additions and 1230 deletions

View File

@ -2,12 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/config/app_constants.dart';
import '../providers/repository_provider.dart';
import '../widgets/game_over/game_over_header.dart';
import '../widgets/game_over/game_over_stats.dart';
import '../widgets/game_over/game_over_actions.dart';
import '../widgets/game_over/hinge_divider.dart';
import '../widgets/pokemon_image.dart';
/// Écran de fin de partie. Affiche le Pokémon manqué, les stats, et propose de rejouer.
class GameOverPage extends ConsumerStatefulWidget {
const GameOverPage({Key? key}) : super(key: key);
@ -37,18 +33,28 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
final pokemonImage = args?['pokemonImage'] ?? '';
final pokemonName = args?['pokemonName'] ?? 'Unknown';
final Map<String, dynamic>? args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
final String pokemonImage = args?['pokemonImage'] ?? '';
final String pokemonName = args?['pokemonName'] ?? 'Unknown';
final int streak = args?['streak'] ?? 0;
final int score = args?['score'] ?? 0;
const darkRed = Color(0xFF9E1B1B);
const silverBg = Color(0xFFC8D1D8);
const messageBoxBg = Color(0xFF1B2333);
// Pad streak with zeroes to 3 digits as in mockup (e.g. 004)
final String streakText = streak.toString().padLeft(3, '0');
// Define color palette from mockup
const Color pokedexRed = Color(0xFFD32F2F);
const Color darkRed = Color(0xFF9E1B1B);
const Color silverBg = Color(0xFFC8D1D8);
const Color messageBoxBg = Color(0xFF1B2333);
const Color statBoxBg = Color(0xFFD9E0E5); // slightly lighter/different silver for stats
const Color tryAgainBtn = Color(0xFF2962FF); // Blue
const Color backBtn = Color(0xFFA66A00); // Brown
return Scaffold(
backgroundColor: const Color(0xFFD32F2F),
backgroundColor: pokedexRed,
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.white))
: SingleChildScrollView(
@ -56,16 +62,109 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
GameOverHeader(pokemonImage: pokemonImage, pokemonName: pokemonName),
const HingeDivider(),
// Top Box: Pokemon Silhouette & GAME OVER
Container(
decoration: BoxDecoration(color: darkRed, border: Border.all(color: darkRed, width: 4)),
decoration: BoxDecoration(
color: darkRed, // Border color
border: Border.all(color: darkRed, width: 4),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
color: silverBg,
child: Column(
children: [
// GAME OVER Banner
Container(
color: darkRed,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: const Text(
"GAME OVER",
style: TextStyle(
fontSize: 26,
color: Colors.yellow,
fontWeight: FontWeight.bold,
letterSpacing: 2,
shadows: [
Shadow(
offset: Offset(1.5, 1.5),
color: Colors.black,
),
],
),
),
),
const SizedBox(height: 16),
// Pokemon Image and Name
if (pokemonImage.isNotEmpty)
SizedBox(
height: 140,
child: PokemonImage(
imageUrl: pokemonImage,
fit: BoxFit.contain,
),
),
const SizedBox(height: 12),
Text(
"It was $pokemonName!",
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF1B2333),
letterSpacing: 1,
),
),
const SizedBox(height: 8),
],
),
),
),
// Divider between boxes
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Row(
children: [
Expanded(
child: Container(height: 2, color: darkRed),
),
const SizedBox(width: 8),
Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) =>
Container(
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: const BoxDecoration(
color: darkRed,
shape: BoxShape.circle,
),
)
),
),
const SizedBox(width: 8),
Expanded(
child: Container(height: 2, color: darkRed),
),
],
),
),
// Bottom Box: Message, Stats, Buttons
Container(
decoration: BoxDecoration(
color: darkRed,
border: Border.all(color: darkRed, width: 4),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: silverBg,
child: Column(
children: [
// Message Box
Container(
width: double.infinity,
color: messageBoxBg,
@ -73,19 +172,158 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
child: const Text(
"\"Looks like your journey\nends here. You've run out\nof energy!\"",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 18, height: 1.5),
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.normal,
height: 1.5,
),
),
),
const SizedBox(height: 16),
GameOverStats(
streak: streak.toString().padLeft(3, '0'),
seen: "$_seenCount/${AppConstants.totalPokemon}",
score: "$score",
// Stats Row
Row(
children: [
Expanded(
child: Container(
color: statBoxBg,
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
const Text(
"STREAK",
style: TextStyle(
color: Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
streakText,
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
color: statBoxBg,
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
const Text(
"SEEN",
style: TextStyle(
color: Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
"$_seenCount/${AppConstants.totalPokemon}",
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
color: statBoxBg,
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
const Text(
"SCORE",
style: TextStyle(
color: Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
"$score",
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(height: 16),
GameOverActions(
onTryAgain: () => Navigator.pop(context, true),
onBack: () => Navigator.pop(context, false),
// Try Again Button
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context, true);
},
icon: const Icon(Icons.refresh, color: Colors.white, size: 24),
label: const Text(
"TRY AGAIN",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: tryAgainBtn,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
const SizedBox(height: 16),
// Back to Pokedex Button
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context, false);
},
icon: const Icon(Icons.menu_book, color: Colors.white, size: 24),
label: const Text(
"BACK TO POKEDEX",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: backBtn,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
],
),

View File

@ -1,17 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/config/app_constants.dart';
import '../../domain/game/game_state.dart';
import '../providers/gen_filter_provider.dart';
import '../providers/game_provider.dart';
import '../providers/navigation_provider.dart';
import '../widgets/scanline_overlay.dart';
import '../widgets/guess/guess_silhouette.dart';
import '../widgets/guess/gen_filter_section.dart';
import '../widgets/guess/lives_row.dart';
import '../widgets/guess/guess_input_section.dart';
import '../widgets/guess/score_board.dart';
import '../widgets/pokemon_image.dart';
/// Page de jeu "devine le Pokémon" : orchestre gameProvider et compose les sous-widgets.
class GuessPage extends ConsumerStatefulWidget {
const GuessPage({Key? key}) : super(key: key);
@ -27,6 +22,7 @@ class _GuessPageState extends ConsumerState<GuessPage> {
@override
void initState() {
super.initState();
// Démarre la partie après le premier frame (le provider est prêt).
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_started) {
_started = true;
@ -45,6 +41,7 @@ class _GuessPageState extends ConsumerState<GuessPage> {
final result = await ref.read(gameProvider.notifier).submitGuess(_guessController.text);
if (!mounted) return;
final state = ref.read(gameProvider);
switch (result) {
case GuessResult.correct:
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
@ -84,10 +81,16 @@ class _GuessPageState extends ConsumerState<GuessPage> {
if (playAgain == false) {
ref.read(selectedTabProvider.notifier).set(0); // onglet LIST
}
// true / false / null (geste retour) relancent tous une partie pour ne pas rester bloqué.
// true (Try Again), false (Back to Pokédex) et null (geste retour système)
// relancent tous une nouvelle partie pour ne pas rester bloqué en game over.
await ref.read(gameProvider.notifier).startNewGame();
}
String _maskedName(String name) {
if (name.length <= 2) return name; // trop court pour masquer utilement
return '${name[0]}${List.filled(name.length - 2, '_').join()}${name[name.length - 1]}';
}
@override
Widget build(BuildContext context) {
final state = ref.watch(gameProvider);
@ -101,42 +104,260 @@ class _GuessPageState extends ConsumerState<GuessPage> {
final pokemon = state.currentPokemon!;
final isGuessed = state.status == GameStatus.roundWon;
final notifier = ref.read(gameProvider.notifier);
return Container(
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
child: Stack(
children: [
const ScanlineOverlay(),
Positioned.fill(
child: ListView.builder(
itemCount: 100,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Container(
height: 4,
margin: const EdgeInsets.only(bottom: 4),
color: Colors.black.withAlpha(2),
),
),
),
SingleChildScrollView(
child: Column(
children: [
GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed),
GenFilterSection(
isOpen: _genFilterOpen,
onToggleOpen: () => setState(() => _genFilterOpen = !_genFilterOpen),
// Silhouette screen
Container(
height: 250,
width: double.infinity,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF3B6EE3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: isGuessed
? PokemonImage(
imageUrl: state.isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
fallbackUrl: pokemon.imageUrl,
fit: BoxFit.contain,
)
: PokemonImage(
imageUrl: state.isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
fallbackUrl: pokemon.imageUrl,
fit: BoxFit.contain,
color: state.isShiny ? Colors.yellow[700]! : Colors.black,
colorBlendMode: BlendMode.srcIn,
),
),
),
Container(
color: const Color(0xFF1B2333),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
state.isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
textAlign: TextAlign.center,
style: TextStyle(
color: state.isShiny ? Colors.yellow[400] : Colors.white,
fontSize: state.isShiny ? 18 : 22,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
)
],
),
),
// Gen filter
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => setState(() => _genFilterOpen = !_genFilterOpen),
icon: Icon(
_genFilterOpen ? Icons.expand_less : Icons.filter_list,
color: const Color(0xFF1B2333),
),
label: const Text(
'GEN FILTER',
style: TextStyle(
color: Color(0xFF1B2333),
fontWeight: FontWeight.bold,
fontSize: 16,
letterSpacing: 2,
),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF1B2333), width: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: _genFilterOpen
? _GenFilterPanel(
selectedGens: ref.watch(genFilterProvider),
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
)
: const SizedBox.shrink(),
),
],
),
),
const SizedBox(height: 12),
LivesRow(lives: state.lives),
const SizedBox(height: 16),
GuessInputSection(
controller: _guessController,
pokemonName: pokemon.formatedName,
isHintUsed: state.isHintUsed,
isGuessed: isGuessed,
hints: state.hints,
skips: state.skips,
onGuess: _onGuess,
onContinue: () => notifier.loadNextPokemon(),
onHint: (state.isHintUsed || state.hints <= 0) ? null : () => notifier.useHint(),
onSkip: state.skips > 0 ? () => notifier.useSkip() : null,
// Lives
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(AppConstants.startingLives, (index) {
return Icon(
index < state.lives ? Icons.favorite : Icons.favorite_border,
color: Colors.red,
size: 32,
);
}),
),
const SizedBox(height: 24),
const SizedBox(height: 16),
// Guess section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: ScoreBoard(currentScore: state.currentScore, bestScore: state.bestScore),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("IDENTIFICATION INPUT",
style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
if (state.isHintUsed)
Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.amber[100],
border: Border.all(color: Colors.amber[600]!, width: 2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
"HINT: ${_maskedName(pokemon.formatedName)}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4),
),
),
Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey[400]!),
),
child: TextField(
controller: _guessController,
style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none,
hintText: 'Enter Pokémon name...',
),
onSubmitted: (_) => _onGuess(),
),
),
const SizedBox(height: 16),
if (isGuessed)
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: () => ref.read(gameProvider.notifier).loadNextPokemon(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
child: const Text("CONTINUE",
style: TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2)),
),
)
else ...[
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: _onGuess,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B6EE3),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
child: const Text("GUESS!",
style: TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2)),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: (state.isHintUsed || state.hints <= 0)
? null
: () => ref.read(gameProvider.notifier).useHint(),
icon: const Icon(Icons.lightbulb, color: Colors.black87),
label: Text("HINT (${state.hints})",
style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.amber,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: state.skips > 0
? () => ref.read(gameProvider.notifier).useSkip()
: null,
icon: const Icon(Icons.skip_next, color: Colors.black87),
label: Text("SKIP (${state.skips})",
style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[400],
padding: const EdgeInsets.symmetric(vertical: 16),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
],
),
],
const SizedBox(height: 24),
// Score
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withAlpha(153),
border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
const Text("CURRENT SCORE",
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold)),
Text("${state.currentScore}",
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF3B6EE3))),
const Divider(height: 24),
Text("PERSONAL BEST: ${state.bestScore}",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87)),
],
),
),
],
),
),
const SizedBox(height: 32),
],
@ -147,3 +368,73 @@ class _GuessPageState extends ConsumerState<GuessPage> {
);
}
}
class _GenFilterPanel extends StatelessWidget {
final Set<int> selectedGens;
final void Function(int) onToggle;
const _GenFilterPanel({required this.selectedGens, required this.onToggle});
static const _genNames = ['Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'];
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFF1B2333), width: 2),
),
child: Column(
children: List.generate(AppConstants.genRanges.length, (i) {
final range = AppConstants.genRanges[i];
final isSelected = selectedGens.contains(i);
final isLast = i == AppConstants.genRanges.length - 1;
return InkWell(
onTap: () => onToggle(i),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF1B2333).withAlpha(12) : Colors.transparent,
border: isLast ? null : Border(bottom: BorderSide(color: Colors.grey.shade200)),
),
child: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 20,
height: 20,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF3B6EE3) : Colors.transparent,
border: Border.all(
color: isSelected ? const Color(0xFF3B6EE3) : Colors.grey.shade400,
width: 2,
),
borderRadius: BorderRadius.circular(3),
),
child: isSelected ? const Icon(Icons.check, size: 13, color: Colors.white) : null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_genNames[i],
style: TextStyle(
fontSize: 16,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? const Color(0xFF1B2333) : Colors.black54,
),
),
),
Text(
'#${range.$1}#${range.$2}',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
],
),
),
);
}),
),
);
}
}

View File

@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import '../../domain/entities/pokemon.dart';
import '../widgets/detail/pokemon_detail_top.dart';
import '../widgets/detail/pokemon_stats_panel.dart';
import '../widgets/pokemon_type.dart';
import '../widgets/pokemon_image.dart';
/// Fiche détaillée d'un Pokémon (reçu via les arguments de route).
class PokemonDetailPage extends StatefulWidget {
const PokemonDetailPage({Key? key}) : super(key: key);
@ -14,9 +13,57 @@ class PokemonDetailPage extends StatefulWidget {
class _PokemonDetailPageState extends State<PokemonDetailPage> {
bool _isShiny = false;
Widget _buildStatBar(String label, int value, Color color) {
// Let's assume max base stat is 255
double ratio = (value / 255).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
SizedBox(
width: 50,
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
),
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: Colors.grey[400],
),
child: Row(
children: [
Expanded(
flex: (ratio * 100).toInt(),
child: Container(color: color),
),
Expanded(
flex: 100 - (ratio * 100).toInt(),
child: Container(),
),
],
),
),
),
const SizedBox(width: 16),
SizedBox(
width: 40,
child: Text(
value.toString(),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
textAlign: TextAlign.right,
),
)
],
),
);
}
@override
Widget build(BuildContext context) {
final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
final Pokemon pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
return Scaffold(
backgroundColor: const Color(0xFF1B2333),
@ -32,30 +79,8 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
child: SingleChildScrollView(
child: Column(
children: [
_backBar(context),
PokemonDetailTop(
pokemon: pokemon,
isShiny: _isShiny,
onToggleShiny: () => setState(() => _isShiny = !_isShiny),
),
const SizedBox(height: 20),
_hinge(),
const SizedBox(height: 20),
PokemonStatsPanel(pokemon: pokemon),
const SizedBox(height: 30),
_bottomDots(),
const SizedBox(height: 20),
],
),
),
),
),
),
);
}
Widget _backBar(BuildContext context) {
return Container(
// App Bar / Top Red Padding
Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
@ -63,33 +88,198 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle),
decoration: const BoxDecoration(
color: Color(0xFFA12020),
shape: BoxShape.circle),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
),
);
}
),
Widget _hinge() {
return Row(
// TOP SCREEN
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFF1B2333),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
),
child: Container(
color: const Color(0xFF90A4AE),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: const Color(0xFF1B2333),
child: Text(
"NO. ${pokemon.id.toString().padLeft(3, '0')}",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
GestureDetector(
onTap: () {
setState(() {
_isShiny = !_isShiny;
});
},
child: Container(
height: 180,
alignment: Alignment.center,
color: const Color(0xFF81CCA5).withAlpha(153), // subtle green background behind sprite
child: PokemonImage(
imageUrl: _isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
fallbackUrl: _isShiny ? pokemon.imageUrl : null,
fit: BoxFit.contain,
),
),
),
Container(
color: const Color(0xFF37474F),
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
pokemon.formatedName.toUpperCase(),
style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 2),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Row(
children: [
PokemonTypeWidget(pokemon.type1),
if (pokemon.type2 != null) const SizedBox(width: 4),
if (pokemon.type2 != null) PokemonTypeWidget(pokemon.type2!),
],
)
],
),
)
],
),
),
),
// HINGE DETAILS
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
],
);
}
),
const SizedBox(height: 20),
Widget _bottomDots() {
return Row(
// BOTTOM SCREEN
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF1B2333),
borderRadius: BorderRadius.circular(8),
),
child: Container(
color: const Color(0xFFC8D1D8),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"BASE STATS",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2),
),
Text(
"MODEL: DS-01",
style: TextStyle(fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.bold),
)
],
),
const Divider(color: Colors.black38, thickness: 2, height: 20),
_buildStatBar("HP", pokemon.hp, const Color(0xFFE53935)),
_buildStatBar("ATK", pokemon.atk, const Color(0xFFFB8C00)),
_buildStatBar("DEF", pokemon.def, const Color(0xFFFDD835)),
_buildStatBar("SPD", pokemon.spd, const Color(0xFF1E88E5)),
const SizedBox(height: 24),
// DESCRIPTION BOX
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFE2EBF0),
border: Border.all(color: Colors.grey[400]!),
),
child: Text(
pokemon.description != null && pokemon.description!.isNotEmpty
? '"${pokemon.description!}"'
: '"No description available for this Pokémon."',
style: const TextStyle(fontSize: 16, height: 1.5),
),
),
const SizedBox(height: 16),
// DECORATIVE LIGHTS
Row(
children: [
Container(
width: 24, height: 24,
decoration: BoxDecoration(
color: const Color(0xFF1E88E5), shape: BoxShape.circle,
border: Border.all(color: const Color(0xFF1565C0), width: 2),
),
),
const SizedBox(width: 8),
Container(
width: 24, height: 24,
decoration: BoxDecoration(
color: const Color(0xFFFFB300), shape: BoxShape.circle,
border: Border.all(color: const Color(0xFFF57C00), width: 2),
),
),
const Spacer(),
Row(
children: [
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
const SizedBox(width: 4),
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
],
)
],
)
],
),
),
),
const SizedBox(height: 30),
// BOTTOM DOTS
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
4,
(i) => Container(
width: 6,
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 2),
decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle),
children: [
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
const SizedBox(width: 4),
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
const SizedBox(width: 4),
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
const SizedBox(width: 4),
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
],
),
const SizedBox(height: 20),
],
),
),
),
),
),
);

View File

@ -3,11 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/pokemon.dart';
import '../providers/pokedex_provider.dart';
import '../widgets/pokemon_tile.dart';
import '../widgets/scanline_overlay.dart';
import '../widgets/list/pokedex_list_header.dart';
import '../widgets/list/pokedex_count_bar.dart';
/// Liste du Pokédex national avec filtre ALL / CAUGHT.
class PokemonListPage extends ConsumerStatefulWidget {
const PokemonListPage({Key? key}) : super(key: key);
@ -39,7 +35,21 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
child: Column(
children: [
const PokedexListHeader(),
// Header
Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
color: const Color(0xFF90A4AE),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.menu, color: Colors.black87),
Text('LIST - NATIONAL',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
Icon(Icons.search, color: Colors.black87),
],
),
),
// Tabs
Container(
color: const Color(0xFF90A4AE),
height: 40,
@ -50,20 +60,62 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
],
),
),
PokedexCountBar(caught: caughtCount, total: pokedexAsync.valueOrNull?.length ?? 0),
// Caught Count Bar
Container(
padding: const EdgeInsets.symmetric(vertical: 12.0),
decoration: const BoxDecoration(
color: Color(0xFFB0BEC5),
border: Border(bottom: BorderSide(color: Color(0xFF78909C), width: 2)),
),
child: Column(
children: [
Text(
'${caughtCount.toString().padLeft(3, '0')} / ${pokedexAsync.valueOrNull?.length ?? 0}',
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const Text('POKEMON DISCOVERED',
style: TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)),
],
),
),
// The List
Expanded(
child: Stack(
children: [
const ScanlineOverlay(),
Positioned.fill(
child: ListView.builder(
itemCount: 100,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Container(
height: 4,
margin: const EdgeInsets.only(bottom: 4),
color: Colors.black.withAlpha(2),
),
),
),
pokedexAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Text('Erreur de chargement\n$e',
textAlign: TextAlign.center, style: const TextStyle(color: Colors.black54)),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.black54)),
),
data: (all) {
final filtered = _applyFilter(all);
if (filtered.isEmpty) return _emptyState();
if (filtered.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_off, size: 64, color: Colors.black26),
const SizedBox(height: 16),
Text('NO POKEMON FOUND IN $_filter',
style: const TextStyle(
color: Colors.black45, fontSize: 18, fontWeight: FontWeight.bold)),
],
),
);
}
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(12),
@ -75,6 +127,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
],
),
),
// Footer
Container(
height: 24,
color: const Color(0xFF1B2333),
@ -87,20 +140,6 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
);
}
Widget _emptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_off, size: 64, color: Colors.black26),
const SizedBox(height: 16),
Text('NO POKEMON FOUND IN $_filter',
style: const TextStyle(color: Colors.black45, fontSize: 18, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildTab(String title, bool isSelected) {
return Expanded(
child: GestureDetector(
@ -120,7 +159,9 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
alignment: Alignment.center,
child: Text(title,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: isSelected ? Colors.black : Colors.black54)),
fontSize: 18,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.black : Colors.black54)),
),
),
);

View File

@ -4,12 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../../core/config/app_constants.dart';
import '../providers/pokedex_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/system/system_header.dart';
import '../widgets/system/section_title.dart';
import '../widgets/system/system_stats.dart';
import '../widgets/system/palette_picker.dart';
/// Page "Système" : statistiques de jeu et sélection de la palette de couleurs.
class SystemPage extends ConsumerWidget {
const SystemPage({super.key});
@ -28,14 +23,14 @@ class SystemPage extends ConsumerWidget {
color: const Color(0xFFC8D1D8),
child: Column(
children: [
SystemHeader(primaryColor: palette.primary),
_Header(primaryColor: palette.primary),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SectionTitle(text: 'STATISTIQUES', color: palette.primary),
_SectionTitle(text: 'STATISTIQUES', color: palette.primary),
const SizedBox(height: 8),
pokedexAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
@ -49,13 +44,13 @@ class SystemPage extends ConsumerWidget {
future: _loadBestScore(),
builder: (context, snap) {
final best = snap.data ?? 0;
return StatsGrid(
return _StatsGrid(
primaryColor: palette.primary,
items: [
StatItem(label: 'Meilleur score', value: '$best', icon: Icons.emoji_events),
StatItem(label: 'Attrapés', value: '$caught / $total', icon: Icons.catching_pokemon),
StatItem(label: 'Vus', value: '$seen / $total', icon: Icons.visibility),
StatItem(label: 'Complétion', value: '$pct%', icon: Icons.pie_chart),
_StatItem(label: 'Meilleur score', value: '$best', icon: Icons.emoji_events),
_StatItem(label: 'Attrapés', value: '$caught / $total', icon: Icons.catching_pokemon),
_StatItem(label: 'Vus', value: '$seen / $total', icon: Icons.visibility),
_StatItem(label: 'Complétion', value: '$pct%', icon: Icons.pie_chart),
],
);
},
@ -63,9 +58,9 @@ class SystemPage extends ConsumerWidget {
},
),
const SizedBox(height: 24),
SectionTitle(text: 'PALETTE DE COULEURS', color: palette.primary),
_SectionTitle(text: 'PALETTE DE COULEURS', color: palette.primary),
const SizedBox(height: 8),
PalettePicker(
_PalettePicker(
selectedIndex: paletteIndex,
onSelect: (i) => ref.read(themeProvider.notifier).setPalette(i),
),
@ -78,3 +73,188 @@ class SystemPage extends ConsumerWidget {
);
}
}
class _Header extends StatelessWidget {
final Color primaryColor;
const _Header({required this.primaryColor});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: primaryColor,
border: Border(bottom: BorderSide(color: primaryColor.withAlpha(180), width: 3)),
),
child: const Text(
'SYSTÈME',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: 4,
),
),
);
}
}
class _SectionTitle extends StatelessWidget {
final String text;
final Color color;
const _SectionTitle({required this.text, required this.color});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(width: 4, height: 20, color: color),
const SizedBox(width: 8),
Text(
text,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
letterSpacing: 2,
),
),
],
);
}
}
class _StatItem {
final String label;
final String value;
final IconData icon;
const _StatItem({required this.label, required this.value, required this.icon});
}
class _StatsGrid extends StatelessWidget {
final Color primaryColor;
final List<_StatItem> items;
const _StatsGrid({required this.primaryColor, required this.items});
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.4,
children: items.map((item) => _StatCard(item: item, color: primaryColor)).toList(),
);
}
}
class _StatCard extends StatelessWidget {
final _StatItem item;
final Color color;
const _StatCard({required this.item, required this.color});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withAlpha(80), width: 2),
boxShadow: [
BoxShadow(color: Colors.black.withAlpha(25), blurRadius: 4, offset: const Offset(0, 2)),
],
),
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(item.icon, color: color, size: 28),
const SizedBox(height: 6),
Text(
item.value,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color),
),
Text(
item.label,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
],
),
);
}
}
class _PalettePicker extends StatelessWidget {
final int selectedIndex;
final void Function(int) onSelect;
const _PalettePicker({required this.selectedIndex, required this.onSelect});
@override
Widget build(BuildContext context) {
return Column(
children: List.generate(appPalettes.length, (i) {
final p = appPalettes[i];
final isSelected = i == selectedIndex;
return GestureDetector(
onTap: () => onSelect(i),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? p.primary : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? p.primary : Colors.grey.shade300,
width: isSelected ? 3 : 1.5,
),
boxShadow: isSelected
? [BoxShadow(color: p.primary.withAlpha(80), blurRadius: 8, offset: const Offset(0, 3))]
: [],
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: p.primary,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
const SizedBox(width: 8),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: p.surface,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5),
),
),
const SizedBox(width: 16),
Text(
p.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : Colors.black87,
),
),
const Spacer(),
if (isSelected)
const Icon(Icons.check_circle, color: Colors.white, size: 22),
],
),
),
);
}),
);
}
}

View File

@ -1,83 +0,0 @@
import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.dart';
import '../pokemon_type.dart';
import '../pokemon_image.dart';
/// Écran supérieur du détail : numéro, sprite (tap pour basculer shiny), nom et types.
class PokemonDetailTop extends StatelessWidget {
final Pokemon pokemon;
final bool isShiny;
final VoidCallback onToggleShiny;
const PokemonDetailTop({
super.key,
required this.pokemon,
required this.isShiny,
required this.onToggleShiny,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFF1B2333),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
),
child: Container(
color: const Color(0xFF90A4AE),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: const Color(0xFF1B2333),
child: Text(
"NO. ${pokemon.id.toString().padLeft(3, '0')}",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
GestureDetector(
onTap: onToggleShiny,
child: Container(
height: 180,
alignment: Alignment.center,
color: const Color(0xFF81CCA5).withAlpha(153),
child: PokemonImage(
imageUrl: isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
fallbackUrl: isShiny ? pokemon.imageUrl : null,
fit: BoxFit.contain,
),
),
),
Container(
color: const Color(0xFF37474F),
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
pokemon.formatedName.toUpperCase(),
style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 2),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Row(
children: [
PokemonTypeWidget(pokemon.type1),
if (pokemon.type2 != null) const SizedBox(width: 4),
if (pokemon.type2 != null) PokemonTypeWidget(pokemon.type2!),
],
),
],
),
),
],
),
),
);
}
}

View File

@ -1,131 +0,0 @@
import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.dart';
/// Écran inférieur du détail : stats de base, description et éléments décoratifs.
class PokemonStatsPanel extends StatelessWidget {
final Pokemon pokemon;
const PokemonStatsPanel({super.key, required this.pokemon});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: const Color(0xFF1B2333), borderRadius: BorderRadius.circular(8)),
child: Container(
color: const Color(0xFFC8D1D8),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("BASE STATS", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
Text("MODEL: DS-01", style: TextStyle(fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.bold)),
],
),
const Divider(color: Colors.black38, thickness: 2, height: 20),
_StatBar(label: "HP", value: pokemon.hp, color: const Color(0xFFE53935)),
_StatBar(label: "ATK", value: pokemon.atk, color: const Color(0xFFFB8C00)),
_StatBar(label: "DEF", value: pokemon.def, color: const Color(0xFFFDD835)),
_StatBar(label: "SPD", value: pokemon.spd, color: const Color(0xFF1E88E5)),
const SizedBox(height: 24),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: const Color(0xFFE2EBF0), border: Border.all(color: Colors.grey[400]!)),
child: Text(
pokemon.description != null && pokemon.description!.isNotEmpty
? '"${pokemon.description!}"'
: '"No description available for this Pokémon."',
style: const TextStyle(fontSize: 16, height: 1.5),
),
),
const SizedBox(height: 16),
const _DecorativeLights(),
],
),
),
);
}
}
/// Barre d'une statistique (libellé, jauge proportionnelle, valeur).
class _StatBar extends StatelessWidget {
final String label;
final int value;
final Color color;
const _StatBar({required this.label, required this.value, required this.color});
@override
Widget build(BuildContext context) {
final ratio = (value / 255).clamp(0.0, 1.0); // stat de base max supposée = 255
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
SizedBox(width: 50, child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(color: Colors.grey[400]),
child: Row(
children: [
Expanded(flex: (ratio * 100).toInt(), child: Container(color: color)),
Expanded(flex: 100 - (ratio * 100).toInt(), child: Container()),
],
),
),
),
const SizedBox(width: 16),
SizedBox(
width: 40,
child: Text(value.toString(),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), textAlign: TextAlign.right),
),
],
),
);
}
}
/// Petites diodes décoratives en bas du panneau (purement cosmétiques).
class _DecorativeLights extends StatelessWidget {
const _DecorativeLights();
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: const Color(0xFF1E88E5),
shape: BoxShape.circle,
border: Border.all(color: const Color(0xFF1565C0), width: 2),
),
),
const SizedBox(width: 8),
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: const Color(0xFFFFB300),
shape: BoxShape.circle,
border: Border.all(color: const Color(0xFFF57C00), width: 2),
),
),
const Spacer(),
Row(
children: [
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
const SizedBox(width: 4),
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
],
),
],
);
}
}

View File

@ -1,44 +0,0 @@
import 'package:flutter/material.dart';
/// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex.
class GameOverActions extends StatelessWidget {
final VoidCallback onTryAgain;
final VoidCallback onBack;
const GameOverActions({super.key, required this.onTryAgain, required this.onBack});
@override
Widget build(BuildContext context) {
return Column(
children: [
_button(label: "TRY AGAIN", icon: Icons.refresh, color: const Color(0xFF2962FF), onPressed: onTryAgain),
const SizedBox(height: 16),
_button(label: "BACK TO POKEDEX", icon: Icons.menu_book, color: const Color(0xFFA66A00), onPressed: onBack),
],
);
}
Widget _button({
required String label,
required IconData icon,
required Color color,
required VoidCallback onPressed,
}) {
return SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon, color: Colors.white, size: 24),
label: Text(
label,
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 2),
),
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
);
}
}

View File

@ -1,58 +0,0 @@
import 'package:flutter/material.dart';
import '../pokemon_image.dart';
/// Bloc supérieur de l'écran game over : bannière "GAME OVER", image et nom du Pokémon.
class GameOverHeader extends StatelessWidget {
final String pokemonImage;
final String pokemonName;
const GameOverHeader({super.key, required this.pokemonImage, required this.pokemonName});
static const _darkRed = Color(0xFF9E1B1B);
static const _silverBg = Color(0xFFC8D1D8);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: _darkRed, border: Border.all(color: _darkRed, width: 4)),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
color: _silverBg,
child: Column(
children: [
Container(
color: _darkRed,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: const Text(
"GAME OVER",
style: TextStyle(
fontSize: 26,
color: Colors.yellow,
fontWeight: FontWeight.bold,
letterSpacing: 2,
shadows: [Shadow(offset: Offset(1.5, 1.5), color: Colors.black)],
),
),
),
const SizedBox(height: 16),
if (pokemonImage.isNotEmpty)
SizedBox(height: 140, child: PokemonImage(imageUrl: pokemonImage, fit: BoxFit.contain)),
const SizedBox(height: 12),
Text(
"It was $pokemonName!",
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF1B2333),
letterSpacing: 1,
),
),
const SizedBox(height: 8),
],
),
),
);
}
}

View File

@ -1,52 +0,0 @@
import 'package:flutter/material.dart';
/// Rangée de statistiques de fin de partie (STREAK / SEEN / SCORE).
class GameOverStats extends StatelessWidget {
final String streak;
final String seen;
final String score;
const GameOverStats({super.key, required this.streak, required this.seen, required this.score});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(child: _StatBox(label: "STREAK", value: streak)),
const SizedBox(width: 16),
Expanded(child: _StatBox(label: "SEEN", value: seen)),
const SizedBox(width: 16),
Expanded(child: _StatBox(label: "SCORE", value: score)),
],
);
}
}
/// Une case de statistique : un libellé rouge au-dessus d'une valeur.
class _StatBox extends StatelessWidget {
final String label;
final String value;
const _StatBox({required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xFFD9E0E5),
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
Text(
label,
style: const TextStyle(color: Colors.red, fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 1),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
);
}
}

View File

@ -1,35 +0,0 @@
import 'package:flutter/material.dart';
/// Séparateur décoratif (deux traits + trois points) entre les blocs de l'écran game over.
class HingeDivider extends StatelessWidget {
const HingeDivider({super.key});
static const _darkRed = Color(0xFF9E1B1B);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Row(
children: [
Expanded(child: Container(height: 2, color: _darkRed)),
const SizedBox(width: 8),
Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
3,
(index) => Container(
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: const BoxDecoration(color: _darkRed, shape: BoxShape.circle),
),
),
),
const SizedBox(width: 8),
Expanded(child: Container(height: 2, color: _darkRed)),
],
),
);
}
}

View File

@ -1,132 +0,0 @@
import 'package:flutter/material.dart';
import '../../../core/config/app_constants.dart';
/// Bouton "GEN FILTER" et son panneau dépliable de sélection des générations.
class GenFilterSection extends StatelessWidget {
final bool isOpen;
final VoidCallback onToggleOpen;
final Set<int> selectedGens;
final void Function(int) onToggle;
const GenFilterSection({
super.key,
required this.isOpen,
required this.onToggleOpen,
required this.selectedGens,
required this.onToggle,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: onToggleOpen,
icon: Icon(
isOpen ? Icons.expand_less : Icons.filter_list,
color: const Color(0xFF1B2333),
),
label: const Text(
'GEN FILTER',
style: TextStyle(
color: Color(0xFF1B2333),
fontWeight: FontWeight.bold,
fontSize: 16,
letterSpacing: 2,
),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF1B2333), width: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: isOpen
? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle)
: const SizedBox.shrink(),
),
],
),
);
}
}
class _GenFilterPanel extends StatelessWidget {
final Set<int> selectedGens;
final void Function(int) onToggle;
const _GenFilterPanel({required this.selectedGens, required this.onToggle});
static const _genNames = [
'Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'
];
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFF1B2333), width: 2),
),
child: Column(
children: List.generate(AppConstants.genRanges.length, (i) {
final range = AppConstants.genRanges[i];
final isSelected = selectedGens.contains(i);
final isLast = i == AppConstants.genRanges.length - 1;
return InkWell(
onTap: () => onToggle(i),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF1B2333).withAlpha(12) : Colors.transparent,
border: isLast ? null : Border(bottom: BorderSide(color: Colors.grey.shade200)),
),
child: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 20,
height: 20,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF3B6EE3) : Colors.transparent,
border: Border.all(
color: isSelected ? const Color(0xFF3B6EE3) : Colors.grey.shade400,
width: 2,
),
borderRadius: BorderRadius.circular(3),
),
child: isSelected ? const Icon(Icons.check, size: 13, color: Colors.white) : null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_genNames[i],
style: TextStyle(
fontSize: 16,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? const Color(0xFF1B2333) : Colors.black54,
),
),
),
Text(
'#${range.$1}#${range.$2}',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
],
),
),
);
}),
),
);
}
}

View File

@ -1,136 +0,0 @@
import 'package:flutter/material.dart';
/// Section de saisie : indice optionnel, champ de réponse et boutons d'action
/// (Guess / Continue / Hint / Skip). Purement présentationnelle : tout passe par les callbacks.
class GuessInputSection extends StatelessWidget {
final TextEditingController controller;
final String pokemonName;
final bool isHintUsed;
final bool isGuessed;
final int hints;
final int skips;
final VoidCallback onGuess;
final VoidCallback onContinue;
final VoidCallback? onHint;
final VoidCallback? onSkip;
const GuessInputSection({
super.key,
required this.controller,
required this.pokemonName,
required this.isHintUsed,
required this.isGuessed,
required this.hints,
required this.skips,
required this.onGuess,
required this.onContinue,
required this.onHint,
required this.onSkip,
});
/// Masque le nom en ne laissant visibles que la première et la dernière lettre.
String _maskedName(String name) {
if (name.length <= 2) return name; // trop court pour masquer utilement
return '${name[0]}${List.filled(name.length - 2, '_').join()}${name[name.length - 1]}';
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"IDENTIFICATION INPUT",
style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
if (isHintUsed)
Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.amber[100],
border: Border.all(color: Colors.amber[600]!, width: 2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
"HINT: ${_maskedName(pokemonName)}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4),
),
),
Container(
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: Colors.grey[400]!)),
child: TextField(
controller: controller,
style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none,
hintText: 'Enter Pokémon name...',
),
onSubmitted: (_) => onGuess(),
),
),
const SizedBox(height: 16),
if (isGuessed)
_bigButton(label: "CONTINUE", color: Colors.green, onPressed: onContinue)
else ...[
_bigButton(label: "GUESS!", color: const Color(0xFF3B6EE3), onPressed: onGuess),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _actionButton(icon: Icons.lightbulb, label: "HINT ($hints)", color: Colors.amber, onPressed: onHint),
),
const SizedBox(width: 8),
Expanded(
child: _actionButton(icon: Icons.skip_next, label: "SKIP ($skips)", color: Colors.grey[400]!, onPressed: onSkip),
),
],
),
],
],
),
);
}
Widget _bigButton({required String label, required Color color, required VoidCallback onPressed}) {
return SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
child: Text(
label,
style: const TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2),
),
),
);
}
Widget _actionButton({
required IconData icon,
required String label,
required Color color,
required VoidCallback? onPressed,
}) {
return ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon, color: Colors.black87),
label: Text(label, style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: color,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
);
}
}

View File

@ -1,70 +0,0 @@
import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.dart';
import '../pokemon_image.dart';
/// Écran bleu affichant la silhouette (jeu en cours) ou l'image révélée (manche gagnée).
class GuessSilhouette extends StatelessWidget {
final Pokemon pokemon;
final bool isShiny;
final bool isGuessed;
const GuessSilhouette({
super.key,
required this.pokemon,
required this.isShiny,
required this.isGuessed,
});
@override
Widget build(BuildContext context) {
final imageUrl = isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl;
return Container(
height: 250,
width: double.infinity,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF3B6EE3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: isGuessed
? PokemonImage(
imageUrl: imageUrl,
fallbackUrl: pokemon.imageUrl,
fit: BoxFit.contain,
)
: PokemonImage(
imageUrl: imageUrl,
fallbackUrl: pokemon.imageUrl,
fit: BoxFit.contain,
color: isShiny ? Colors.yellow[700]! : Colors.black,
colorBlendMode: BlendMode.srcIn,
),
),
),
Container(
color: const Color(0xFF1B2333),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
textAlign: TextAlign.center,
style: TextStyle(
color: isShiny ? Colors.yellow[400] : Colors.white,
fontSize: isShiny ? 18 : 22,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
),
],
),
);
}
}

View File

@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
import '../../../core/config/app_constants.dart';
/// Rangée de cœurs représentant les vies restantes.
class LivesRow extends StatelessWidget {
final int lives;
const LivesRow({super.key, required this.lives});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(AppConstants.startingLives, (index) {
return Icon(
index < lives ? Icons.favorite : Icons.favorite_border,
color: Colors.red,
size: 32,
);
}),
);
}
}

View File

@ -1,39 +0,0 @@
import 'package:flutter/material.dart';
/// Encart affichant le score courant et le meilleur score personnel.
class ScoreBoard extends StatelessWidget {
final int currentScore;
final int bestScore;
const ScoreBoard({super.key, required this.currentScore, required this.bestScore});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withAlpha(153),
border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
const Text(
"CURRENT SCORE",
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
),
Text(
"$currentScore",
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF3B6EE3)),
),
const Divider(height: 24),
Text(
"PERSONAL BEST: $bestScore",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
],
),
);
}
}

View File

@ -1,29 +0,0 @@
import 'package:flutter/material.dart';
/// Bandeau affichant le nombre de Pokémon découverts sur le total.
class PokedexCountBar extends StatelessWidget {
final int caught;
final int total;
const PokedexCountBar({super.key, required this.caught, required this.total});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 12.0),
decoration: const BoxDecoration(
color: Color(0xFFB0BEC5),
border: Border(bottom: BorderSide(color: Color(0xFF78909C), width: 2)),
),
child: Column(
children: [
Text(
'${caught.toString().padLeft(3, '0')} / $total',
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const Text('POKEMON DISCOVERED',
style: TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)),
],
),
);
}
}

View File

@ -1,23 +0,0 @@
import 'package:flutter/material.dart';
/// Barre de titre de la liste du Pokédex.
class PokedexListHeader extends StatelessWidget {
const PokedexListHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
color: const Color(0xFF90A4AE),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.menu, color: Colors.black87),
Text('LIST - NATIONAL',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
Icon(Icons.search, color: Colors.black87),
],
),
);
}
}

View File

@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
/// Effet rétro de scanlines superposé en fond. À placer directement dans un [Stack].
class ScanlineOverlay extends StatelessWidget {
const ScanlineOverlay({super.key});
@override
Widget build(BuildContext context) {
return Positioned.fill(
child: ListView.builder(
itemCount: 100,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Container(
height: 4,
margin: const EdgeInsets.only(bottom: 4),
color: Colors.black.withAlpha(2),
),
),
);
}
}

View File

@ -1,72 +0,0 @@
import 'package:flutter/material.dart';
import '../../providers/theme_provider.dart';
/// Sélecteur de palette de couleurs de l'application.
class PalettePicker extends StatelessWidget {
final int selectedIndex;
final void Function(int) onSelect;
const PalettePicker({super.key, required this.selectedIndex, required this.onSelect});
@override
Widget build(BuildContext context) {
return Column(
children: List.generate(appPalettes.length, (i) {
final p = appPalettes[i];
final isSelected = i == selectedIndex;
return GestureDetector(
onTap: () => onSelect(i),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? p.primary : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? p.primary : Colors.grey.shade300,
width: isSelected ? 3 : 1.5,
),
boxShadow: isSelected
? [BoxShadow(color: p.primary.withAlpha(80), blurRadius: 8, offset: const Offset(0, 3))]
: [],
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: p.primary,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
const SizedBox(width: 8),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: p.surface,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5),
),
),
const SizedBox(width: 16),
Text(
p.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : Colors.black87,
),
),
const Spacer(),
if (isSelected) const Icon(Icons.check_circle, color: Colors.white, size: 22),
],
),
),
);
}),
);
}
}

View File

@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
/// Titre de section avec une barre verticale colorée à gauche.
class SectionTitle extends StatelessWidget {
final String text;
final Color color;
const SectionTitle({super.key, required this.text, required this.color});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(width: 4, height: 20, color: color),
const SizedBox(width: 8),
Text(
text,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: color, letterSpacing: 2),
),
],
);
}
}

View File

@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
/// En-tête de la page Système.
class SystemHeader extends StatelessWidget {
final Color primaryColor;
const SystemHeader({super.key, required this.primaryColor});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: primaryColor,
border: Border(bottom: BorderSide(color: primaryColor.withAlpha(180), width: 3)),
),
child: const Text(
'SYSTÈME',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 4),
),
);
}
}

View File

@ -1,63 +0,0 @@
import 'package:flutter/material.dart';
/// Donnée d'une statistique affichée dans la grille.
class StatItem {
final String label;
final String value;
final IconData icon;
const StatItem({required this.label, required this.value, required this.icon});
}
/// Grille 2 colonnes de cartes de statistiques.
class StatsGrid extends StatelessWidget {
final Color primaryColor;
final List<StatItem> items;
const StatsGrid({super.key, required this.primaryColor, required this.items});
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.4,
children: items.map((item) => _StatCard(item: item, color: primaryColor)).toList(),
);
}
}
class _StatCard extends StatelessWidget {
final StatItem item;
final Color color;
const _StatCard({required this.item, required this.color});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withAlpha(80), width: 2),
boxShadow: [
BoxShadow(color: Colors.black.withAlpha(25), blurRadius: 4, offset: const Offset(0, 2)),
],
),
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(item.icon, color: color, size: 28),
const SizedBox(height: 6),
Text(item.value, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color)),
Text(
item.label,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
],
),
);
}
}