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;
}
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 {
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<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}');
}
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<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}');
}
return jsonDecode(response.body);
return _jsonDecode(response);
}
Future<void> 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<Map<String, dynamic>> getDiary() async {
return await get("/diary?date=2023-07-29");
Future<Map<String, dynamic>> 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<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):
id = map['id'] as int,
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,
protein = map['protein'] as double,
carb = map['carb'] as double,

View file

@ -21,4 +21,14 @@ class Entry {
required this.fat,
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 {
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.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.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 {
if (widget.apiClient.refreshToken == null) {
return;
}
try {
await widget.apiClient.refresh();
showText("Welcome back!");
@ -96,17 +100,18 @@ class _LoginScreen extends State<LoginScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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,

View file

@ -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<MainScreen> {
Diary? diary;
DateTime date = DateTime.now();
@override
void initState () {
@ -23,36 +24,90 @@ class _MainScreen extends State<MainScreen> {
}
Future<void> _asyncInitState() async {
var diaryMap = await widget.apiClient.getDiary();
var diaryMap = await widget.apiClient.getDiary(date: date);
setState(() {
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
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: <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 {
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),
),
);
}
}

View file

@ -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: <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) {
return Container(
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(
top: 36.0, left: 6.0, right: 6.0, bottom: 6.0),
child: ExpansionTile(
title: const Text('SEKS Z KOBIETA'),
children: <Widget>[
for (var entry in meal.entries)
EntryWidget(
entry: entry,
title: Column(
children: <Widget>[
Row(
children: <Widget>[
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),
),
],
),
],
),
);
}
}