initial state done

This commit is contained in:
doman 2023-07-29 23:54:51 +02:00
parent 669dff82d4
commit 43dd4a8543
12 changed files with 449 additions and 34 deletions

View file

@ -40,6 +40,14 @@ class ApiClient {
return headers; return headers;
} }
Map<String, dynamic> _jsonDecode(http.Response response) {
try {
return jsonDecode(utf8.decode(response.bodyBytes));
} catch (e) {
throw Exception('Response returned status code: ${response.statusCode}');
}
}
Future<Map<String, dynamic>> get(String path) async { Future<Map<String, dynamic>> get(String path) async {
final response = await httpClient.get( final response = await httpClient.get(
Uri.parse('$baseUrl$path'), Uri.parse('$baseUrl$path'),
@ -50,7 +58,7 @@ class ApiClient {
throw Exception('Response returned status code: ${response.statusCode}'); throw Exception('Response returned status code: ${response.statusCode}');
} }
return jsonDecode(response.body); return _jsonDecode(response);
} }
Future<Map<String, dynamic>> post(String path, Map<String, dynamic> body) async { Future<Map<String, dynamic>> post(String path, Map<String, dynamic> body) async {
@ -64,7 +72,7 @@ class ApiClient {
throw Exception('Response returned status code: ${response.statusCode}'); 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}'); throw Exception('Response returned status code: ${response.statusCode}');
} }
return jsonDecode(response.body); return _jsonDecode(response);
} }
Future<Map<String, dynamic>> patch(String path, Map<String, dynamic> body) async { Future<Map<String, dynamic>> patch(String path, Map<String, dynamic> body) async {
@ -92,7 +100,7 @@ class ApiClient {
throw Exception('Response returned status code: ${response.statusCode}'); throw Exception('Response returned status code: ${response.statusCode}');
} }
return jsonDecode(response.body); return _jsonDecode(response);
} }
Future<void> login(String username, String password) async { Future<void> login(String username, String password) async {
@ -114,11 +122,11 @@ class ApiClient {
throw Exception('Failed to login'); throw Exception('Failed to login');
} }
final token = jsonDecode(response.body)['access_token']; final token = _jsonDecode(response)['access_token'];
this.token = token; this.token = token;
window.localStorage['token'] = token; window.localStorage['token'] = token;
final refreshToken = jsonDecode(response.body)['refresh_token']; final refreshToken = _jsonDecode(response)['refresh_token'];
this.refreshToken = refreshToken; this.refreshToken = refreshToken;
window.localStorage['refreshToken'] = refreshToken; window.localStorage['refreshToken'] = refreshToken;
} }
@ -141,7 +149,37 @@ class ApiClient {
window.localStorage['refreshToken'] = refreshToken!; window.localStorage['refreshToken'] = refreshToken!;
} }
Future<Map<String, dynamic>> getDiary() async { Future<Map<String, dynamic>> getDiary({required DateTime date}) async {
return await get("/diary?date=2023-07-29"); 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<Map<String, dynamic>> getProducts(String q) async {
var response = await get("/product?${Uri(queryParameters: {"q": q}).query}");
return response;
}
Future<void> 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);
} }
} }

View file

@ -23,7 +23,7 @@ class Diary {
Diary.fromJson(Map<String, dynamic> map): Diary.fromJson(Map<String, dynamic> map):
id = map['id'] as int, id = map['id'] as int,
date = DateTime.parse(map['date']), date = DateTime.parse(map['date']),
meals = <Meal>[], meals = (map['meals'] as List<dynamic>).map((e) => Meal.fromJson(e as Map<String, dynamic>)).toList(),
calories = map['calories'] as double, calories = map['calories'] as double,
protein = map['protein'] as double, protein = map['protein'] as double,
carb = map['carb'] as double, carb = map['carb'] as double,

View file

@ -21,4 +21,14 @@ class Entry {
required this.fat, required this.fat,
required this.carb, required this.carb,
}); });
Entry.fromJson(Map<String, dynamic> map):
id = map['id'] as int,
grams = map['grams'] as double,
product = Product.fromJson(map['product'] as Map<String, dynamic>),
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;
} }

View file

@ -3,8 +3,35 @@ import 'package:fooder_web/models/entry.dart';
class Meal { class Meal {
final List<Entry> entries; final List<Entry> 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.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<String, dynamic> map):
entries = (map['entries'] as List<dynamic>).map((e) => Entry.fromJson(e as Map<String, dynamic>)).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;
} }

View file

@ -14,4 +14,12 @@ class Product {
required this.carb, required this.carb,
required this.fat, required this.fat,
}); });
Product.fromJson(Map<String, dynamic> 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;
} }

128
lib/screens/add_entry.dart Normal file
View file

@ -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<AddEntryScreen> createState() => _AddEntryScreen();
}
class _AddEntryScreen extends State<AddEntryScreen> {
final gramsController = TextEditingController();
final productNameController = TextEditingController();
List<Product> 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<void> _getProducts() async {
var productsMap = await widget.apiClient.getProducts(productNameController.text);
setState(() {
products = (productsMap['products'] as List<dynamic>).map((e) => Product.fromJson(e as Map<String, dynamic>)).toList();
});
}
void showError(String message)
{
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message, textAlign: TextAlign.center),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
Future<void> _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: <Widget>[
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),
),
);
}
}

View file

@ -72,6 +72,10 @@ class _LoginScreen extends State<LoginScreen> {
} }
Future<void> _asyncInitState() async { Future<void> _asyncInitState() async {
if (widget.apiClient.refreshToken == null) {
return;
}
try { try {
await widget.apiClient.refresh(); await widget.apiClient.refresh();
showText("Welcome back!"); showText("Welcome back!");
@ -96,17 +100,18 @@ class _LoginScreen extends State<LoginScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
TextFormField( TextFormField(
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Username', labelText: 'Username',
), ),
controller: usernameController, controller: usernameController,
), ),
TextFormField( TextFormField(
obscureText: true, obscureText: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Password', labelText: 'Password',
), ),
controller: passwordController, controller: passwordController,
onFieldSubmitted: (_) => _login()
), ),
FilledButton( FilledButton(
onPressed: _login, onPressed: _login,

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fooder_web/screens/based.dart'; import 'package:fooder_web/screens/based.dart';
import 'package:fooder_web/models/meal.dart'; import 'package:fooder_web/screens/login.dart';
import 'package:fooder_web/models/entry.dart'; import 'package:fooder_web/screens/add_entry.dart';
import 'package:fooder_web/models/diary.dart'; import 'package:fooder_web/models/diary.dart';
import 'package:fooder_web/widgets/diary.dart'; import 'package:fooder_web/widgets/diary.dart';
@ -15,6 +15,7 @@ class MainScreen extends BasedScreen {
class _MainScreen extends State<MainScreen> { class _MainScreen extends State<MainScreen> {
Diary? diary; Diary? diary;
DateTime date = DateTime.now();
@override @override
void initState () { void initState () {
@ -23,36 +24,90 @@ class _MainScreen extends State<MainScreen> {
} }
Future<void> _asyncInitState() async { Future<void> _asyncInitState() async {
var diaryMap = await widget.apiClient.getDiary(); var diaryMap = await widget.apiClient.getDiary(date: date);
setState(() { setState(() {
diary = Diary.fromJson(diaryMap); diary = Diary.fromJson(diaryMap);
date = date;
}); });
} }
Future<void> _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<void> _addEntry() async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddEntryScreen(apiClient: widget.apiClient, diary: diary!),
),
).then((_) => _asyncInitState());
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var content; Widget content;
var title = "FOODER"; Widget title;
if (diary != null) { if (diary != null) {
content = Container( content = Container(
constraints: const BoxConstraints(maxWidth: 600), constraints: const BoxConstraints(maxWidth: 720),
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: DiaryWidget(diary: diary!), child: DiaryWidget(diary: diary!),
); );
title = "FOODER - ${diary!.date.year}-${diary!.date.month}-${diary!.date.day}"; title = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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 { } else {
content = const CircularProgressIndicator(); content = const CircularProgressIndicator();
title = const Text("🅵🅾🅾🅳🅴🆁");
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(title), title: title,
), ),
body: Center( body: Center(
child: content, child: content,
), ),
floatingActionButton: FloatingActionButton(
onPressed: _addEntry,
child: const Icon(Icons.add),
),
); );
} }
} }

View file

@ -20,7 +20,36 @@ class DiaryWidget extends StatelessWidget {
MealWidget( MealWidget(
meal: meal, meal: meal,
), ),
Text(diary.date.toString()), Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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),
),
],
),
],
),
),
)
], ],
), ),
); );

View file

@ -12,7 +12,42 @@ class EntryWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text(entry.product.name), child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Text(
entry.product.name,
style: Theme.of(context).textTheme.titleLarge,
),
),
Text("${entry.calories.toStringAsFixed(2)} kcal"),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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),
),
],
),
],
),
); );
} }
} }

View file

@ -16,12 +16,43 @@ class MealWidget extends StatelessWidget {
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 36.0, left: 6.0, right: 6.0, bottom: 6.0), top: 36.0, left: 6.0, right: 6.0, bottom: 6.0),
child: ExpansionTile( child: ExpansionTile(
title: const Text('SEKS Z KOBIETA'), title: Column(
children: <Widget>[ children: <Widget>[
for (var entry in meal.entries) Row(
EntryWidget( children: <Widget>[
entry: entry, Expanded(
child: Text(
meal.name,
style: Theme.of(context).textTheme.titleLarge,
),
),
Text("${meal.calories.toStringAsFixed(2)} kcal"),
],
), ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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: <Widget>[
for (var entry in meal.entries)
EntryWidget(
entry: entry,
),
], ],
), ),
), ),

49
lib/widgets/product.dart Normal file
View file

@ -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: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Text(
product.name,
style: Theme.of(context).textTheme.titleLarge,
),
),
Text("${product.calories.toStringAsFixed(2)} kcal"),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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),
),
],
),
],
),
);
}
}