feat: add tournoment feature

This commit is contained in:
2026-07-07 15:18:55 +03:30
parent 5b0a66bb54
commit 5424695f25
10 changed files with 1349 additions and 229 deletions
@@ -0,0 +1,48 @@
import '../../../../../core/locator/locator.dart';
import '../../../../../core/network/api_provider_imp.dart';
import '../../../domain/entities/tournament.dart';
/// دادهٔ تورنومنت‌ها از backend (فهرست، رده‌بندی، ثبت‌نام).
class TournamentApiProvider {
ApiProviderImp get _api => locator<ApiProviderImp>();
Future<List<Tournament>> getTournaments() async {
final res = await _api.get('/tournaments');
if (res.statusCode != 200) {
throw Exception('tournaments: ${res.statusCode}');
}
final list = (res.data['tournaments'] as List?) ?? const [];
return list
.map((e) => Tournament.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
}
Future<List<TournamentStanding>> getStandings(int id) async {
final res = await _api.get('/tournaments/standings', query: {'id': '$id'});
if (res.statusCode != 200) {
throw Exception('standings: ${res.statusCode}');
}
final list = (res.data['standings'] as List?) ?? const [];
return list
.map((e) =>
TournamentStanding.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
}
/// ثبت‌نام؛ null یعنی موفق، وگرنه پیامِ خطای فارسی.
Future<String?> join(int id) async {
final res = await _api.post('/tournaments/join', body: {'id': id});
if (res.statusCode == 200) return null;
final msg = (res.data is Map) ? res.data['message']?.toString() : null;
switch (res.statusCode) {
case 402:
return 'سکهٔ کافی برای ثبت‌نام ندارید';
case 409:
return msg ?? 'امکان ثبت‌نام نیست';
case 404:
return 'تورنومنت یافت نشد';
default:
return msg ?? 'ثبت‌نام ناموفق بود';
}
}
}
@@ -0,0 +1,99 @@
/// یک تورنومنتِ امتیازی (از GET /api/tournaments).
class Tournament {
final int id;
final String title;
final String description;
final int entryFee;
final List<int> prizes;
final int prizePool;
final String status; // upcoming | active | ended
final String startsAt;
final String endsAt;
final String startsLocal; // به وقتِ تهران (نمایشی)
final String endsLocal;
final int players;
final bool joined;
final int myPoints;
final int myRank; // ۰ یعنی رتبه‌ای ندارد
const Tournament({
required this.id,
required this.title,
required this.description,
required this.entryFee,
required this.prizes,
required this.prizePool,
required this.status,
required this.startsAt,
required this.endsAt,
required this.startsLocal,
required this.endsLocal,
required this.players,
required this.joined,
required this.myPoints,
required this.myRank,
});
bool get isActive => status == 'active';
bool get isUpcoming => status == 'upcoming';
bool get isEnded => status == 'ended';
bool get canJoin => !joined && !isEnded;
factory Tournament.fromJson(Map<String, dynamic> j) => Tournament(
id: (j['id'] ?? 0) as int,
title: (j['title'] ?? '') as String,
description: (j['description'] ?? '') as String,
entryFee: (j['entry_fee'] ?? 0) as int,
prizes: ((j['prizes'] as List?) ?? const [])
.map((e) => (e ?? 0) as int)
.toList(),
prizePool: (j['prize_pool'] ?? 0) as int,
status: (j['status'] ?? 'ended') as String,
startsAt: (j['starts_at'] ?? '') as String,
endsAt: (j['ends_at'] ?? '') as String,
startsLocal: (j['starts_local'] ?? '') as String,
endsLocal: (j['ends_local'] ?? '') as String,
players: (j['players'] ?? 0) as int,
joined: (j['joined'] ?? false) as bool,
myPoints: (j['my_points'] ?? 0) as int,
myRank: (j['my_rank'] ?? 0) as int,
);
}
/// یک ردیفِ جدولِ رده‌بندیِ تورنومنت.
class TournamentStanding {
final int rank;
final int userId;
final String name;
final String avatar;
final int points;
final int wins;
final int games;
final int prize;
const TournamentStanding({
required this.rank,
required this.userId,
required this.name,
required this.avatar,
required this.points,
required this.wins,
required this.games,
required this.prize,
});
/// seedِ آواتار: آواتارِ انتخابی، وگرنه نام.
String get avatarSeed => avatar.isNotEmpty ? avatar : name;
factory TournamentStanding.fromJson(Map<String, dynamic> j) =>
TournamentStanding(
rank: (j['rank'] ?? 0) as int,
userId: (j['user_id'] ?? 0) as int,
name: (j['name'] ?? '') as String,
avatar: (j['avatar'] ?? '') as String,
points: (j['points'] ?? 0) as int,
wins: (j['wins'] ?? 0) as int,
games: (j['games'] ?? 0) as int,
prize: (j['prize'] ?? 0) as int,
);
}
@@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:random_avatar/random_avatar.dart';
import '../../../../core/locator/locator.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/game_ui.dart';
import '../../data/data_source/remote/tournament_api_provider.dart';
import '../../domain/entities/tournament.dart';
/// جدولِ رده‌بندیِ یک تورنومنت + خلاصه‌ی جوایز.
class TournamentStandingsScreen extends StatefulWidget {
final Tournament tournament;
const TournamentStandingsScreen({super.key, required this.tournament});
@override
State<TournamentStandingsScreen> createState() =>
_TournamentStandingsScreenState();
}
class _TournamentStandingsScreenState extends State<TournamentStandingsScreen> {
final _api = locator<TournamentApiProvider>();
late Future<List<TournamentStanding>> _future;
@override
void initState() {
super.initState();
_future = _api.getStandings(widget.tournament.id);
}
@override
Widget build(BuildContext context) {
final t = widget.tournament;
return Scaffold(
body: GameBackground(
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(children: [
const AppBackButton(),
const Spacer(),
Flexible(child: GlowText(t.title, size: 20)),
const Spacer(),
const SizedBox(width: 48),
]),
),
_prizeBar(t),
Expanded(
child: FutureBuilder<List<TournamentStanding>>(
future: _future,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(
child:
CircularProgressIndicator(color: AppColors.gold));
}
final rows = snap.data ?? const <TournamentStanding>[];
if (rows.isEmpty) {
return const Center(
child: Text('هنوز کسی امتیازی نگرفته است',
style: TextStyle(color: Colors.white54)));
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(14, 8, 14, 24),
itemCount: rows.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (_, i) => _row(rows[i]),
);
},
),
),
// اگر تورنومنت فعال و کاربر ثبت‌نام کرده ⇒ راهِ مستقیم به بازی برای
// کسبِ امتیاز (تا کاربر رابطه‌ی «بازی ⇒ امتیازِ تورنومنت» را حس کند).
if (t.isActive && t.joined)
Padding(
padding: const EdgeInsets.fromLTRB(14, 4, 14, 12),
child: GameButton(
label: 'بازی کن و امتیاز بگیر',
icon: Icons.sports_esports,
width: double.infinity,
colors: const [AppColors.gold, AppColors.goldDark],
onTap: () {
Navigator.of(context).pop(); // به لیستِ تورنومنت‌ها
context.push('/game/tiers');
},
),
),
],
),
),
),
);
}
Widget _prizeBar(Tournament t) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 14),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppColors.gold.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.goldFaint),
),
child: Row(children: [
const Icon(Icons.emoji_events, color: AppColors.gold, size: 20),
const SizedBox(width: 8),
Text('جایزه کل: ${t.prizePool} سکه',
style: const TextStyle(
color: AppColors.gold, fontWeight: FontWeight.bold)),
const Spacer(),
if (t.prizes.isNotEmpty)
Text(
t.prizes
.asMap()
.entries
.map((e) => '${e.key + 1}: ${e.value}')
.join(''),
style: const TextStyle(color: Colors.white60, fontSize: 11),
),
]),
);
}
Widget _row(TournamentStanding s) {
final medal = s.rank <= 3;
final medalColor = switch (s.rank) {
1 => const Color(0xFFFFD700),
2 => const Color(0xFFC0C0C0),
3 => const Color(0xFFCD7F32),
_ => Colors.transparent,
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.lapisHi, AppColors.lapisLo],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: medal ? medalColor : AppColors.goldFaint,
width: medal ? 1.6 : 1,
),
),
child: Row(children: [
SizedBox(
width: 30,
child: medal
? Icon(Icons.emoji_events, color: medalColor, size: 22)
: Text('${s.rank}',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white70, fontWeight: FontWeight.bold)),
),
const SizedBox(width: 6),
ClipOval(
child: SizedBox(
width: 38,
height: 38,
child: RandomAvatar(s.avatarSeed),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
Text('${s.wins} برد • ${s.games} بازی',
style: const TextStyle(color: Colors.white54, fontSize: 11)),
],
),
),
if (s.prize > 0) ...[
Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.monetization_on, color: AppColors.gold, size: 15),
const SizedBox(width: 3),
Text('${s.prize}',
style: const TextStyle(
color: AppColors.gold,
fontSize: 12,
fontWeight: FontWeight.bold)),
]),
const SizedBox(width: 10),
],
Column(children: [
Text('${s.points}',
style: const TextStyle(
color: AppColors.gold,
fontSize: 16,
fontWeight: FontWeight.bold)),
const Text('امتیاز',
style: TextStyle(color: Colors.white54, fontSize: 10)),
]),
]),
);
}
}
@@ -0,0 +1,392 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/locator/locator.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/game_ui.dart';
import '../../../wallet/presentation/bloc/wallet_bloc.dart';
import '../../../wallet/presentation/bloc/wallet_event.dart';
import '../../../wallet/presentation/bloc/wallet_state.dart';
import '../../../wallet/presentation/bloc/wallet_status.dart';
import '../../data/data_source/remote/tournament_api_provider.dart';
import '../../domain/entities/tournament.dart';
import '../widgets/tournament_guide.dart';
import 'tournament_standings_screen.dart';
/// فهرستِ تورنومنت‌ها: جایزه، ورودی، وضعیت و دکمه‌ی ثبت‌نام / جدول.
class TournamentsScreen extends StatefulWidget {
const TournamentsScreen({super.key});
@override
State<TournamentsScreen> createState() => _TournamentsScreenState();
}
class _TournamentsScreenState extends State<TournamentsScreen> {
final _api = locator<TournamentApiProvider>();
late Future<List<Tournament>> _future;
int? _busyId;
@override
void initState() {
super.initState();
_future = _api.getTournaments();
}
void _reload() => setState(() {
_future = _api.getTournaments();
});
Future<void> _join(Tournament t) async {
setState(() => _busyId = t.id);
final err = await _api.join(t.id);
if (!mounted) return;
setState(() => _busyId = null);
if (err == null) {
context.read<WalletBloc>().add(LoadWalletEvent());
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('ثبت‌نام انجام شد ✓')),
);
_reload();
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err)));
}
}
void _openStandings(Tournament t) {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => TournamentStandingsScreen(tournament: t),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GameBackground(
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(children: [
const AppBackButton(),
const Spacer(),
const GlowText('تورنومنت‌ها', size: 24),
const Spacer(),
IconButton(
tooltip: 'راهنما',
onPressed: () => TournamentGuide.show(context),
icon: const Icon(Icons.help_outline, color: AppColors.gold),
),
BlocBuilder<WalletBloc, WalletBlocState>(
builder: (context, s) {
final w = s.walletStatus is WalletLoaded
? (s.walletStatus as WalletLoaded).wallet
: null;
return StatChip(
icon: Icons.monetization_on,
value: '${w?.coins ?? 0}',
);
},
),
]),
),
Expanded(
child: RefreshIndicator(
color: AppColors.gold,
onRefresh: () async => _reload(),
child: FutureBuilder<List<Tournament>>(
future: _future,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(
child:
CircularProgressIndicator(color: AppColors.gold));
}
if (snap.hasError) {
return _retry();
}
final items = snap.data ?? const <Tournament>[];
if (items.isEmpty) {
return _empty();
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 24),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(height: 14),
itemBuilder: (_, i) => _card(items[i]),
);
},
),
),
),
],
),
),
),
);
}
Widget _retry() => ListView(children: [
const SizedBox(height: 120),
const Center(
child: Text('خطا در دریافت تورنومنت‌ها',
style: TextStyle(color: Colors.white70))),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: _reload,
child: const Text('تلاش مجدد',
style: TextStyle(color: AppColors.gold)),
),
),
]);
Widget _empty() => ListView(children: const [
SizedBox(height: 140),
Icon(Icons.emoji_events_outlined, color: Colors.white24, size: 60),
SizedBox(height: 12),
Center(
child: Text('در حال حاضر تورنومنتی برگزار نمی‌شود',
style: TextStyle(color: Colors.white54))),
]);
Widget _card(Tournament t) {
final busy = _busyId == t.id;
return GestureDetector(
onTap: () => _openStandings(t),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF3A2A6E), Color(0xFF15193F), AppColors.lapisInk],
),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: AppColors.gold, width: 1.6),
boxShadow: [
BoxShadow(
color: AppColors.gold.withValues(alpha: 0.18), blurRadius: 12),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Container(
padding: const EdgeInsets.all(9),
decoration: const BoxDecoration(
gradient:
LinearGradient(colors: [AppColors.gold, AppColors.goldDark]),
shape: BoxShape.circle,
),
child: const Icon(Icons.emoji_events,
color: AppColors.lapisInk, size: 22),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.gold,
fontSize: 18,
fontWeight: FontWeight.bold)),
if (t.description.isNotEmpty)
Text(t.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white60, fontSize: 12)),
],
),
),
_statusBadge(t.status),
]),
const SizedBox(height: 12),
Row(children: [
_info(Icons.emoji_events, 'جایزه کل', '${t.prizePool}',
gold: true),
const SizedBox(width: 8),
_info(Icons.login, 'ورودی',
t.entryFee == 0 ? 'رایگان' : '${t.entryFee}'),
const SizedBox(width: 8),
_info(Icons.people, 'شرکت‌کننده', '${t.players}'),
]),
if (t.startsLocal.isNotEmpty) ...[
const SizedBox(height: 8),
Row(children: [
const Icon(Icons.schedule, color: Colors.white38, size: 13),
const SizedBox(width: 5),
Text(
t.isUpcoming
? 'شروع: ${t.startsLocal}'
: 'پایان: ${t.endsLocal}',
style: const TextStyle(color: Colors.white54, fontSize: 11),
),
]),
],
const SizedBox(height: 12),
_action(t, busy),
],
),
),
);
}
Widget _statusBadge(String status) {
late Color c;
late String label;
switch (status) {
case 'active':
c = AppColors.success;
label = 'در حال برگزاری';
break;
case 'upcoming':
c = const Color(0xFFE9952F);
label = 'به‌زودی';
break;
default:
c = Colors.white38;
label = 'پایان‌یافته';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
decoration: BoxDecoration(
color: c.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: c),
),
child: Text(label,
style:
TextStyle(color: c, fontSize: 11, fontWeight: FontWeight.bold)),
);
}
Widget _info(IconData icon, String label, String value, {bool gold = false}) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
),
child: Column(children: [
Icon(icon, size: 16, color: gold ? AppColors.gold : Colors.white70),
const SizedBox(height: 3),
Text(value,
style: TextStyle(
color: gold ? AppColors.gold : Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold)),
Text(label,
style: const TextStyle(color: Colors.white54, fontSize: 10)),
]),
),
);
}
Widget _action(Tournament t, bool busy) {
if (t.joined) {
return Column(children: [
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.success),
),
child: Text(
t.myRank > 0
? 'رتبه شما: ${t.myRank} • امتیاز: ${t.myPoints}'
: 'ثبت‌نام شده • امتیاز: ${t.myPoints}',
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13),
),
),
const SizedBox(height: 8),
Row(children: [
if (t.isActive)
Expanded(
child: GameButton(
label: 'بازی کن و امتیاز بگیر',
icon: Icons.sports_esports,
colors: const [AppColors.gold, AppColors.goldDark],
onTap: () async {
await context.push('/game/tiers');
if (context.mounted) _reload();
},
),
)
else
Expanded(
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
side: const BorderSide(color: AppColors.gold),
padding: const EdgeInsets.symmetric(vertical: 10),
),
onPressed: () => _openStandings(t),
icon: const Icon(Icons.leaderboard,
color: AppColors.gold, size: 18),
label: const Text('جدول نتایج',
style: TextStyle(color: AppColors.gold)),
),
),
const SizedBox(width: 8),
_tableButton(t),
]),
]);
}
if (t.canJoin) {
return Row(children: [
Expanded(
child: GameButton(
label: busy
? '...'
: (t.entryFee == 0
? 'ثبت‌نام رایگان'
: 'ثبت‌نام (${t.entryFee} سکه)'),
icon: Icons.how_to_reg,
colors: const [AppColors.success, AppColors.successDark],
onTap: busy ? null : () => _join(t),
),
),
const SizedBox(width: 8),
_tableButton(t),
]);
}
// پایان‌یافته و ثبت‌نام‌نشده ⇒ فقط جدول.
return SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
side: const BorderSide(color: AppColors.gold),
padding: const EdgeInsets.symmetric(vertical: 10),
),
onPressed: () => _openStandings(t),
icon: const Icon(Icons.leaderboard, color: AppColors.gold, size: 18),
label: const Text('جدول نتایج',
style: TextStyle(color: AppColors.gold)),
),
);
}
Widget _tableButton(Tournament t) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.gold),
),
child: IconButton(
tooltip: 'جدول',
onPressed: () => _openStandings(t),
icon: const Icon(Icons.leaderboard, color: AppColors.gold),
),
);
}
@@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart';
/// راهنمای تورنومنت: توضیحِ گام‌به‌گامِ نحوه‌ی کار (ثبت‌نام، کسبِ امتیاز، جایزه).
class TournamentGuide {
static const _steps = <(IconData, String, String)>[
(
Icons.how_to_reg,
'۱. ثبت‌نام کن',
'با پرداختِ ورودی (سکه) در تورنومنت ثبت‌نام کن. ثبت‌نام تا پیش از پایانِ '
'تورنومنت باز است.',
),
(
Icons.sports_esports,
'۲. بازی کن و امتیاز بگیر',
'در بازه‌ی زمانیِ تورنومنت، عادی بازی کن. هر بازی که ببری ۱۰۰ امتیاز و هر '
'بازی که شرکت کنی ۲۵ امتیاز می‌گیری. هرچه بیشتر ببری، امتیازت بیشتر می‌شود.',
),
(
Icons.leaderboard,
'۳. در جدول بالا برو',
'امتیازِ همه در جدولِ رده‌بندی ثبت می‌شود؛ بیشترین امتیاز بالاتر می‌ایستد. '
'در تساوی، تعدادِ بردِ بیشتر و ثبت‌نامِ زودتر ملاک است.',
),
(
Icons.emoji_events,
'۴. جایزه بگیر',
'وقتی تورنومنت تمام شد، نفراتِ برتر به‌ترتیبِ رتبه جایزه‌ی سکه می‌گیرند. '
'جوایز به‌صورتِ خودکار به کیف‌پولت اضافه می‌شود.',
),
];
static Future<void> show(BuildContext context) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const _GuideSheet(),
);
}
}
class _GuideSheet extends StatelessWidget {
const _GuideSheet();
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: DraggableScrollableSheet(
initialChildSize: 0.72,
minChildSize: 0.4,
maxChildSize: 0.92,
expand: false,
builder:
(context, controller) => Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.lapisMid, AppColors.lapisLo],
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(24),
),
border: Border.all(color: AppColors.gold, width: 1.6),
),
child: ListView(
controller: controller,
padding: const EdgeInsets.fromLTRB(18, 12, 18, 28),
children: [
Center(
child: Container(
width: 44,
height: 4,
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.emoji_events, color: AppColors.gold, size: 24),
SizedBox(width: 8),
Text(
'راهنمای تورنومنت',
style: TextStyle(
color: AppColors.gold,
fontSize: 19,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 6),
const Text(
'تورنومنتِ امتیازی؛ بیشتر ببر، بالاتر بایست، جایزه بگیر!',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white60, fontSize: 12.5),
),
const SizedBox(height: 18),
for (final (icon, title, body) in TournamentGuide._steps)
_stepTile(icon, title, body),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.gold.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.goldFaint),
),
child: const Row(
children: [
Icon(
Icons.lightbulb_outline,
color: AppColors.gold,
size: 18,
),
SizedBox(width: 8),
Expanded(
child: Text(
'نکته: می‌توانی در هر لحظه ثبت‌نام کنی، اما هرچه زودتر شروع '
'کنی فرصتِ بیشتری برای کسبِ امتیاز داری.',
style: TextStyle(
color: Colors.white,
fontSize: 12.5,
height: 1.7,
),
),
),
],
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.gold,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
onPressed: () => Navigator.pop(context),
child: const Text(
'فهمیدم',
style: TextStyle(
color: AppColors.lapisInk,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
),
],
),
),
),
);
}
Widget _stepTile(IconData icon, String title, String body) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(9),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.gold, AppColors.goldDark],
),
shape: BoxShape.circle,
),
child: Icon(icon, color: AppColors.lapisInk, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: AppColors.gold,
fontSize: 14.5,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
body,
style: const TextStyle(
color: Colors.white,
fontSize: 12.5,
height: 1.8,
),
),
],
),
),
],
),
);
}
}