Working auth and added shared notes

This commit is contained in:
Nathan Anderson
2023-07-27 01:40:26 -06:00
parent 18aad2b3d5
commit 83393807c7
68 changed files with 2138 additions and 661 deletions
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:helpers/helpers/misc_build/build_media.dart';
import 'package:rluv/features/account/signup.dart';
import 'package:rluv/global/styles.dart';
import '../../global/api.dart';
import '../../global/widgets/ui_button.dart';
import '../../main.dart';
import 'login.dart';
class AccountCreateScreen extends ConsumerStatefulWidget {
const AccountCreateScreen({super.key});
@override
ConsumerState<AccountCreateScreen> createState() =>
_AccountCreateScreenState();
}
enum _AccountScreen { options, login, signup }
class _AccountCreateScreenState extends ConsumerState<AccountCreateScreen> {
_AccountScreen currentScreen = _AccountScreen.options;
static final signupFormKey = GlobalKey<FormState>();
static final loginFormKey = GlobalKey<FormState>();
bool usingUsername = true;
bool hasFamilyCode = false;
@override
Widget build(BuildContext context) {
if (ref.watch(tokenProvider) != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (ctx) => const Home()),
(r) => false,
);
},
);
}
final screen = BuildMedia(context).size;
return Scaffold(
backgroundColor: Styles.purpleNurple,
body: SafeArea(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: currentScreen == _AccountScreen.options
? Center(
child: SizedBox(
width: screen.width * 0.5 > 400 ? 400 : screen.width * 0.5,
child: Column(
children: [
const Spacer(),
const Text(
'Welcome!',
style: TextStyle(fontSize: 28),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Image.asset("assets/app_icon.png",
height:
screen.width > 500 ? 250 : screen.width * 0.5,
width: screen.width > 500
? 250
: screen.width * 0.5),
),
UiButton(
color: Styles.sunflower,
onPressed: () {
setState(
() => currentScreen = _AccountScreen.signup,
);
},
text: 'Signup',
),
const SizedBox(height: 20),
UiButton(
color: Styles.flounderBlue,
onPressed: () {
setState(
() => currentScreen = _AccountScreen.login,
);
},
text: 'Login',
),
const Spacer(),
],
),
),
)
: currentScreen == _AccountScreen.login
? Login(formKey: loginFormKey, exitNav: exitNav())
: Signup(
formKey: signupFormKey,
exitNav: exitNav(),
),
),
),
);
}
Widget exitNav() => Align(
alignment: Alignment.topLeft,
child: IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () {
setState(
() => currentScreen = _AccountScreen.options,
);
},
),
);
}
+232
View File
@@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:helpers/helpers/misc_build/build_media.dart';
import 'package:helpers/helpers/print.dart';
import '../../global/api.dart';
import '../../global/styles.dart';
import '../../global/utils.dart';
import '../../global/widgets/ui_button.dart';
class Login extends ConsumerStatefulWidget {
const Login({super.key, required this.formKey, required this.exitNav});
final GlobalKey<FormState> formKey;
final Widget exitNav;
@override
ConsumerState<Login> createState() => _LoginState();
}
class _LoginState extends ConsumerState<Login> {
bool usingUsername = false;
final emailController = TextEditingController();
final usernameController = TextEditingController();
final passwordController = TextEditingController();
final focusNodes = [
FocusNode(),
FocusNode(),
FocusNode(),
];
@override
Widget build(BuildContext context) {
final screen = BuildMedia(context).size;
return Form(
key: widget.formKey,
child: Stack(
children: [
Column(
children: [
const Spacer(),
const Text(
'Login',
style: TextStyle(fontSize: 24),
),
const Spacer(flex: 2),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
const Padding(
padding: EdgeInsets.all(2.0),
child: Text(
'Username',
style: TextStyle(
fontSize: 16,
),
),
),
Checkbox(
activeColor: Styles.lavender,
onChanged: (bool? value) {
setState(() => usingUsername = true);
},
value: usingUsername,
),
],
),
const SizedBox(
width: 35,
),
Column(
children: [
const Padding(
padding: EdgeInsets.all(2.0),
child: Text(
'Email',
style: TextStyle(
fontSize: 16,
),
),
),
Checkbox(
activeColor: Styles.lavender,
onChanged: (bool? value) {
setState(() => usingUsername = false);
},
value: !usingUsername,
),
],
),
],
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: !usingUsername
? generateTextField(
controller: emailController,
size: screen,
text: 'Email: ',
validatorFunc: (s) {
if (s != null && s.isNotEmpty && !isEmailValid(s)) {
return 'Email entered is invalid';
}
return null;
},
index: 0)
: generateTextField(
controller: usernameController,
size: screen,
text: 'Username: ',
validatorFunc: (s) {
if (s == null || s.isEmpty) {
return 'Invalid Username';
}
if (s.length < 3) {
return 'Username Not 3 Letters Long';
}
if (s.length > 20) {
return 'Username Too Long';
}
if (!isUsernameValid(s)) {
return 'Letters, Numbers, and ., -, _ Allowed';
}
return null;
},
index: 0),
),
generateTextField(
controller: passwordController,
size: screen,
text: 'Password: ',
validatorFunc: (s) {
if (s != null && s.length < 6) {
return 'Please do a better password (you have to)';
}
return null;
},
index: 1,
isPassword: true),
const Spacer(flex: 2),
UiButton(
width: screen.width * 0.85 > 400 ? 400 : screen.width * 0.85,
showLoading: true,
onPressed: () => login(),
text: 'Submit',
color: Styles.seaweedGreen,
),
const Spacer(),
],
),
widget.exitNav,
],
),
);
}
Widget generateTextField({
required String text,
String? label,
required int index,
required TextEditingController controller,
isPassword = false,
required Size size,
required String? Function(String?) validatorFunc,
}) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
text,
style: TextStyle(fontSize: size.width < 350 ? 16 : 20),
),
),
const Spacer(),
Container(
decoration: Styles.boxLavenderBubble,
width: size.width * 0.5 > 300 ? 300 : size.width * 0.5,
child: TextFormField(
validator: validatorFunc,
controller: controller,
decoration: Styles.inputLavenderBubble(labelText: label),
obscureText: isPassword,
focusNode: focusNodes[index],
style: const TextStyle(fontSize: 16),
onFieldSubmitted: (_) {
if (index != focusNodes.length - 1) {
focusNodes[index + 1].requestFocus();
}
},
),
),
SizedBox(
width: size.width * 0.05,
),
],
),
);
}
Future login() async {
try {
if (widget.formKey.currentState != null &&
!widget.formKey.currentState!.validate()) {
return;
}
final data =
await ref.read(apiProvider.notifier).post(path: 'auth/login', data: {
'username':
usernameController.text.isEmpty ? null : usernameController.text,
'email': emailController.text.isEmpty ? null : emailController.text,
'password': passwordController.text,
});
final bool success = data?['success'] ?? false;
showSnack(
ref: ref,
text: data?['message'] ?? success
? 'Login successful'
: 'Login unsuccessful',
type: !success ? SnackType.error : SnackType.success);
printAmber(data);
} catch (err, st) {
printRed('Error in login: $err\n$st');
}
}
}
+250
View File
@@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:helpers/helpers/misc_build/build_media.dart';
import 'package:helpers/helpers/print.dart';
import '../../global/api.dart';
import '../../global/styles.dart';
import '../../global/utils.dart';
import '../../global/widgets/ui_button.dart';
class Signup extends ConsumerStatefulWidget {
const Signup({super.key, required this.formKey, required this.exitNav});
final GlobalKey<FormState> formKey;
final Widget exitNav;
@override
ConsumerState<Signup> createState() => _SignupState();
}
class _SignupState extends ConsumerState<Signup> {
bool hasFamilyCode = false;
final nameCotroller = TextEditingController();
final usernameController = TextEditingController();
final emailController = TextEditingController();
final passwordController = TextEditingController();
final familyCodeController = TextEditingController();
final focusNodes = [
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
];
@override
Widget build(BuildContext context) {
final screen = BuildMedia(context).size;
return Form(
key: widget.formKey,
child: Stack(
children: [
Column(
children: [
const Spacer(),
const Text(
'Signup',
style: TextStyle(fontSize: 24),
),
const Spacer(),
generateTextField(
controller: nameCotroller,
size: screen,
text: 'Name:',
validatorFunc: (s) {
if (s == null || s.isEmpty) {
return 'You matter! Enter a name :)';
}
return null;
},
index: 0),
generateTextField(
controller: usernameController,
size: screen,
text: 'Username: ',
validatorFunc: (s) {
if (s == null || s.isEmpty) {
return 'Invalid Username';
}
if (s.length < 3) {
return 'Username Not 3 Letters Long';
}
if (s.length > 20) {
return 'Username Too Long';
}
if (!isUsernameValid(s)) {
return 'Letters, Numbers, and ., -, _ Allowed';
}
return null;
},
index: 1),
generateTextField(
controller: emailController,
size: screen,
validatorFunc: (s) {
if (s != null && s.isNotEmpty && !isEmailValid(s)) {
return 'Email entered is invalid';
}
return null;
},
text: 'Email: (optional)',
index: 2),
generateTextField(
controller: passwordController,
size: screen,
validatorFunc: (s) {
if (s != null && s.length < 6) {
return 'Please do a better password (you have to)';
}
return null;
},
text: 'Password:',
index: 3,
isPassword: true),
const SizedBox(height: 30),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: hasFamilyCode
? Column(
children: [
generateTextField(
controller: familyCodeController,
size: screen,
validatorFunc: (s) {
if (hasFamilyCode) {
if (s == null || s.length != 5) {
return 'Invalid Code';
}
}
return null;
},
text: 'Family Code:',
index: 4,
),
UiButton(
width: screen.width * 0.4,
icon: const Icon(Icons.cancel,
color: Styles.washedStone, size: 20),
onPressed: () {
setState(
() => hasFamilyCode = false,
);
},
text: 'Cancel',
),
],
)
: Padding(
padding: const EdgeInsets.all(8.0),
child: UiButton(
width: screen.width * 0.5 > 400
? 400
: screen.width * 0.5,
text: 'JOIN FAMILY',
// color: Styles.flounderBlue,
onPressed: () {
setState(
() => hasFamilyCode = true,
);
},
),
)),
const Spacer(),
UiButton(
showLoading: true,
onPressed: () => signup(),
text: 'Submit',
color: Styles.seaweedGreen,
),
const Spacer(),
],
),
widget.exitNav,
],
),
);
}
Widget generateTextField({
required String text,
String? label,
required int index,
required TextEditingController controller,
isPassword = false,
required Size size,
required String? Function(String?) validatorFunc,
}) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
text,
style: TextStyle(fontSize: size.width < 350 ? 16 : 20),
),
),
const Spacer(),
Container(
decoration: Styles.boxLavenderBubble,
width: size.width * 0.5 > 300 ? 300 : size.width * 0.5,
child: TextFormField(
validator: validatorFunc,
controller: controller,
decoration: Styles.inputLavenderBubble(labelText: label),
obscureText: isPassword,
focusNode: focusNodes[index],
style: const TextStyle(fontSize: 16),
onFieldSubmitted: (_) {
if (index != focusNodes.length - 1) {
focusNodes[index + 1].requestFocus();
}
},
),
),
SizedBox(
width: size.width * 0.05,
),
],
),
);
}
Future signup() async {
try {
if (widget.formKey.currentState != null &&
!widget.formKey.currentState!.validate()) {
return;
}
final data =
await ref.read(apiProvider.notifier).post(path: 'auth/signup', data: {
'name': nameCotroller.text,
'username':
usernameController.text.isEmpty ? null : usernameController.text,
'email': emailController.text.isEmpty ? null : emailController.text,
'password': passwordController.text,
'family_code': familyCodeController.text.isEmpty
? null
: familyCodeController.text.toUpperCase(),
'budget_name': 'Budget'
});
final success = data?['success'] ?? false;
showSnack(
ref: ref,
text: data?['message'] ?? success
? 'Login successful'
: 'Login unsuccessful',
type: !success ? SnackType.error : SnackType.success);
printAmber(data);
// final user = User.fromJson(data?['user']);
// ref.read(tokenProvider.notifier).state =
} catch (err) {
printRed(err);
}
}
}
+266 -237
View File
@@ -26,256 +26,285 @@ class _BudgetOverviewScreenState extends ConsumerState<BudgetOverviewScreen> {
final budgetListScrollController = ScrollController();
@override
Widget build(BuildContext context) {
final budgetCategories = ref.watch(Store().budgetCategoriesProvider);
final transactions = ref.watch(Store().transactionsProvider);
final budgetCategories = ref.watch(budgetCategoriesProvider);
final transactions = ref.watch(transactionsProvider);
final screen = BuildMedia(context).size;
double netExpense = 0.0;
double netIncome = 0.0;
Map<int, double> budgetCategoryNetMap = {};
netExpense = transactions
.where((t) => t.type == TransactionType.expense)
.fold(netExpense, (net, t) => net + t.amount);
netIncome = transactions
.where((t) => t.type == TransactionType.income)
.fold(netIncome, (net, t) => net + t.amount);
for (final bud in budgetCategories) {
double net = 0.0;
net = transactions
.where((t) => t.budgetCategoryId == bud.id)
.fold(net, (net, t) => net + t.amount);
budgetCategoryNetMap[bud.id!] = net;
}
return RefreshIndicator(
displacement: 20.0,
onRefresh: () async {
final family = ref.read(Store().familyProvider).valueOrNull;
final _ =
ref.read(Store().dashboardProvider.notifier).fetchDashboard(family);
},
child: Column(
children: [
/// TOP HALF, TITLE & OVERVIEW
Container(
height: screen.height * 0.3,
width: screen.width,
color: Styles.purpleNurple,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(flex: 2),
Text(
formatDate(DateTime.now()),
style:
const TextStyle(fontSize: 16, color: Styles.electricBlue),
),
const Spacer(),
const Text('MONTHLY',
style: TextStyle(fontSize: 42, color: Styles.electricBlue)),
const Spacer(flex: 2),
BudgetNetBar(isPositive: true, net: netIncome),
const Spacer(),
BudgetNetBar(isPositive: false, net: netExpense),
const Spacer(),
],
),
),
/// BOTTOM HALF, BUDGET BREAKDOWN
Expanded(
child: Container(
color: Styles.sunflower,
netExpense = transactions
.where((t) => t.type == TransactionType.expense)
.fold(netExpense, (net, t) => net + t.amount);
netIncome = transactions
.where((t) => t.type == TransactionType.income)
.fold(netIncome, (net, t) => net + t.amount);
for (final bud in budgetCategories) {
double net = 0.0;
net = transactions
.where((t) => t.budgetCategoryId == bud.id)
.fold(net, (net, t) => net + t.amount);
budgetCategoryNetMap[bud.id!] = net;
}
return Stack(
children: [
Column(
children: [
/// TOP HALF, TITLE & OVERVIEW
Container(
height: screen.height * 0.3,
width: screen.width,
color: Styles.purpleNurple,
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(14.0),
child: Container(
decoration: BoxDecoration(
color: Styles.blushingPink,
borderRadius: BorderRadius.circular(16.0),
),
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'BUDGET',
style: TextStyle(
fontSize: 28, color: Styles.electricBlue),
),
),
budgetCategories.isEmpty
? Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: screen.width * 0.8,
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'No budget categories created yet, add some!'),
),
ElevatedButton(
onPressed: ref.watch(
Store().budgetProvider) ==
null
? null
: () => showDialog(
context: context,
builder: (context) => Dialog(
shape:
Styles.dialogShape,
backgroundColor:
Styles.dialogColor,
child:
const AddBudgetCategoryDialog()),
),
child: const Text('Add Category'),
),
],
),
),
)
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 2.0, vertical: 4.0),
child: SizedBox(
height: screen.height * 0.36,
child: Scrollbar(
controller: budgetListScrollController,
thumbVisibility: true,
child: ListView(
physics: const BouncingScrollPhysics(),
controller: budgetListScrollController,
shrinkWrap: true,
children: [
...budgetCategories
.mapIndexed((i, category) {
return BudgetCategoryBar(
budgetCategory: category,
currentAmount:
budgetCategoryNetMap[
category.id]!,
index: i,
);
}).toList(),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
SizedBox(
width: 140,
child: ElevatedButton(
onPressed: ref.watch(Store()
.budgetProvider) ==
null
? null
: () => showDialog(
context: context,
builder: (context) => Dialog(
shape: Styles
.dialogShape,
backgroundColor:
Styles
.dialogColor,
child:
const AddBudgetCategoryDialog()),
),
child: const Text(
'Add Category'),
),
),
],
),
const SizedBox(height: 20),
],
),
),
),
),
],
),
),
const Spacer(flex: 2),
Text(
formatDate(DateTime.now()),
style: const TextStyle(
fontSize: 16, color: Styles.electricBlue),
),
const Spacer(),
Row(
children: [
Expanded(
child: Padding(
padding:
const EdgeInsets.only(left: 20.0, right: 15.0),
child: Container(
height: 70,
decoration: BoxDecoration(
color: Styles.seaweedGreen,
borderRadius: BorderRadius.circular(15.0),
),
child: InkWell(
child: const Center(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 20.0, vertical: 14.0),
child: FittedBox(
child: Text(
'Transaction History',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 25),
),
),
),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const TransactionsListview()));
},
),
),
),
),
Padding(
padding: const EdgeInsets.only(right: 20.0),
child: Container(
decoration: BoxDecoration(
color: Styles.purpleNurple,
borderRadius: BorderRadius.circular(40.0),
),
height: 80,
width: 80,
child: IconButton(
icon: const Icon(
Icons.add,
size: 48,
color: Styles.lavender,
),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
backgroundColor: Styles.dialogColor,
shape: Styles.dialogShape,
child: const AddTransactionDialog());
},
);
},
),
),
),
],
),
const Text('MONTHLY',
style:
TextStyle(fontSize: 42, color: Styles.electricBlue)),
const Spacer(flex: 2),
BudgetNetBar(isPositive: true, net: netIncome),
const Spacer(),
BudgetNetBar(isPositive: false, net: netExpense),
const Spacer(),
],
),
),
)
],
),
/// BOTTOM HALF, BUDGET BREAKDOWN
Expanded(
child: Container(
color: Styles.sunflower,
width: screen.width,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: const EdgeInsets.all(14.0),
child: Container(
decoration: BoxDecoration(
color: Styles.blushingPink,
borderRadius: BorderRadius.circular(16.0),
),
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'BUDGET',
style: TextStyle(
fontSize: 28, color: Styles.electricBlue),
),
),
budgetCategories.isEmpty
? Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: screen.width * 0.8,
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'No budget categories created yet, add some!'),
),
ElevatedButton(
onPressed:
ref.watch(budgetProvider) ==
null
? null
: () => showDialog(
context: context,
builder: (context) => Dialog(
shape: Styles
.dialogShape,
backgroundColor:
Styles
.dialogColor,
child:
const AddBudgetCategoryDialog()),
),
child: const Text('Add Category'),
),
],
),
),
)
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 2.0, vertical: 4.0),
child: SizedBox(
height: screen.height * 0.36,
child: Scrollbar(
controller: budgetListScrollController,
thumbVisibility: true,
child: ListView(
physics:
const BouncingScrollPhysics(),
controller:
budgetListScrollController,
shrinkWrap: true,
children: [
...budgetCategories
.mapIndexed((i, category) {
return BudgetCategoryBar(
budgetCategory: category,
currentAmount:
budgetCategoryNetMap[
category.id]!,
index: i,
);
}).toList(),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
SizedBox(
width: 140,
child: ElevatedButton(
onPressed: ref.watch(
budgetProvider) ==
null
? null
: () => showDialog(
context: context,
builder: (context) => Dialog(
shape: Styles
.dialogShape,
backgroundColor:
Styles
.dialogColor,
child:
const AddBudgetCategoryDialog()),
),
child: const Text(
'Add Category'),
),
),
],
),
const SizedBox(height: 20),
],
),
),
),
),
],
),
),
),
const Spacer(),
Row(
children: [
Expanded(
child: Padding(
padding:
const EdgeInsets.only(left: 20.0, right: 15.0),
child: Container(
height: 70,
decoration: BoxDecoration(
color: Styles.seaweedGreen,
borderRadius: BorderRadius.circular(15.0),
),
child: InkWell(
child: const Center(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 20.0, vertical: 14.0),
child: FittedBox(
child: Text(
'Transaction History',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 25),
),
),
),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const TransactionsListview()));
},
),
),
),
),
Padding(
padding: const EdgeInsets.only(right: 20.0),
child: Container(
decoration: BoxDecoration(
color: Styles.purpleNurple,
borderRadius: BorderRadius.circular(40.0),
),
height: 80,
width: 80,
child: IconButton(
icon: const Icon(
Icons.add,
size: 48,
color: Styles.lavender,
),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
backgroundColor: Styles.dialogColor,
shape: Styles.dialogShape,
child: const AddTransactionDialog());
},
);
},
),
),
),
],
),
const Spacer(),
],
),
),
)
],
),
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 5.0, right: 5.0),
child: IconButton(
icon: const Icon(Icons.loop),
color: Styles.seaweedGreen,
onPressed: () =>
ref.read(dashboardProvider.notifier).fetchDashboard(),
),
),
),
IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 150),
opacity: ref.watch(loadingStateProvider) ? 0.75 : 0.0,
child: Container(
color: Colors.black12,
height: screen.height,
width: screen.width,
child: const Center(
child: CircularProgressIndicator(
color: Styles.lavender,
strokeWidth: 2.5,
),
),
),
),
)
],
);
}
}
@@ -16,7 +16,7 @@ class TransactionsListview extends ConsumerStatefulWidget {
class _TransactionsListviewState extends ConsumerState<TransactionsListview> {
@override
Widget build(BuildContext context) {
final transactions = ref.watch(Store().transactionsProvider);
final transactions = ref.watch(transactionsProvider);
transactions.sort(
(a, b) => b.date.compareTo(a.date),
);
@@ -8,6 +8,8 @@ import 'package:rluv/models/budget_category_model.dart';
import '../../../global/api.dart';
import '../../../global/store.dart';
import '../../../global/utils.dart';
import '../../../global/widgets/ui_button.dart';
class AddBudgetCategoryDialog extends ConsumerStatefulWidget {
const AddBudgetCategoryDialog({super.key});
@@ -19,8 +21,6 @@ class AddBudgetCategoryDialog extends ConsumerStatefulWidget {
class _AddBudgetCategoryDialogState
extends ConsumerState<AddBudgetCategoryDialog> {
bool loading = false;
bool complete = false;
late final Budget? budget;
final categoryNameController = TextEditingController();
final amountController = TextEditingController();
@@ -34,7 +34,7 @@ class _AddBudgetCategoryDialogState
final formKey = GlobalKey<FormState>();
@override
void initState() {
budget = ref.read(Store().budgetProvider);
budget = ref.read(budgetProvider);
WidgetsBinding.instance.addPostFrameCallback((_) {
categoryFocusNode.requestFocus();
});
@@ -46,20 +46,10 @@ class _AddBudgetCategoryDialogState
if (budget == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Could not get your budget'),
),
);
});
return Container();
} else if (complete) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Budget Category Added!'),
),
showSnack(
ref: ref,
text: 'Could not get your budget',
type: SnackType.error,
);
});
return Container();
@@ -215,10 +205,20 @@ class _AddBudgetCategoryDialogState
),
),
),
ElevatedButton(
onPressed: () => submitCategory(colors[selectedColorIndex]),
child: const Text('SAVE'),
)
UiButton(
showLoading: true,
color: Styles.lavender,
text: 'SAVE',
onPressed: () =>
submitCategory(colors[selectedColorIndex]).then((_) {
Navigator.pop(context);
showSnack(
ref: ref,
text: 'Budget Category Added!',
type: SnackType.success,
);
}),
),
],
),
),
@@ -227,7 +227,6 @@ class _AddBudgetCategoryDialogState
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
if (loading) const Center(child: CircularProgressIndicator()),
],
),
);
@@ -238,9 +237,6 @@ class _AddBudgetCategoryDialogState
printPink('Failed validation');
return;
}
setState(
() => loading = true,
);
final newBudget = BudgetCategory(
id: null,
amount: double.parse(amountController.text),
@@ -249,17 +245,16 @@ class _AddBudgetCategoryDialogState
name: categoryNameController.text,
);
final budgetData =
await Api().post(path: 'budget_category', data: newBudget.toJson());
final budgetData = await ref
.read(apiProvider.notifier)
.post(path: 'budget_category', data: newBudget.toJson());
final success = budgetData != null ? budgetData['success'] as bool : false;
if (success) {
ref
.read(Store().dashboardProvider.notifier)
.read(dashboardProvider.notifier)
.add({'budget_categories': budgetData});
} else {
showSnack(ref: ref, text: 'Could not add budget', type: SnackType.error);
}
complete = true;
setState(() {
loading = false;
});
}
}
@@ -2,10 +2,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:helpers/helpers/misc_build/build_media.dart';
import 'package:rluv/global/utils.dart';
import '../../../global/api.dart';
import '../../../global/store.dart';
import '../../../global/styles.dart';
import '../../../global/widgets/ui_button.dart';
import '../../../models/budget_category_model.dart';
import '../../../models/transaction_model.dart';
@@ -20,8 +22,7 @@ class AddTransactionDialog extends ConsumerStatefulWidget {
}
class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
bool loading = false;
bool complete = false;
late DateTime selectedDate;
late final TextEditingController amountController;
late final TextEditingController memoController;
@@ -38,11 +39,13 @@ class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
TextEditingController(text: widget.transaction!.amount.toString());
memoController = TextEditingController(text: widget.transaction!.memo);
transactionType = widget.transaction!.type;
selectedDate = widget.transaction!.date;
} else {
amountController = TextEditingController();
memoController = TextEditingController();
selectedDate = DateTime.now();
}
final categories = ref.read(Store().budgetCategoriesProvider);
final categories = ref.read(budgetCategoriesProvider);
if (categories.isNotEmpty) {
selectedBudgetCategory = categories.first;
}
@@ -51,21 +54,8 @@ class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
@override
Widget build(BuildContext context) {
if (complete) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.transaction != null
? 'Transaction updated!'
: 'Transaction added!'),
),
);
});
return Container();
}
final List<BudgetCategory> budgetCategories =
ref.read(Store().budgetCategoriesProvider);
ref.read(budgetCategoriesProvider);
return SizedBox(
width: BuildMedia(context).width * Styles.dialogScreenWidthFactor,
child: budgetCategories.isEmpty
@@ -260,9 +250,20 @@ class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
onFieldSubmitted: (_) {},
),
),
ElevatedButton(
onPressed: loading ? null : () => submitTransaction(),
child: const Text('Add'),
UiButton(
showLoading: true,
text: 'ADD',
color: Styles.lavender,
onPressed: () => submitTransaction().then((_) {
Navigator.pop(context);
showSnack(
ref: ref,
text: widget.transaction != null
? 'Transaction updated!'
: 'Transaction added!',
type: SnackType.success,
);
}),
),
],
),
@@ -270,12 +271,9 @@ class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
}
Future submitTransaction() async {
setState(() {
loading = true;
});
Map<String, dynamic>? data;
if (widget.transaction != null) {
data = await Api().put(
data = await ref.read(apiProvider.notifier).put(
path: 'transactions',
data: Transaction.copyWith(widget.transaction!, {
'memo': memoController.text.isNotEmpty ? memoController.text : null,
@@ -285,7 +283,7 @@ class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
: selectedBudgetCategory.id,
}).toJson());
} else {
data = await Api().post(
data = await ref.read(apiProvider.notifier).post(
path: 'transactions',
data: Transaction(
amount: double.parse(amountController.text),
@@ -304,26 +302,17 @@ class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
final success = data != null ? data['success'] as bool : false;
if (success) {
if (widget.transaction != null) {
ref
.read(Store().dashboardProvider.notifier)
.update({'transactions': data});
ref.read(dashboardProvider.notifier).update({'transactions': data});
} else {
ref
.read(Store().dashboardProvider.notifier)
.add({'transactions': data});
ref.read(dashboardProvider.notifier).add({'transactions': data});
}
complete = true;
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.transaction != null
showSnack(
ref: ref,
text: widget.transaction != null
? 'Failed to edit transaction'
: 'Failed to add transaction'),
),
);
: 'Failed to add transaction',
type: SnackType.error);
}
setState(() {
loading = false;
});
}
}
@@ -14,7 +14,7 @@ class BudgetCategoryBar extends StatefulWidget {
required this.currentAmount,
required this.index,
this.height = 32,
this.innerPadding = 1.5});
this.innerPadding = 2.5});
final int index;
final double currentAmount;
@@ -27,6 +27,7 @@ class BudgetCategoryBar extends StatefulWidget {
class _BudgetCategoryBarState extends State<BudgetCategoryBar> {
double percentSpent = 0.0;
@override
void initState() {
Future.delayed(Duration(milliseconds: min(1600, 200 * widget.index)), () {
@@ -76,7 +77,7 @@ class _BudgetCategoryBarState extends State<BudgetCategoryBar> {
height: widget.height,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(14.0),
borderRadius: BorderRadius.circular(13.0),
),
),
Padding(
@@ -35,8 +35,8 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
@override
Widget build(BuildContext context) {
final transaction = ref.watch(Store().transactionsProvider)[widget.index];
final budgetCategories = ref.read(Store().budgetCategoriesProvider);
final transaction = ref.watch(transactionsProvider)[widget.index];
final budgetCategories = ref.read(budgetCategoriesProvider);
if (transaction.type == TransactionType.expense) {
budgetCategory = budgetCategories.singleWhere(
(category) => category.id == transaction.budgetCategoryId,
+254 -17
View File
@@ -1,8 +1,15 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:helpers/helpers/misc_build/build_media.dart';
import 'package:helpers/helpers/print.dart';
import 'package:rluv/global/utils.dart';
import 'package:rluv/models/shared_note.dart';
import '../../../global/api.dart';
import '../../../global/store.dart';
import '../../../global/styles.dart';
import '../../../global/widgets/ui_button.dart';
class SharedNotesScreen extends ConsumerStatefulWidget {
const SharedNotesScreen({super.key, required this.initialData});
@@ -15,7 +22,8 @@ class SharedNotesScreen extends ConsumerStatefulWidget {
class _SharedNotesScreenState extends ConsumerState<SharedNotesScreen> {
@override
Widget build(BuildContext context) {
final sharedNotes = ref.watch(Store().sharedNotesProvider);
final screen = BuildMedia(context).size;
final sharedNotes = ref.watch(sharedNotesProvider);
return Column(
children: [
const Padding(
@@ -26,26 +34,255 @@ class _SharedNotesScreenState extends ConsumerState<SharedNotesScreen> {
textAlign: TextAlign.center,
),
),
if (sharedNotes.isEmpty) const Text('Add notes to get started:'),
if (sharedNotes.isEmpty) ...[
const Text('Add notes to get started:'),
],
if (sharedNotes.isNotEmpty)
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (BuildContext context, int index) {
final note = sharedNotes[index];
return SizedBox(
width: BuildMedia(context).width * 0.4,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(note.content),
SizedBox(
height: screen.height * 0.65,
child: GridView.builder(
itemCount: sharedNotes.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (BuildContext context, int index) {
final note = sharedNotes[index];
final colorBrightness = note.color == null
? Brightness.light
: getBrightness(note.color!);
final textStyle = TextStyle(
fontSize: 16.0,
color: colorBrightness == Brightness.light
? Colors.black54
: Colors.white70,
);
return Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
child: Card(
color: note.color ?? Styles.washedStone,
child: SizedBox(
width: screen.width * 0.4,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
FittedBox(
child: Text(note.title,
style: TextStyle(fontSize: 20))),
Flexible(
child: Text(
note.content.length > 80
? "${note.content.substring(0, 75)}..."
: note.content,
style: textStyle,
),
),
],
),
),
),
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
backgroundColor: Colors.transparent,
context: context,
builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).viewInsets.bottom),
child: NoteBottomSheet(
index: index,
),
);
});
},
),
),
);
},
);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0, horizontal: 12.0),
child: UiButton(
text: 'Add Note',
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
backgroundColor: Colors.transparent,
context: context,
builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom),
child: const NoteBottomSheet(
index: null,
),
);
});
}),
),
],
);
}
}
class NoteBottomSheet extends ConsumerStatefulWidget {
const NoteBottomSheet({super.key, required this.index});
final int? index;
@override
ConsumerState<NoteBottomSheet> createState() => _NoteBottomSheetState();
}
class _NoteBottomSheetState extends ConsumerState<NoteBottomSheet> {
late final SharedNote? oldNote;
late SharedNote newNote;
final titleFocusNode = FocusNode();
final contentFocusNode = FocusNode();
late final TextEditingController titleController;
late final TextEditingController contentController;
@override
void initState() {
super.initState();
if (widget.index == null) {
oldNote = null;
final family = ref.read(familyProvider);
final user = ref.read(userProvider);
newNote = SharedNote(
familyId: family!.id,
createdByUserId: user!.id!,
title: 'Title',
content: '',
tagIds: [],
color: Styles.washedStone,
isMarkdown: false,
);
} else {
oldNote = ref.read(sharedNotesProvider).elementAt(widget.index!);
newNote = SharedNote.copy(oldNote!);
}
titleController = TextEditingController(text: newNote.title);
contentController = TextEditingController(text: newNote.content);
WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) => titleFocusNode.requestFocus(),
);
}
@override
Widget build(BuildContext context) {
final screen = BuildMedia(context).size;
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Container(
width: screen.width,
decoration: BoxDecoration(
color: newNote.color ?? Styles.washedStone,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20.0), topRight: Radius.circular(20.0)),
boxShadow: const [
BoxShadow(
color: Colors.black26,
spreadRadius: 5.0,
blurRadius: 2.0,
offset: Offset(0, -2))
]),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
style: const TextStyle(fontSize: 18.0),
maxLines: 1,
focusNode: titleFocusNode,
textAlign: TextAlign.left,
controller: titleController,
// decoration: Styles.inputLavenderBubble(),
),
TextField(
style: const TextStyle(fontSize: 18.0),
maxLines: 6,
focusNode: contentFocusNode,
textAlign: TextAlign.left,
controller: contentController,
// decoration: Styles.inputLavenderBubble(),
),
Padding(
padding:
const EdgeInsets.symmetric(vertical: 28.0, horizontal: 36.0),
child: UiButton(
onPressed: !noteChanged(oldNote, newNote)
? null
: () async {
newNote.content = contentController.text;
newNote.title = titleController.text;
try {
Response? res;
if (widget.index == null) {
res = await ref
.read(apiProvider)
.post('shared_notes', data: newNote.toJson());
} else {
res = await ref
.read(apiProvider)
.put('shared_notes', data: newNote.toJson());
}
if (res.data != null && res.data['success']) {
if (widget.index == null) {
ref
.read(dashboardProvider.notifier)
.add({'shared_notes': res.data});
} else {
ref
.read(dashboardProvider.notifier)
.update({'shared_notes': res.data});
}
showSnack(
ref: ref,
text: 'Added note',
type: SnackType.success);
} else {
showSnack(
ref: ref,
text: res.data['message'] ??
'Unexpected error occurred',
type: SnackType.error);
}
} catch (err) {
showSnack(
ref: ref,
text: 'Unexpected error occurred',
type: SnackType.error);
printRed(err);
}
Navigator.pop(context);
},
text: 'Save',
),
),
],
),
),
);
}
bool noteChanged(SharedNote? n, SharedNote m) {
if (n == null) return true;
if (n.content != m.content) return true;
if (n.title != m.content) return true;
if (n.tagIds != m.tagIds) return true;
if (n.color != m.color) return true;
return false;
}
}
@@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../global/api.dart';
import '../../../global/store.dart';
import '../../../global/widgets/ui_button.dart';
class SettingsScreen extends ConsumerStatefulWidget {
const SettingsScreen({super.key});
@@ -11,9 +15,29 @@ class SettingsScreen extends ConsumerStatefulWidget {
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
@override
Widget build(BuildContext context) {
return const Column(
final user = ref.read(userProvider);
final family = ref.read(familyProvider);
return Column(
children: [
Text('Settings'),
const Text('Settings'),
const SizedBox(height: 20),
Text(user!.name),
Text("Username: ${user.username ?? 'N/A'}"),
Text("Email: ${user.email ?? 'N/A'}"),
Text("Family Code: ${family?.code ?? 'N/A'}"),
const Spacer(),
UiButton(
onPressed: () {
ref.read(jwtProvider.notifier).revokeToken();
// Navigator.pushAndRemoveUntil(
// context,
// MaterialPageRoute(builder: (ctx) => const AccountCreateScreen()),
// (_) => false,
// );
},
text: 'Sign Out',
),
const SizedBox(height: 20),
],
);
}