Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@ flutter run

## 📱 Screenshots

<p align="center">
Add screenshots 👀
### Main Screen
![Main Screen](./lib/screenshots/main%20screen.png)

### Add Expense Screen
![Add Expense Screen](./lib/screenshots/add%20expense.png)

### Splash Screen
![Splash Screen](./lib/screenshots/splash%20screen.png)

</p>

## Custom App Icon 🎨
**-** Icons are located in:
Expand Down
29 changes: 24 additions & 5 deletions lib/models/expense.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ const categoryIcons = {

class Expense {
Expense(
{
// required this.id,
required this.title,
{required this.title,
required this.amount,
required this.date,
required this.category})
Expand All @@ -39,6 +37,28 @@ class Expense {
String get formattedDate {
return formatter.format(date);
}

Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'amount': amount,
'date': date.toIso8601String(),
'category': category.toString(),
};
}

factory Expense.fromMap(Map<String, dynamic> map) {
return Expense(
title: map['title'],
amount: map['amount'],
date: DateTime.parse(map['date']),
category: Category.values.firstWhere(
(e) => e.toString() == map['category'],
orElse: () => Category.leisure,
),
);
}
}

class ExpenseBucket {
Expand All @@ -48,7 +68,6 @@ class ExpenseBucket {
: expenses = allExpenses
.where((expense) => expense.category == category)
.toList();
// to filter all expenses according to category

final Category category;
final List<Expense> expenses;
Expand All @@ -62,4 +81,4 @@ class ExpenseBucket {

return sum;
}
}
}
83 changes: 57 additions & 26 deletions lib/widgets/expenses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import 'package:expense_tracker/widgets/new_expense.dart';
import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';
import 'package:expense_tracker/widgets/expenses_list/expenses_list.dart';
import 'chart/chart.dart';
import 'package:expense_tracker/widgets/chart/chart.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class Expenses extends StatefulWidget {
const Expenses({
Expand All @@ -16,28 +18,37 @@ class Expenses extends StatefulWidget {
}

class _ExpensesState extends State<Expenses> {
// Issue 1: Hardcoded initial expenses that don't reflect real usage
final List<Expense> _registeredExpenses = [
Expense(
title: 'Flutter course',
amount: 19.99,
date: DateTime.now(),
category: Category.work,
),
Expense(
title: 'Movie night',
amount: 5,
date: DateTime.now(),
category: Category.leisure,
),
];
final List<Expense> _registeredExpenses = [];

@override
void initState() {
super.initState();
_loadExpenses();
}

Future<void> _loadExpenses() async {
final prefs = await SharedPreferences.getInstance();
final expenseList = prefs.getStringList('expenses') ?? [];
setState(() {
_registeredExpenses.clear();
for (final expenseString in expenseList) {
final expenseMap = jsonDecode(expenseString) as Map<String, dynamic>;
_registeredExpenses.add(Expense.fromMap(expenseMap));
}
});
}

Future<void> _saveExpenses() async {
final prefs = await SharedPreferences.getInstance();
final expenseList = _registeredExpenses.map((expense) => jsonEncode(expense.toMap())).toList();
await prefs.setStringList('expenses', expenseList);
}

// Issue 2: No validation for duplicate expenses
void _addExpense(Expense expense) {
setState(() {
_registeredExpenses.add(expense);
});
// Issue 3: Fixed duration for all notifications
_saveExpenses();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
duration: const Duration(seconds: 3),
content: const Text('Expense Added!'),
Expand All @@ -54,12 +65,12 @@ class _ExpensesState extends State<Expenses> {
setState(() {
_registeredExpenses.remove(expense);
});
_saveExpenses();
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
duration: const Duration(seconds: 3),
content: const Text('Expense deleted.'),
behavior: SnackBarBehavior
.floating, // makes it float instead of stick to bottom
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
Expand All @@ -70,9 +81,18 @@ class _ExpensesState extends State<Expenses> {
setState(() {
_registeredExpenses.insert(expenseIndex, expense);
});
_saveExpenses();
})));
}

void _updateExpense(Expense originalExpense, Expense newExpense) {
final expenseIndex = _registeredExpenses.indexOf(originalExpense);
setState(() {
_registeredExpenses[expenseIndex] = newExpense;
});
_saveExpenses();
}

void _openAddExpenseOverlay() {
showModalBottomSheet(
context: context,
Expand All @@ -81,33 +101,44 @@ class _ExpensesState extends State<Expenses> {
);
}

void _openEditExpenseOverlay(Expense expense) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (ctx) => NewExpense(
onAddExpense: (newExpense) {
_updateExpense(expense, newExpense);
},
expense: expense,
),
);
}

@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;

// Issue 4: Hardcoded breakpoint for responsive layout
Widget mainContent =
const Center(child: Text("No Expenses added. \n Add Something?"));

if (_registeredExpenses.isNotEmpty) {
mainContent = ExpensesList(
expenses: _registeredExpenses,
onRemoveExpense: _removeExpense,
onEditExpense: _openEditExpenseOverlay,
);
}
return Scaffold(
appBar: AppBar(
// Issue 5: Hardcoded text styles in AppBar
title: const Text(
title: Text(
'Expenses Tracker',
style: TextStyle(fontSize: 23, fontWeight: FontWeight.bold),
style: Theme.of(context).textTheme.titleLarge,
),
actions: [
IconButton(
onPressed: _openAddExpenseOverlay, icon: const Icon(Icons.add))
],
),
// Issue 6: Fixed width breakpoint for layout switching
body: width < 600
? Column(
children: [
Expand All @@ -128,4 +159,4 @@ class _ExpensesState extends State<Expenses> {
],
));
}
}
}
56 changes: 31 additions & 25 deletions lib/widgets/expenses_list/expense_item.dart
Original file line number Diff line number Diff line change
@@ -1,44 +1,50 @@
import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';
// import 'expense_item.dart';

class ExpenseItem extends StatelessWidget {
const ExpenseItem(this.expense, {super.key});
const ExpenseItem(this.expense, {super.key, this.onEdit});
final Expense expense;
final VoidCallback? onEdit;

@override
Widget build(BuildContext context) {
return Card(
child: Padding(
// Issue 1: Excessive padding making cards too large
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 36),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Issue 2: Hardcoded text style instead of using theme
Text(expense.title,
style:
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)
// Theme.of(context).textTheme.titleLarge
),
// Issue 3: Small spacing between elements
Text(
expense.title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
Row(children: [
// Issue 4: No currency symbol localization
Text('\$${expense.amount.toStringAsFixed(2)}'),
const Spacer(),
Row(
children: [
Icon(categoryIcons[expense.category]),
// Issue 5: Large gap between icon and date
const SizedBox(width: 20),
Text(expense.formattedDate)
],
)
])
Row(
children: [
Text(
'\$${expense.amount.toStringAsFixed(2)}',
),
const Spacer(),
Row(
children: [
Icon(categoryIcons[expense.category]),
const SizedBox(width: 8),
Text(expense.formattedDate),
],
),
if (onEdit != null)
IconButton(
icon: const Icon(Icons.edit),
onPressed: onEdit,
),
],
),
],
),
),
);
}
}
}
13 changes: 8 additions & 5 deletions lib/widgets/expenses_list/expenses_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ class ExpensesList extends StatelessWidget {
super.key,
required this.expenses,
required this.onRemoveExpense,
required this.onEditExpense,
});
final List<Expense> expenses;
final void Function(Expense expense) onRemoveExpense;
final void Function(Expense expense) onEditExpense;

@override
Widget build(BuildContext context) {
Future.delayed(const Duration(milliseconds: 800));

final reversedExpenses = expenses.reversed.toList();

return ListView.builder(
itemCount: expenses.length > 5 ? 5 : expenses.length,
itemCount: expenses.length,
itemBuilder: (context, index) => Dismissible(
key: ValueKey(reversedExpenses[index]),
background: Container(
Expand Down Expand Up @@ -48,7 +48,10 @@ class ExpensesList extends StatelessWidget {
onDismissed: (direction) {
onRemoveExpense(reversedExpenses[index]);
},
child: ExpenseItem(reversedExpenses[index])),
child: ExpenseItem(
reversedExpenses[index],
onEdit: () => onEditExpense(reversedExpenses[index]),
)),
);
}
}
}
Loading