From 43dd4a8543d45ec52cfef31d7e228a5f1881ad88 Mon Sep 17 00:00:00 2001 From: doman Date: Sat, 29 Jul 2023 23:54:51 +0200 Subject: [PATCH] initial state done --- lib/client.dart | 54 +++++++++++++--- lib/models/diary.dart | 2 +- lib/models/entry.dart | 10 +++ lib/models/meal.dart | 29 ++++++++- lib/models/product.dart | 8 +++ lib/screens/add_entry.dart | 128 +++++++++++++++++++++++++++++++++++++ lib/screens/login.dart | 23 ++++--- lib/screens/main.dart | 71 +++++++++++++++++--- lib/widgets/diary.dart | 31 ++++++++- lib/widgets/entry.dart | 37 ++++++++++- lib/widgets/meal.dart | 41 ++++++++++-- lib/widgets/product.dart | 49 ++++++++++++++ 12 files changed, 449 insertions(+), 34 deletions(-) create mode 100644 lib/screens/add_entry.dart create mode 100644 lib/widgets/product.dart diff --git a/lib/client.dart b/lib/client.dart index 5064377..0ecc3f5 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -40,6 +40,14 @@ class ApiClient { return headers; } + Map _jsonDecode(http.Response response) { + try { + return jsonDecode(utf8.decode(response.bodyBytes)); + } catch (e) { + throw Exception('Response returned status code: ${response.statusCode}'); + } + } + Future> get(String path) async { final response = await httpClient.get( Uri.parse('$baseUrl$path'), @@ -50,7 +58,7 @@ class ApiClient { throw Exception('Response returned status code: ${response.statusCode}'); } - return jsonDecode(response.body); + return _jsonDecode(response); } Future> post(String path, Map body) async { @@ -64,7 +72,7 @@ class ApiClient { throw Exception('Response returned status code: ${response.statusCode}'); } - return jsonDecode(response.body); + return _jsonDecode(response); } @@ -78,7 +86,7 @@ class ApiClient { throw Exception('Response returned status code: ${response.statusCode}'); } - return jsonDecode(response.body); + return _jsonDecode(response); } Future> patch(String path, Map body) async { @@ -92,7 +100,7 @@ class ApiClient { throw Exception('Response returned status code: ${response.statusCode}'); } - return jsonDecode(response.body); + return _jsonDecode(response); } Future login(String username, String password) async { @@ -114,11 +122,11 @@ class ApiClient { throw Exception('Failed to login'); } - final token = jsonDecode(response.body)['access_token']; + final token = _jsonDecode(response)['access_token']; this.token = token; window.localStorage['token'] = token; - final refreshToken = jsonDecode(response.body)['refresh_token']; + final refreshToken = _jsonDecode(response)['refresh_token']; this.refreshToken = refreshToken; window.localStorage['refreshToken'] = refreshToken; } @@ -141,7 +149,37 @@ class ApiClient { window.localStorage['refreshToken'] = refreshToken!; } - Future> getDiary() async { - return await get("/diary?date=2023-07-29"); + Future> getDiary({required DateTime date}) async { + var params = { + "date": "${date.year}-${date.month}-${date.day}", + }; + var response = await get("/diary?${Uri(queryParameters: params).query}"); + return response; + } + + void logout() { + token = null; + refreshToken = null; + window.localStorage.remove('token'); + window.localStorage.remove('refreshToken'); + } + + Future> getProducts(String q) async { + var response = await get("/product?${Uri(queryParameters: {"q": q}).query}"); + return response; + } + + Future addEntry({ + required double grams, + required int productId, + required int mealId, + } + ) async { + var entry = { + "grams": grams, + "product_id": productId, + "meal_id": mealId, + }; + await post("/entry", entry); } } diff --git a/lib/models/diary.dart b/lib/models/diary.dart index 6de827b..232d82b 100644 --- a/lib/models/diary.dart +++ b/lib/models/diary.dart @@ -23,7 +23,7 @@ class Diary { Diary.fromJson(Map map): id = map['id'] as int, date = DateTime.parse(map['date']), - meals = [], + meals = (map['meals'] as List).map((e) => Meal.fromJson(e as Map)).toList(), calories = map['calories'] as double, protein = map['protein'] as double, carb = map['carb'] as double, diff --git a/lib/models/entry.dart b/lib/models/entry.dart index 31d9cfc..84622ba 100644 --- a/lib/models/entry.dart +++ b/lib/models/entry.dart @@ -21,4 +21,14 @@ class Entry { required this.fat, required this.carb, }); + + Entry.fromJson(Map map): + id = map['id'] as int, + grams = map['grams'] as double, + product = Product.fromJson(map['product'] as Map), + mealId = map['meal_id'] as int, + calories = map['calories'] as double, + protein = map['protein'] as double, + fat = map['fat'] as double, + carb = map['carb'] as double; } diff --git a/lib/models/meal.dart b/lib/models/meal.dart index 62b9f4a..5bf9bc3 100644 --- a/lib/models/meal.dart +++ b/lib/models/meal.dart @@ -3,8 +3,35 @@ import 'package:fooder_web/models/entry.dart'; class Meal { final List entries; + final int id; + final String name; + final int order; + final double calories; + final double protein; + final double carb; + final double fat; + final int diaryId; - const Meal({ + Meal({ required this.entries, + required this.id, + required this.name, + required this.order, + required this.calories, + required this.protein, + required this.carb, + required this.fat, + required this.diaryId, }); + + Meal.fromJson(Map map): + entries = (map['entries'] as List).map((e) => Entry.fromJson(e as Map)).toList(), + id = map['id'] as int, + name = map['name'] as String, + order = map['order'] as int, + calories = map['calories'] as double, + protein = map['protein'] as double, + carb = map['carb'] as double, + fat = map['fat'] as double, + diaryId = map['diary_id'] as int; } diff --git a/lib/models/product.dart b/lib/models/product.dart index d05366f..5aa3739 100644 --- a/lib/models/product.dart +++ b/lib/models/product.dart @@ -14,4 +14,12 @@ class Product { required this.carb, required this.fat, }); + + Product.fromJson(Map map): + id = map['id'] as int, + name = map['name'] as String, + calories = map['calories'] as double, + protein = map['protein'] as double, + carb = map['carb'] as double, + fat = map['fat'] as double; } diff --git a/lib/screens/add_entry.dart b/lib/screens/add_entry.dart new file mode 100644 index 0000000..d66a5fd --- /dev/null +++ b/lib/screens/add_entry.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:fooder_web/screens/based.dart'; +import 'package:fooder_web/models/product.dart'; +import 'package:fooder_web/models/diary.dart'; +import 'package:fooder_web/widgets/product.dart'; + + +class AddEntryScreen extends BasedScreen { + final Diary diary; + + const AddEntryScreen({super.key, required super.apiClient, required this.diary}); + + @override + State createState() => _AddEntryScreen(); +} + + +class _AddEntryScreen extends State { + final gramsController = TextEditingController(); + final productNameController = TextEditingController(); + List products = []; + + @override + void dispose() { + gramsController.dispose(); + productNameController.dispose(); + super.dispose(); + } + + void popMeDady() { + Navigator.pop( + context, + ); + } + + @override + void initState () { + super.initState(); + _getProducts().then((value) => null); + } + + Future _getProducts() async { + var productsMap = await widget.apiClient.getProducts(productNameController.text); + setState(() { + products = (productsMap['products'] as List).map((e) => Product.fromJson(e as Map)).toList(); + }); + } + + void showError(String message) + { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message, textAlign: TextAlign.center), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + + Future _addEntry() async { + if (products.length != 1) { + showError("Pick product first"); + return; + } + + try { + double.parse(gramsController.text); + } catch (e) { + print(e); + showError("Grams must be a number"); + return; + } + + await widget.apiClient.addEntry( + grams: double.parse(gramsController.text), + productId: products[0].id, + mealId: widget.diary.meals[0].id, + ); + popMeDady(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text("πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†"), + ), + body: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 720), + padding: const EdgeInsets.all(10), + child: ListView( + children: [ + TextFormField( + decoration: const InputDecoration( + labelText: 'Grams', + ), + controller: gramsController, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Product name', + ), + controller: productNameController, + onChanged: (_) => _getProducts(), + ), + for (var product in products) + ListTile( + onTap: () { + setState(() { + products = [product]; + }); + }, + title: ProductWidget( + product: product, + ), + ), + ] + ) + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _addEntry, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/login.dart b/lib/screens/login.dart index 916084d..0a4d6c2 100644 --- a/lib/screens/login.dart +++ b/lib/screens/login.dart @@ -72,6 +72,10 @@ class _LoginScreen extends State { } Future _asyncInitState() async { + if (widget.apiClient.refreshToken == null) { + return; + } + try { await widget.apiClient.refresh(); showText("Welcome back!"); @@ -96,17 +100,18 @@ class _LoginScreen extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ TextFormField( - decoration: const InputDecoration( - labelText: 'Username', - ), - controller: usernameController, + decoration: const InputDecoration( + labelText: 'Username', + ), + controller: usernameController, ), TextFormField( - obscureText: true, - decoration: const InputDecoration( - labelText: 'Password', - ), - controller: passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Password', + ), + controller: passwordController, + onFieldSubmitted: (_) => _login() ), FilledButton( onPressed: _login, diff --git a/lib/screens/main.dart b/lib/screens/main.dart index e044e35..6876837 100644 --- a/lib/screens/main.dart +++ b/lib/screens/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:fooder_web/screens/based.dart'; -import 'package:fooder_web/models/meal.dart'; -import 'package:fooder_web/models/entry.dart'; +import 'package:fooder_web/screens/login.dart'; +import 'package:fooder_web/screens/add_entry.dart'; import 'package:fooder_web/models/diary.dart'; import 'package:fooder_web/widgets/diary.dart'; @@ -15,6 +15,7 @@ class MainScreen extends BasedScreen { class _MainScreen extends State { Diary? diary; + DateTime date = DateTime.now(); @override void initState () { @@ -23,36 +24,90 @@ class _MainScreen extends State { } Future _asyncInitState() async { - var diaryMap = await widget.apiClient.getDiary(); + var diaryMap = await widget.apiClient.getDiary(date: date); setState(() { diary = Diary.fromJson(diaryMap); + date = date; }); } + Future _pickDate() async { + date = (await showDatePicker( + context: context, + initialDate: date, + firstDate: DateTime(2020), + lastDate: DateTime(2025), + ))!; + await _asyncInitState(); + } + + void _logout() async { + widget.apiClient.logout(); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => LoginScreen(apiClient: widget.apiClient), + ), + ); + } + + Future _addEntry() async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddEntryScreen(apiClient: widget.apiClient, diary: diary!), + ), + ).then((_) => _asyncInitState()); + } + @override Widget build(BuildContext context) { - var content; - var title = "FOODER"; + Widget content; + Widget title; if (diary != null) { content = Container( - constraints: const BoxConstraints(maxWidth: 600), + constraints: const BoxConstraints(maxWidth: 720), padding: const EdgeInsets.all(10), child: DiaryWidget(diary: diary!), ); - title = "FOODER - ${diary!.date.year}-${diary!.date.month}-${diary!.date.day}"; + title = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†"), + const Spacer(), + Text( + "${date.year}-${date.month}-${date.day}", + style: const TextStyle(fontSize: 20), + ), + IconButton( + icon: const Icon(Icons.calendar_month), + onPressed: _pickDate, + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.logout), + onPressed: _logout, + ), + ], + ); } else { content = const CircularProgressIndicator(); + title = const Text("πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†"); } return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(title), + title: title, ), body: Center( child: content, ), + floatingActionButton: FloatingActionButton( + onPressed: _addEntry, + child: const Icon(Icons.add), + ), ); } } diff --git a/lib/widgets/diary.dart b/lib/widgets/diary.dart index 9447abd..4eb8566 100644 --- a/lib/widgets/diary.dart +++ b/lib/widgets/diary.dart @@ -20,7 +20,36 @@ class DiaryWidget extends StatelessWidget { MealWidget( meal: meal, ), - Text(diary.date.toString()), + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "carb: ${diary.carb.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "fat: ${diary.fat.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "protein: ${diary.protein.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "calories: ${diary.calories.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ], + ), + ), + ) ], ), ); diff --git a/lib/widgets/entry.dart b/lib/widgets/entry.dart index e891099..8e3815d 100644 --- a/lib/widgets/entry.dart +++ b/lib/widgets/entry.dart @@ -12,7 +12,42 @@ class EntryWidget extends StatelessWidget { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(8), - child: Text(entry.product.name), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + entry.product.name, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + Text("${entry.calories.toStringAsFixed(2)} kcal"), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "carb: ${entry.carb.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "fat: ${entry.fat.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "protein: ${entry.protein.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "amount: ${entry.grams.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + ], + ), + ], + ), ); } } diff --git a/lib/widgets/meal.dart b/lib/widgets/meal.dart index dca69fe..7799918 100644 --- a/lib/widgets/meal.dart +++ b/lib/widgets/meal.dart @@ -16,12 +16,43 @@ class MealWidget extends StatelessWidget { padding: const EdgeInsets.only( top: 36.0, left: 6.0, right: 6.0, bottom: 6.0), child: ExpansionTile( - title: const Text('SEKS Z KOBIETA'), - children: [ - for (var entry in meal.entries) - EntryWidget( - entry: entry, + title: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + meal.name, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + Text("${meal.calories.toStringAsFixed(2)} kcal"), + ], ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "carb: ${meal.carb.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "fat: ${meal.fat.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "protein: ${meal.protein.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + ], + ), + ], + ), + children: [ + for (var entry in meal.entries) + EntryWidget( + entry: entry, + ), ], ), ), diff --git a/lib/widgets/product.dart b/lib/widgets/product.dart new file mode 100644 index 0000000..83c6270 --- /dev/null +++ b/lib/widgets/product.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:fooder_web/models/product.dart'; +import 'dart:core'; + + +class ProductWidget extends StatelessWidget { + final Product product; + + const ProductWidget({super.key, required this.product}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + product.name, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + Text("${product.calories.toStringAsFixed(2)} kcal"), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "carb: ${product.carb.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "fat: ${product.fat.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + Text( + "protein: ${product.protein.toStringAsFixed(2)}", + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + ], + ), + ], + ), + ); + } +}