initial commit
This commit is contained in:
121
lib/main.dart
Normal file
121
lib/main.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'providers/transaction_provider.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
import 'screens/make_payment_screen.dart';
|
||||
import 'screens/history_screen.dart';
|
||||
import 'screens/profile_screen.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => TransactionProvider(),
|
||||
child: MaterialApp.router(
|
||||
title: 'QPay',
|
||||
theme: AppTheme.lightTheme,
|
||||
routerConfig: GoRouter(
|
||||
initialLocation: '/',
|
||||
routes: [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) {
|
||||
return ScaffoldWithNavBar(child: child);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/make-payment',
|
||||
builder: (context, state) => const MakePaymentScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/history',
|
||||
builder: (context, state) => const HistoryScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: (context, state) => const ProfileScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScaffoldWithNavBar extends StatelessWidget {
|
||||
const ScaffoldWithNavBar({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: (index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.go('/');
|
||||
break;
|
||||
case 1:
|
||||
context.go('/make-payment');
|
||||
break;
|
||||
case 2:
|
||||
context.go('/history');
|
||||
break;
|
||||
case 3:
|
||||
context.go('/profile');
|
||||
break;
|
||||
}
|
||||
},
|
||||
selectedIndex: _calculateSelectedIndex(context),
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.payment_outlined),
|
||||
selectedIcon: Icon(Icons.payment),
|
||||
label: 'Pay',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.history_outlined),
|
||||
selectedIcon: Icon(Icons.history),
|
||||
label: 'History',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.person_outline),
|
||||
selectedIcon: Icon(Icons.person),
|
||||
label: 'Profile',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _calculateSelectedIndex(BuildContext context) {
|
||||
final String location = GoRouterState.of(context).uri.path;
|
||||
if (location.startsWith('/make-payment')) return 1;
|
||||
if (location.startsWith('/history')) return 2;
|
||||
if (location.startsWith('/profile')) return 3;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
110
lib/providers/transaction_provider.dart
Normal file
110
lib/providers/transaction_provider.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Transaction {
|
||||
final String id;
|
||||
final String title;
|
||||
final double amount;
|
||||
final DateTime date;
|
||||
final String type;
|
||||
final String status;
|
||||
final IconData icon;
|
||||
|
||||
Transaction({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.amount,
|
||||
required this.date,
|
||||
required this.type,
|
||||
required this.status,
|
||||
required this.icon,
|
||||
});
|
||||
}
|
||||
|
||||
class TransactionProvider with ChangeNotifier {
|
||||
final List<Transaction> _transactions = [
|
||||
Transaction(
|
||||
id: '1',
|
||||
title: 'Electricity Bill',
|
||||
amount: -120.00,
|
||||
date: DateTime.now().subtract(const Duration(days: 0)),
|
||||
type: 'Bill',
|
||||
status: 'Completed',
|
||||
icon: Icons.electric_bolt,
|
||||
),
|
||||
Transaction(
|
||||
id: '2',
|
||||
title: 'Internet Bill',
|
||||
amount: -75.00,
|
||||
date: DateTime.now().subtract(const Duration(days: 1)),
|
||||
type: 'Bill',
|
||||
status: 'Completed',
|
||||
icon: Icons.wifi,
|
||||
),
|
||||
Transaction(
|
||||
id: '3',
|
||||
title: 'Mobile Bill',
|
||||
amount: -50.00,
|
||||
date: DateTime.now().subtract(const Duration(days: 2)),
|
||||
type: 'Bill',
|
||||
status: 'Completed',
|
||||
icon: Icons.phone_android,
|
||||
),
|
||||
Transaction(
|
||||
id: '4',
|
||||
title: 'Water Bill',
|
||||
amount: -45.00,
|
||||
date: DateTime.now().subtract(const Duration(days: 3)),
|
||||
type: 'Bill',
|
||||
status: 'Completed',
|
||||
icon: Icons.water_drop,
|
||||
),
|
||||
Transaction(
|
||||
id: '5',
|
||||
title: 'Gas Bill',
|
||||
amount: -65.00,
|
||||
date: DateTime.now().subtract(const Duration(days: 4)),
|
||||
type: 'Bill',
|
||||
status: 'Completed',
|
||||
icon: Icons.local_fire_department,
|
||||
),
|
||||
];
|
||||
|
||||
double _balance = 2450.00;
|
||||
|
||||
List<Transaction> get transactions => _transactions;
|
||||
double get balance => _balance;
|
||||
|
||||
void addTransaction(Transaction transaction) {
|
||||
_transactions.insert(0, transaction);
|
||||
_balance += transaction.amount;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeTransaction(String id) {
|
||||
final transaction = _transactions.firstWhere((t) => t.id == id);
|
||||
_transactions.removeWhere((t) => t.id == id);
|
||||
_balance -= transaction.amount;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Transaction> getTransactionsByType(String type) {
|
||||
if (type == 'All') return _transactions;
|
||||
return _transactions.where((t) => t.type == type).toList();
|
||||
}
|
||||
|
||||
String getFormattedDate(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
return 'Today';
|
||||
} else if (difference.inDays == 1) {
|
||||
return 'Yesterday';
|
||||
} else if (difference.inDays < 7) {
|
||||
return '${difference.inDays} days ago';
|
||||
} else {
|
||||
return '${date.day}/${date.month}/${date.year}';
|
||||
}
|
||||
}
|
||||
}
|
||||
349
lib/screens/history_screen.dart
Normal file
349
lib/screens/history_screen.dart
Normal file
@@ -0,0 +1,349 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HistoryScreen extends StatefulWidget {
|
||||
const HistoryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HistoryScreen> createState() => _HistoryScreenState();
|
||||
}
|
||||
|
||||
class _HistoryScreenState extends State<HistoryScreen> {
|
||||
final List<Map<String, dynamic>> _transactions = [
|
||||
{
|
||||
'title': 'Electricity Bill',
|
||||
'amount': -120.00,
|
||||
'date': 'Today',
|
||||
'icon': Icons.electric_bolt,
|
||||
'status': 'Completed',
|
||||
},
|
||||
{
|
||||
'title': 'Internet Bill',
|
||||
'amount': -75.00,
|
||||
'date': 'Yesterday',
|
||||
'icon': Icons.wifi,
|
||||
'status': 'Completed',
|
||||
},
|
||||
{
|
||||
'title': 'Mobile Bill',
|
||||
'amount': -50.00,
|
||||
'date': '2 days ago',
|
||||
'icon': Icons.phone_android,
|
||||
'status': 'Completed',
|
||||
},
|
||||
{
|
||||
'title': 'Water Bill',
|
||||
'amount': -45.00,
|
||||
'date': '3 days ago',
|
||||
'icon': Icons.water_drop,
|
||||
'status': 'Completed',
|
||||
},
|
||||
{
|
||||
'title': 'Gas Bill',
|
||||
'amount': -65.00,
|
||||
'date': '4 days ago',
|
||||
'icon': Icons.local_fire_department,
|
||||
'status': 'Completed',
|
||||
},
|
||||
];
|
||||
|
||||
String _selectedFilter = 'All';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Transaction History'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
onPressed: _showFilterDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildFilterChips(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _transactions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final transaction = _transactions[index];
|
||||
return _buildTransactionCard(transaction, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChips() {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFilterChip('All'),
|
||||
_buildFilterChip('Bills'),
|
||||
_buildFilterChip('Payments'),
|
||||
_buildFilterChip('Transfers'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChip(String label) {
|
||||
final isSelected = _selectedFilter == label;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(label),
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_selectedFilter = label;
|
||||
});
|
||||
},
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
selectedColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
|
||||
checkmarkColor: Theme.of(context).colorScheme.primary,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransactionCard(Map<String, dynamic> transaction, int index) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: InkWell(
|
||||
onTap: () => _showTransactionDetails(transaction),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
transaction['icon'] as IconData,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
transaction['title'] as String,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
transaction['date'] as String,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'\$${transaction['amount'].abs().toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
color: transaction['amount'] < 0
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Colors.green,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
transaction['status'] as String,
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFilterDialog() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Filter Transactions',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildFilterOption('All Transactions'),
|
||||
_buildFilterOption('Bills Only'),
|
||||
_buildFilterOption('Payments Only'),
|
||||
_buildFilterOption('Transfers Only'),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterOption(String option) {
|
||||
return ListTile(
|
||||
title: Text(option),
|
||||
trailing: _selectedFilter == option
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedFilter = option;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showTransactionDetails(Map<String, dynamic> transaction) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
transaction['icon'] as IconData,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
transaction['title'] as String,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
transaction['date'] as String,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildDetailRow('Amount', '\$${transaction['amount'].abs().toStringAsFixed(2)}'),
|
||||
_buildDetailRow('Status', transaction['status'] as String),
|
||||
_buildDetailRow('Transaction ID', '#${DateTime.now().millisecondsSinceEpoch}'),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Close'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
338
lib/screens/home_screen.dart
Normal file
338
lib/screens/home_screen.dart
Normal file
@@ -0,0 +1,338 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _slideAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
);
|
||||
_slideAnimation = Tween<double>(begin: -1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic),
|
||||
);
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
title: const Text('QPay'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Welcome back,',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'John Doe',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0),
|
||||
end: const Offset(0, 1),
|
||||
).animate(_slideAnimation),
|
||||
child: _buildBalanceCard(context),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Quick Actions',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildQuickActions(context),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Recent Transactions',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildRecentTransactions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBalanceCard(BuildContext context) {
|
||||
return Card(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Available Balance',
|
||||
style: TextStyle(color: Colors.black87, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'\$0.00',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildActionButton(
|
||||
context,
|
||||
'Add',
|
||||
Icons.add_circle_outline,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(
|
||||
BuildContext context,
|
||||
String label,
|
||||
IconData icon,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActions(BuildContext context) {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 2,
|
||||
children: [
|
||||
_buildAnimatedQuickActionItem(
|
||||
context,
|
||||
'Electricity',
|
||||
Icons.electric_bolt_outlined,
|
||||
() {},
|
||||
0,
|
||||
),
|
||||
_buildAnimatedQuickActionItem(
|
||||
context,
|
||||
'Airtime',
|
||||
Icons.phone_android_outlined,
|
||||
() {},
|
||||
1,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimatedQuickActionItem(
|
||||
BuildContext context,
|
||||
String label,
|
||||
IconData icon,
|
||||
VoidCallback onTap,
|
||||
int index,
|
||||
) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(-1, 0),
|
||||
end: const Offset(0, 0),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Interval(0.4 + (index * 0.1), 0.8 + (index * 0.1)),
|
||||
),
|
||||
),
|
||||
child: _buildQuickActionItem(context, label, icon, onTap),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActionItem(
|
||||
BuildContext context,
|
||||
String label,
|
||||
IconData icon,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecentTransactions() {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: 3,
|
||||
itemBuilder: (context, index) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(-1, 0),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Interval(
|
||||
0.5 + (index * 0.1),
|
||||
0.7 + (index * 0.1),
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.tertiary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
index == 0
|
||||
? Icons.phone_android
|
||||
: index == 1
|
||||
? Icons.wifi
|
||||
: Icons.receipt_long,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
index == 0
|
||||
? 'Mobile Bill'
|
||||
: index == 1
|
||||
? 'Internet Bill'
|
||||
: 'Electricity Bill',
|
||||
),
|
||||
subtitle: Text(
|
||||
index == 0
|
||||
? 'Yesterday'
|
||||
: index == 1
|
||||
? '2 days ago'
|
||||
: '3 days ago',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
trailing: Text(
|
||||
index == 0
|
||||
? '-\$50.00'
|
||||
: index == 1
|
||||
? '-\$75.00'
|
||||
: '-\$120.00',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
229
lib/screens/make_payment_screen.dart
Normal file
229
lib/screens/make_payment_screen.dart
Normal file
@@ -0,0 +1,229 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MakePaymentScreen extends StatefulWidget {
|
||||
const MakePaymentScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MakePaymentScreen> createState() => _MakePaymentScreenState();
|
||||
}
|
||||
|
||||
class _MakePaymentScreenState extends State<MakePaymentScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _amountController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
String _selectedBillType = 'Electricity';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 0.1),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_amountController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Make Payment'),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildBillTypeSelector(),
|
||||
const SizedBox(height: 24),
|
||||
_buildAmountField(),
|
||||
const SizedBox(height: 24),
|
||||
_buildDescriptionField(),
|
||||
const SizedBox(height: 32),
|
||||
_buildPayButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBillTypeSelector() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Select Bill Type',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: _selectedBillType,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
items: [
|
||||
'Electricity',
|
||||
'Water',
|
||||
'Internet',
|
||||
'Mobile',
|
||||
'Gas',
|
||||
].map((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? newValue) {
|
||||
if (newValue != null) {
|
||||
setState(() {
|
||||
_selectedBillType = newValue;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountField() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Amount',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
prefixIcon: Icon(Icons.attach_money),
|
||||
hintText: 'Enter amount',
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter an amount';
|
||||
}
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Please enter a valid amount';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionField() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Description',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Enter description (optional)',
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPayButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// TODO: Implement payment logic
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Payment processed successfully!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text(
|
||||
'Pay Now',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
186
lib/screens/profile_screen.dart
Normal file
186
lib/screens/profile_screen.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProfileScreen extends StatefulWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ProfileScreen> createState() => _ProfileScreenState();
|
||||
}
|
||||
|
||||
class _ProfileScreenState extends State<ProfileScreen> {
|
||||
final List<Map<String, dynamic>> _menuItems = [
|
||||
{
|
||||
'title': 'Personal Information',
|
||||
'icon': Icons.person_outline,
|
||||
'onTap': () {},
|
||||
},
|
||||
{
|
||||
'title': 'Payment Methods',
|
||||
'icon': Icons.credit_card_outlined,
|
||||
'onTap': () {},
|
||||
},
|
||||
{
|
||||
'title': 'Security',
|
||||
'icon': Icons.lock_outline,
|
||||
'onTap': () {},
|
||||
},
|
||||
{
|
||||
'title': 'Notifications',
|
||||
'icon': Icons.notifications_outlined,
|
||||
'onTap': () {},
|
||||
},
|
||||
{
|
||||
'title': 'Help & Support',
|
||||
'icon': Icons.help_outline,
|
||||
'onTap': () {},
|
||||
},
|
||||
{
|
||||
'title': 'About',
|
||||
'icon': Icons.info_outline,
|
||||
'onTap': () {},
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 200,
|
||||
pinned: true,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
const CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: Colors.white,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 50,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'John Doe',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'john.doe@example.com',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Account Settings',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._menuItems.map((item) => _buildMenuItem(item)),
|
||||
const SizedBox(height: 24),
|
||||
_buildLogoutButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuItem(Map<String, dynamic> item) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
item['icon'] as IconData,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
title: Text(
|
||||
item['title'] as String,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: item['onTap'] as VoidCallback,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogoutButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Logout'),
|
||||
content: const Text('Are you sure you want to logout?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implement logout logic
|
||||
},
|
||||
child: const Text('Logout'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text(
|
||||
'Logout',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
74
lib/theme/app_theme.dart
Normal file
74
lib/theme/app_theme.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
static const _primaryColor = Color(0xFFD4AF37); // Gold
|
||||
static const _secondaryColor = Color(0xFF8B0000); // Dark Red
|
||||
static const _tertiaryColor = Color(0xFF000000); // Black
|
||||
static const _backgroundColor = Color(0xFFFFFBFE);
|
||||
static const _surfaceColor = Color(0xFFFFFBFE);
|
||||
static const _errorColor = Color(0xFFB3261E);
|
||||
|
||||
static final ThemeData lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: _primaryColor,
|
||||
secondary: _secondaryColor,
|
||||
tertiary: _tertiaryColor,
|
||||
background: _backgroundColor,
|
||||
surface: _surfaceColor,
|
||||
error: _errorColor,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: _backgroundColor,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: _primaryColor),
|
||||
titleTextStyle: TextStyle(
|
||||
color: _primaryColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: _surfaceColor,
|
||||
indicatorColor: _primaryColor.withOpacity(0.1),
|
||||
labelTextStyle: MaterialStateProperty.all(
|
||||
const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
backgroundColor: _primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: _surfaceColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: _primaryColor),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: _errorColor),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user